Java垃圾回收机制

Java语言是一门自动内存管理的语言,不再需要的对象可以通过垃圾回收自动进行内存释放。

Java运行时内存区域划分

JVM将Java程序运行时内存区域划分成以下几个部分:

  1. 程序计数器(Program Counter Register, PC)
  2. java虚拟机栈
  3. 本地方法栈
  4. java堆
  5. 方法区,方法区中包括运行时常量池
Java垃圾回收机制_第1张图片
java运行时内存区域划分.png

程序计数器可以看做是当前线程所执行字节码的行号指示器。JVM依靠程序计数器来选取需要执行的下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来实现。
虚拟机栈描述了Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用开始至结束的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈与java虚拟机栈类似,只不过它用于为虚拟机使用的Native方法服务。
Java堆用于存放对象实例数据,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域。
方法区用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

其中程序计数器、Java虚拟机栈和本地方法栈都是线程私有的,而java堆和方法区是线程所共享的。
Java垃圾回收主要作用于Java堆。而Java堆又划分为年轻代,老年代。年轻代又分为Eden,Survivor(分为From Survivor和To Survivor)。Java中垃圾回收是分带收集的,不同区域的回收算法是不同的。

判定对象是否已死

判定一个对象是否已死的方法主要有两种:引用计数法和可达性分析算法(也叫根结点枚举法)。

引用计数法

引用计数法思想很简单:为每个对象维持一个引用计数器,当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是可以被回收的对象。

引用计数法优缺点

引用计数法的优点非常明显:实现简单,效率高。但是它无法解决循环引用问题:例如对象A和B互相引用,但是其他任何对象都没有引用A和B或者被A和B引用,此时引用计数法判定A和B不可回收,但是事实上A和B都可以被回收。为了解决循环引用问题,可达性分析算法应运而生。

可达性分析算法

可达性分析算法思想是通过一个"GC Roots"集来判定对象是否已死,以"GC Roots"为起始点,从这些节点往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,这个对象就是不可用的。示意图如下:

Java垃圾回收机制_第2张图片
可达性分析算法示意图

在Java中,可作为 GC Root的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中Native方法引用的对象。

垃圾收集算法

标记清除算法

算法的执行过程与名字一样,先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法有两个问题:

  1. 标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。显然遍历的效率是很低的
  2. 会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。
Java垃圾回收机制_第3张图片
标记清除算法

复制算法

复制算法是为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。

  1. 优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
  2. 缺点:算法的代价是将内存缩小为了原来的一半,未免太高了一点。
Java垃圾回收机制_第4张图片
复制算法

现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。

当然,90%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时(例如,存活的对象需要的空间大于剩余一块Survivor的空间),需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,二是让所有的存活对象都向一端移动,然后清理掉边界外的内存。该方法主要用于老年代。

Java垃圾回收机制_第5张图片
标记-整理算法

分代收集算法

目前商用虚拟机都使用“分代收集算法”,所谓分代就是根据对象的生命周期把内存分为几块,一般把Java堆中分为新生代和老年代,这样就可以根据对象的“年龄”选择合适的垃圾回收算法。

  1. 新生代:“朝生夕死”,存活率低,使用复制算法。
  2. 老年代:存活率较高,使用“标记-清除”算法或者“标记-整理”算法。

垃圾收集器

JVM有7种不同的垃圾收集器,它们是垃圾收集算法的具体实现。下图展示了这7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。

Java垃圾回收机制_第6张图片
垃圾收集器

各个收集器的特点如下:

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

收集器总结

收集器 串行、并行or并发 新生代or老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用
G1 并发 年轻代和老年代 标记-整理+复制算法 响应速度优先 面向服务端应用,将来替换CMS

内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存 以及 回收分配给对象的内存。一般而言,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB),将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中。总的来说,内存分配规则并不是一层不变的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

  • 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。

  • 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

  • 长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。

  • 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

需要注意的是,Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。也就是说,垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。

Minor GC 和 Full GC

  1. 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  2. 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

Minor GC和 Full GC触发条件

Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:

  1. 调用System.gc时,系统建议执行Full GC,但是不必然执行。
  2. 老年代空间不足。
  3. 方法去空间不足。
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
  5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

参考资料

《深入理解Java虚拟机——JVM高级特性与最佳实践》-周志明

你可能感兴趣的:(Java垃圾回收机制)