JVM垃圾回收
1. 堆的基本结构
堆负责存放对象实例,是垃圾回收的主要区域,分为以下三个部分:
- 新生代(Young Generation):Eden、S0、S1
- 老生代(Old Generation):Tenured
- 永久代(Permanent Generation):1.8之后变成元空间,位置在直接内存
2. JVM垃圾回收机制
2.1 内存分配和回收原则
- 对象优先在Eden区分配,当Eden没有空间时,发起一次
Minor GC
- 如果
Minor GC
后的Ed空间仍然不足而老年代足够,则会触发空间分配担保机制,将对象提前转移到老年代中 - 若老年代空间也不足则会触发
Full GC
- 大对象即大量连续内存空间的对象(比如:字符串、数组)直接进入老年代,从而减少新生代垃圾回收的频率
空间分配担保机制是为了确保在
Minor GC
之前老年代本身还有容纳新生代所有对象的剩余空间。若老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC
,否则将进行 Full GC
2.2 年龄机制
- 对象在Eden区分配后,若经过一次
Minor GC
仍存活,则移动至Survivor(S0、S1),并设置Age=1 - Survivor中每经过一次
Minor GC
,Age++, 如果Age >晋升值
则移动至老年代
晋升值的计算:从低到高,累积各年龄所占内存大小,若超过50%(默认)则取该年龄和最大晋升年龄15(默认)最小的作为晋升年龄。
2.3 GC区域
- 部分收集
- 新生代收集(Minor GC/Young GC):只收集新生代
- 老年代收集(Major GC/Old GC):只收集老年代
- 混合收集(Mixed GC):整个新生代+部分老年代
- 整堆收集(Full GC):收集整个Java堆和方法区
2.4 死亡判断
在垃圾回收前首先要判断哪些对象、常量和类已经死亡,并优先进行垃圾回收
2.4.1 对象死亡判断
- 引用计数法 给对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
然而这个方法无法解决循环引用,所以并没有被大规模使用
- 可达性分析
从GC Roots向下搜索,若存在对象不可达,则第一次判断这个对象的finalize是否存在且被执行,若被执行了,则这个对象没有必要回收,否则放在一个队列中进行二次标记,若第二次仍不可达则回收。
可以当作GC Roots的对象:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象JNI(Java Native Interface)引用的对象
2.4.2 常量废弃判断
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被回收
2.4.3 类无用判断
判断类无用需要同时满足三个条件:
- 所有实例被回收
- ClassLoader被回收
- java.lang.Class没被引用,即无法通过反射调用到该类的方法
2.5 引用类型
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关
- 强(StrongReference):默认,若被引用则永远不会被回收
- 软(SoftReference):内存不足时,会被回收
- 弱(WeakReference):对象被弱引用,一旦垃圾收集扫描到,就会被回收
- 虚(PhantomReference):主要用于跟踪对象被垃圾回收的活动,当垃圾回收器准备回收一个对象时,如果发现它有虚引用则会将虚引用加入引用队列,通过判断这个队列来了解对象是否要被垃圾回收。
2.6 垃圾收集算法
2.6.1 标记-清除算法
标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
产生的问题:效率不高、清除后产生大量不连续的内存碎片
2.6.2 复制算法
将内存分为大小相同的两块,每次使用其中一块,当用完之后就把存活对象复制到另一块中,清空原来的
产生的问题:可用内存减半
2.6.3 标记-整理算法
标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
产生的问题:效率不高,但老年代垃圾回收频率不高,所以还ok
2.6.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
- JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK20: G1
3. 垃圾收集器
垃圾收集器是垃圾回收的具体实现,在JDK 8中使用的是Parallel Scavenge(新生代)+ Parallel Old(老年代)垃圾收集器,之后使用的都是G1垃圾收集器。
3.1 Parallel Scavenge & Parallel Old
Parallel Scavenge是使用标记-复制算法的多线程收集器,更关注于吞吐量(高效利用CPU)而不是线程停顿时间(用户体验)。
Parallel Old是Parallel Scavenge的老年代版本,使用标记-整理算法
3.2 G1
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
- 小区域内存划分
- 垃圾回收线程和工作线程能够并行工作,避免STW。
- 不同区域可同时回收,并发性更高,更适合多核服务器。
- 可以先回收一部分区域,回收更快。
混合回收:同时处理新生代和老年代,避免了长时间的
Full GC
可预测的停顿时间:G1可以通过设置最大停顿时间来控制每次垃圾回收的时间