一、JVM的内存区
JVM的内存区分为虚拟机栈、本地方法栈、程序计数器、堆、方法区。
其中,虚拟机栈、本地方法栈、程序计数器是每个线程独占区,堆、方法区是所有线程共享的内存区域。
虚拟机栈:每个线程在运行时都会创建一个虚拟机栈。栈中存储的是栈帧(Stack Frame),每个方法调用都会创建一个栈帧,并将其压入栈中。
特点:
- 每个线程都有自己独立的虚拟机栈。
- 栈帧包含局部变量表、操作数栈、动态链接、方法出口等信息。
- 生命周期与线程相同。
本地方法栈:为执行本地native方法(如C/C++代码)提供支持。
特点:
- 类似于虚拟机栈,但专门用于本地方法调用。
- 每个线程有自己的本地方法栈。
程序计数器:记录当前线程所执行的字节码指令地址。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空(Undefined)。
特点:
- 每个线程都有自己的程序计数器。
- 保证线程切换后能恢复到正确的执行位置
堆:所有对象实例以及数组都要在堆上分配
特点:
- 垃圾回收的主要区域。
- 可细分为年轻代(Young Generation)和老年代(Old Generation)。
方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
特点:
- 在HotSpot虚拟机中,方法区的实现称为“永久代”(PermGen,在JDK 8之前)或“元空间”(Metaspace,在JDK 8及之后)。
运⾏时常量池:运行时常量池是方法区的一部分。JDK 8之前位于永久代(堆内存)中,在JDK 8及之后位于元空间(本地内存)中。
二、垃圾回收算法
1、垃圾回收的基本概念
(1)对象可达性分析:JVM通过一系列称为“根节点(Roots)”的对象开始,遍历所有引用链,找到所有可达的对象。所有不可达的对象被认为是垃圾,可以被回收。
(2)根节点(Roots):
- 虚拟机栈中引⽤的对象(本地变量表)
- 本地方法栈中引⽤的对象
- 方法区中静态属性引⽤的对象
- 方法区中常量引⽤的对象
2、主要的垃圾回收算法
(1)标记-清除算法:
- 标记阶段:从根节点开始遍历所有对象,并标记所有不可达的对象。
- 清除阶段:扫描整个堆,回收被标记的对象(即垃圾对象)。
缺点:
- 容易产生内存碎片,导致后续分配大对象时可能失败。
- 标记和清除过程需要遍历整个堆,效率较低。
(2)标记-整理算法
类似于标记-清除,但在清除阶段,会将存活的对象移动到堆的一端,然后清理剩余部分。
缺点:
- 移动对象的过程较为复杂,耗时较长。
(3)复制算法
将堆分成两个区域,每次只使用一个区域,当一个区域满了,将存活的对象复制到另一个区域,并清理当前区域。
缺点:
- 需要额外的空间来存储对象的副本。
- 适用于年轻代(Young Generation),因为大多数对象生命周期较短。
(4)分代回收算法
基于大多数对象生命周期较短的观察,JVM将内存划分为新生代和老年代。分配的依据是对象的经历的GC次数。对象创建时,一般在新生代的Eden Space申请内存,当经历一次GC之后对象还存活,那么对象的年龄+1,当对象年龄超过一定值(默认值为 15),对象就会进入老年代,当然有足够大的对象,即使年龄未达到阈值,这些对象也可能被直接晋升到老年代,因为复制一个大对象耗时。
三、新生代和老年代的区别
对象特性:大多数新创建的对象会被分配到新生代,这些对象的生命周期通常较短,很多对象在创建后不久就不再使用,成为垃圾对象等待回收。而老年代存储的是经过多次垃圾回收仍然存活的对象。
垃圾回收算法的区别:新生代主要使用复制算法进行垃圾回收。新生代内存被分为一个大的Eden区和两个小的Survivor区。新对象优先分配在Eden区,当Eden区满时,触发Minor GC(新生代垃圾回收),将Eden区和一个Survivor区中存活的对象复制到另一个Survivor区,然后清空Eden区和之前使用过Survivor区。经过多次Minor GC后仍存活的对象被晋升到老年代。老年代一般采用标记-清除算法或者标记-整理算法。老年代的垃圾回收成为Major GC或Full GC。
四、主要的垃圾回收机制
Serial收集器:新生代,单线程,简单高效,适合单核处理器和小内存环境
ParNew收集器:新生代,并行执行,减少停顿时间,适合多核处理器
Parallel Scavenge收集器:新生代,高吞吐量,减少垃圾回收时间,适合后台处理
CMS收集器:老年代,并发执行,减少停顿时间,适合对响应时间要求高的应用
G1收集器:新生代+老年代,分区管理,优先回收垃圾最多的区域,适合大堆内存和低延迟需求