第3章 垃圾收集器和内存分配策略
目录
3.3.4 分代收集算法
3.1 概述
- 哪些内存需要回收?
- 什么时候进行回收?
- 如何进行回收?
第2章介绍了java运行时数据区的各个部分,其中程序计数器、本地方法栈、虚拟机栈这三个区域随线程而生、随线程而死。而且每一个栈帧需要分配多少内存基本上是在类结构确定下来时就已知的。所以这几个区域的内存分配与回收都具备确定性,不需要过多考虑。
java堆和方法区则不同,只有在运行时才知道会创建哪些对象,因此内存的回收和分配都是动态的。这也是垃圾回收所关心的内存区域。
3.2 对象已经死了吗
3.2.1 引用计数法
引用计数法:给对象添加一个引用计数器,被引用一次就+1,引用失效时就-1,当其为0时表示已死,可以回收。
缺点无法判定循环引用。
3.2.2 可达性分析
可达性分析:从GC Roots到这个对象不可达时,表示对象已死。
可作为GC Roots对象的包含:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈JNI引用的对象。
3.2.3 再谈引用
强引用:只要强引用还在,垃圾回收时永远都不会被回收的对象。
软引用:有用但非必需的对象。在系统将要发生内存溢出之前,这些对象将被列入可回收范围内用于进行第二次垃圾回收。如果这次回收还没有足够的内存,才会发生内存溢出。
弱引用:非必需的对象。这些对象只能生存到下一次垃圾回收发生之前。无论当前内存是否足够,都会被回收掉。
虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。唯一作用就是在这个对象被回收时收到一个系统通知。
3.2.4 二次标记
即使在可达性分析算法中不可达的对象,也不是非死不可。要宣告一个对象真正死亡,至少需要经历两次标记过程。
略。
3.2.5 回收方法区
Java虚拟机规范中不要求虚拟机必须在方法区进行垃圾回收,而且在方法区的垃圾回收性价比也比较低。
方法区(或者HotSpot虚拟机中的永久代)主要收集两部分:废弃常量和废弃类。
废弃常量:没有任何对象引用这个常量。
废弃类:该类的所有实例都已经被回收;该类的ClassLoader已经被回收;该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
但是这只是代表可以回收,不表示一定要回收。
在大量使用反射、动态代理、CGLib以及频繁使用自定义的ClassLoader等的场景下,都需要虚拟机具备类卸载的功能,以防永久代溢出。
3.3 垃圾收集算法
3.3.1 标记-清除算法
最基础的收集算法。
标记:标记出需要被回收的对象。
清除:统一回收被标记的对象。
缺点:标记和清除的过程效率都不高;容易产生不连续的内存碎片,导致在分配较大对象时提前触发垃圾回收。
3.3.2 复制算法
将容量分为大小相同的两块,每次使用其中一块。
当这一块内存使用完毕,就将存活对象全部复制到另一块内存上,然后清除这一块内存。
优点:不会产生内存碎片。
缺点:内存浪费。
新生代通常使用复制算法,不过采用8:1:1(Eden:Survivor1: Survivor2)的来分配。因为新生代对象大多是朝生夕死,清理时会回收大部分对象。
每次使用Eden和其中一个Survivor。比如某次回收时将Eden和Survivor1中存活对象移到Survivor2中,再清理掉刚刚使用的区域Eden+ Survivor1。
但是仍然存在某次回收中,Eden+Survivor1中存活对象不能完全复制到Survivor2中,即回收的对象不够多,Survivor2内存不够。则需要老年代进行分配担保。此时这些对象将直接进入老年代。
3.3.3 标记-整理算法
标记:标记所有需要回收的对象。
整理:让所有存活对象向一端移动,然后清理掉端边界以外的内存。
优点:不会产生内存碎片。
3.3.4 分代收集算法
一般将java堆分为新生代和老年代。新生代使用复制算法,老年代使用标记-清除或者标记-整理算法。
3.4 HotSpot的算法实现
略。
3.5 垃圾收集器
收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现。这里的讨论基于HotSpot虚拟机,它包含诸多垃圾收集器,并提供参数让用户选择、组合。
图中如果两个垃圾收集器之间存在连线,说明他们可以组合使用。
//缺图。
名称 | 区域 | 方式 | 算法 | Stop the World | 其他 |
Serial | 新生代 | 串行
| 复制 | Yes | 简单高效,Client模式下的默认收集器 |
ParNew | 新生代 | 并行
| 复制 | Yes | Serial的多线程版本,Server模式下的默认收集器 |
Parallel Savenge | 新生代 | 并行 | 复制 | Yes | 关注吞吐量 |
Serial Old | 老年代 | 串行 | 标记整理 | Yes | Serial的老年代版本 |
Parallel Old | 老年代 | 并行 | 标记整理 | Yes | Parallel Savenge的老年代版本 |
CMS | 老年代 | 并发 | 标记清除 | 部分 | 关注最短停顿时间 |
G1 | 横跨 | 并发 | 标记整理 | 部分 | 使用Region |
并行:多条垃圾收集线程同时工作,但是此时用户线程仍然处于等待状态。
并发:用户线程和收集线程同时执行(可能是交替执行,可能是并行执行),用户程序在继续执行而垃圾收集运行在另一个cpu上。
3.5.1 Serial收集器
新生代收集器,单线程收集器,使用复制算法。
使用一个CPU或一条收集线程去进行垃圾回收。
在垃圾回收时,需要暂停其他所有用户线程。
优点:简单而且高效,至今仍是虚拟机运行在client模式下的默认新生代收集器。
3.5.2 ParNew收集器
新生代收集器,多线程收集器,使用复制算法。
使用多个CPU或多个收集线程去进行垃圾回收。
在垃圾回收时,需要暂停其他所有用户线程。
优点:至今仍是虚拟机运行在Server模式下的默认新生代收集器。除了Serial,只有它能与CMS收集器组合工作。
3.5.3 Parallel Scavenge
新生代收集器,多线程收集器,使用复制算法。但是它的关注点是达到一个可控制的吞吐量。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)。
PS:GC停顿时间是以牺牲吞吐量和新生代空间来换取的。
Parallel Scavenge有一个参数-XX:+UseAdapiveSizePolicy,虚拟机会自动调节来提供合适的停顿时间和最大的吞吐量。自适应调节也是它和ParNew的最大区别。
3.5.4 Serial Old
老年代收集器,单线程收集器,使用标记-整理算法。
Serial的老年代版本,即也需要暂停所有用户线程。
3.5.5 Parallel Old
老年代收集器,多线程收集器,使用标记-整理算法。
Parallel Scavenge的老年代版本,也是吞吐量优先。
3.5.6 CMS收集器
CMS(Concurrent Mark Sweep)关注的是最短GC停顿时间,使用标记-清除算法。
它的运作分为四步,其中只有初始标记和重新标记时需要暂停用户线程(其他步骤看名字也知道,是并发**,不用暂停用户线程)。
- 初始标记:标记GC Roots对象能直接关联的对象。
- 并发标记:进行GC Roots Tracing,找出可达对象。
- 重新标记:修正并发标记时因用户线程运作而导致的标记变动,可并行。
- 并发清除:清除标记对象。
优点:整个过程中耗时最长的是并发标记和并发清除,但是是和用户线程并发执行,所以总体来说GC停顿时间较短。
缺点:并发阶段虽然不会暂停用户线程,但是影响了一部分用户线程执行速度;无法处理浮动垃圾,因为并发清理过程中用户线程仍然在运行,这时产生的垃圾将要等到下一次垃圾回收才会被清除;基于标记-清除会导致内存碎片,不过提供了参数开启内存碎片合并整理过程,但导致了停顿时间变长。
3.5.7 G1收集器
横跨新生代和老年代,不需要与其他收集器配合就能独立管理整个GC堆。整体上基于标记-整理,局部(两个Region之间)基于复制算法。
它将整个java堆划分为多个大小相等的独立区域Region,新生代和老年代不再是物理隔离的,他们都是一部分Region的集合(不需要物理上连续)。
G1跟踪各个Region里的垃圾堆“积”的大小,在后台维护一个优先列表,优先回收价值最大的Region。
它的运作分为四步,其中只有初始标记和最终标记需要暂停用户线程。
- 初始标记:标记GC Roots对象能直接关联的对象。
- 并发标记:进行GC Roots Tracing,找出可达对象。
- 最终标记:修正并发标记时因用户线程运作而导致的标记变动,记录在Remembered Set 里,可并行。
- 筛选回收:根据用户期待的GC停顿时间来制定回收计划。
3.5.8 理解GC日志
略。
3.5.9 垃圾回收参数总结
略。
3.6 内存分配与回收策略
对象首先在新生代的Eden区域分配,如果启动了本地线程分配缓冲,将按线程优先分配在TLAB上。大对象可能会直接分配在老年代中。
3.6.1 对象优先在Eden区域分配
当Eden区域没有足够内存去分配给对象时,将触发一次minor GC。如果minor GC完了之后,存活对象无法全部放入Survivor空间,将通过分配担保将这些对象转移到年老代中。最后再将新对象放入Eden区域。
3.6.2 大对象直接进入老年代
虚拟机提供了参数-XX:PretenureSizeThreshold,大于这个参数的对象将直接分配在老年代中。
3.6.3 长期存活的对象将进入老年代
虚拟机为对象定义了对象年龄计数器(Age)。
对象在Eden出生并且经过一次Minor GC后仍然存活,并且能被Survivor容纳然后转移到Survivor中,将对象年龄设为1。每熬过一次Minor GC,年龄就+1。达到阈值(默认15)时就晋升到老年代中。
3.6.4 动态对象年龄判定
为了更好的适应内存状况,虚拟机不是永远都要求对象age必须达到阈值才能晋升至老年代。如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则>=该年龄的对象可以直接进入老年代。
3.6.5 空间分配担保
当出现大量对象在Minor GC后仍然存活的情况,Survivor区域就有可能不够存放所有存活对象,此时需要老年代进行分配担保,把Survivor无法容纳的对象放入老年代。
在发生Minor GC之前,虚拟机会检查老年代的最大可用连续空间是否大于新生代所有对象总空间(需要考虑极端情况,即Minor GC后没有回收任何对象,且Survivor不够空间存放),如果成立,则Minor GC是肯定安全的,直接进行Minor GC。
如果不成立,Minor GC可能存在风险。
则虚拟机需要去查看参数是否设置为允许担保失败。如果允许,虚拟机还需要查看老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小(动态概率手段,不保证一定在这个条件下就成功)。
如果小于,要进行一次Full GC。如果大于,将进行一次有风险的Minor GC,如果这次有风险的Minor GC出现了担保失败,则在失败后要发起一次Full GC。