第三章 垃圾收集器与内存分配策略
3.2 哪些对象需要回收
3.2.1 引用计数法
对象持有一个计数器,有地方引用则计数器加1,当引用失效时减1,计数器为零时回收。存在环形引用无法回收问题。
3.2.2 可达性分析
当对象和GC Roots对象之间没有引用路径时,需要回收。
GC Roots对象:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的变量
- 本地方法栈中引用的对象
- 基本数据类型对应的Class对象,常驻异常对象,系统类加载器
- 被同步锁持有的对象
除此之外,当局部回收时,如果有对象被回收区域以外的对象引用,也需要把引用的对象加入GC Root对象中。
3.2.3 引用关系类型
- 强引用关系:Object o = new Object();只要还有引用关系,被引用对象不会被回收。
- 软引用关系:SoftReference<Object> reference = new SoftReference<>(new Object());在将要发生OOM时回收;
- 弱引用关系:WeakReference<Object> reference = new WeakReference<>(new Object());当垃圾收集发生时,就会回收;
- 虚引用关系:对对象的生存时间不造成影响,无法通过虚引用找到对象实例。
3.2.4 是否需要回收
- 与GC Roots对象有没有引用路径
- 是否需要执行finalize()方法。若没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,则判定为没有必要执行,直接回收。如果需要执行,放入F-Queue队列中,有一个Finalizer线程启动finalize()方法,如果在该方法中重新关联到GC Roots的引用链上,可以避免回收。
3.2.5 回收方法区
- 废弃的常量
- 不再使用的类型:
- 该类及派生子类的所有实例都被回收
- 加载该类的类加载器被回收
- Class对象没有在任何地方被引用,无法通过反射访问该类的方法
3.3 垃圾收集算法
- Partial GC(部分收集)
- Minor GC(新生代收集)
- Major GC(老年代收集)
- Mixed GC(混合收集)
- Full GC(整堆收集)
3.3.1 分代收集理论
当前商业虚拟机的垃圾收集器,大都遵循了"分代收集"理论进行设计,"分代收集"理论是符合
- 弱分代假说:绝大多数对象都是朝生夕灭
- 强分代假说:熬过越多次垃圾收集过程的对象越难被回收
收集器应该将java堆划分成不同的区域,不同区域的回收频率不同。但是,不同区域之间的对象可能存在引用关系
3. 跨代引用假说:跨代引用相对于同代引用仅占极少数
根据假说3,不需要在跨代引用时扫描整个老年代,只需要在新生代上建立一个全局的数据结构(记忆集),将老年代划分成若干小块,标志出哪一个小块存在跨代引用,在Minor GC时,只有包含了跨代引用的小块内存才加入到GC Roots进行扫描。在对象引用关系改变时,会增加一些维护开销。
3.3.2 标记-清除算法
标记需要清除的对象,统一回收。
缺点:(1)标记和清除操作开销随垃圾对象增加而增加 (2)内存碎片化
3.3.3 标记-复制算法
将内存空间划分成两部分,每次使用其中一部分,需要清理时,将保留的对象复制到另一部分内存中,然后整块清除。
缺点:(1)内存使用率低 (2)当存活的对象很多时,复制操作消耗高
3.3.4 标记-整理算法
在标记后,将存活的对象移动到内存空间的一端,清理掉边界以外的内存。
缺点:(1)当存活的对象很多时,移动操作消耗高,且需要暂停用户程序(ZGC和Shenandoah除外)
3.4 HotSpot算法实现细节
3.4.6 可达性分析
在并发标记阶段,标记线程和用户线程同时运行,一个对象的的引用关系,在标记之后可能会发生变化。
三色标记
黑色节点:当前对象被垃圾回收器访问,且所有引用都以扫描,安全存活,本次回收中不会被再次扫描
灰色节点:当前对象被垃圾回收器访问,至少有一个引用未被扫描
白色节点:当前对象未被垃圾回收器访问,标记开始阶段,所有节点都是白色,在标记结束后,若还是白色,则会被回收
并发标记的问题:
- 标记为黑色的变为白色:浮动垃圾
- 标记为白色的变为黑色:对象消失
当且仅当以下两个条件同时满足时,会导致对象消失
- 新增了一条从黑节点到白节点的引用
- 删除了一条从灰节点到白节点的直接/间接引用
所以解决对象消失问题的办法即破坏其中一个条件:
- (破坏条件1)增量更新:每增加一个黑节点到白节点的引用,记录新增引用,标记完毕后,以每个引用上的黑色节点为根,重新扫描。应用在CMS回收器
- (破坏条件2)原始快照:每删除一条灰节点到白节点的引用,记录删除的引用,标记完毕后,以每个引用上的灰色节点为根,重新扫描。应用在G1回收器
应用原始快照的G1回收器,为了解决新增白色节点引用黑色节点导致对象消失的问题,G1为每个Region设计了两个名叫TAMS的指针,在Region中划分了一部分内存用于并发标记阶段产生的新对象,这些新对象的内存地址保证在TAMS指针指向的地址之上。G1默认这部分内存地址都是隐形标记的,默认存活。
CMS和G1只解决了对象消失的问题,依然会产生浮动垃圾。
3.5 经典垃圾收集器
并行:多个垃圾收集线程配合工作
并发:垃圾收集线程和用户线程同时进行
3.5.1 Serial收集器/Serial Old收集器
单线程。新生代采用复制算法,老年代采用标记-整理算法。垃圾收集期间暂停用户线程。
3.5.2 ParNew 收集器
并行。新生代采用复制算法。垃圾收集期间暂停用户线程。因为多个回收线程并行,吞吐率高,但停顿时间会比较长。
3.5.3 Parallel Scavenge收集器
吞吐率:运行用户代码的时间/(运行用户代码的时间+运行垃圾收集的时间)
并行。可控的吞吐率,高吞吐率可以最高效率地利用处理器资源,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。低吞吐率,则减少系统的卡顿感,但延迟了垃圾线程的工作时间,适合和用户交互的任务。
3.5.4 Parallel Old收集器
Parallel Scavenge收集器的老年代版本。
3.5.5 CMS收集器
老年代垃圾处理器,标记-清除
-
- 初始标记(标记GC Roots) 单线程,暂停用户线程。
- 并发标记(标记引用链)并发
- 重新标记(修正并发标记阶段引用关系变化的部分)并行,暂停用户线程
- 并发清除(清除垃圾对象)并发
优点:以最短停顿时间为目标,停顿发生在初始标记和重新标记阶段,初始标记时间很短,而因为并发标记的关系,重新标记也很短,适合跟用户交互的系统。
缺点:并发阶段,占用了处理器资源,降低了吞吐量。在并发阶段,由于用户线程依然运行,会产生浮动垃圾,所以不能等到老年代空间快满时才回收。因为是标记整理会产生内存碎片。
改进:
- 针对浮动垃圾,CMS默认当内存占用达到一定比例时即开始回收。JDK5默认68%,JDK6默认92%。如果在并发标记阶段内存不够分配,将会启用Serial Old收集器进行老年代垃圾回收,会产生长时间停顿。
- 针对内存碎片,提出了两个方案:(1)在即将进行Full GC时进行内存整理,无法并发 (2)在若干次不进行整理的Full GC之后,下一次进入Full GC之前进行整理 两种方案均在JDK9废弃
- CPU敏感问题,并发阶段,垃圾收集线程数(cpu核心+3)/4,当cpu不足4时,占用线程数高导致应用程序变慢,因此提出了增量式并发收集器,在并发阶段,交替执行回收线程和用户线程。延长了回收时间,但速度变慢减缓。在JDK9被废弃
3.5.6 G1收集器
将内存区域划分成等量大小的Region块,保留新生代和老年代的概念。将Region作为单次回收的最小单元,按照每个Region回收的价值大小维护一个优先级列表,根据用户设定的停顿时间决定回收哪些区域。在延迟可控的情况下获得尽可能高的吞吐量。
-
- 初始标记(标记GC Roots直接关联的对象)单线程,暂停用户线程。
- 并发标记(标记引用链)并发
- 最终标记(标记引用链)并行,暂停用户线程。
- 筛选回收(根据回收价值对Region排序,根据用户期望的停顿时间制定回收计划)暂停用户线程。
缺点:占用内存比大约相当于java堆容量的10%~20%。
3.5.7 高吞吐量和低停顿
吞吐量高指的是用户线程占用CPU时间多,低停顿是指GC时stop the world事件比较短。这两者有时是相冲的,思考一下,一个GC任务,如果全力去做只要1秒钟,但是会暂停用户线程,造成卡顿,那么我们将其拆散成多个小任务,每次花费200毫秒,虽然停顿事件短了,但因为上下文切换等问题,拆成的任务多达8个,那么总的回收时间则是1.6秒,降低了吞吐量。
3.8 内存分配和回收策略
- 对象优先在Eden分配,当空间不足时,触发Minor GC。
- 大对象直接进入老年代,譬如很长的字符串和元素很多的数组。避免高额的内存复制开销。直接进入老年代也是为了避免在新生代的Eden空间和两个Survivor空间来回复制产生的大量复制操作。
- 长期存活的对象将进入老年代,虚拟机给每个对象定义了一个年龄计数器,存放在对象头中,当对象从Eden晋升到Survivor中,年龄增加1,此后每次在Minor GC中存活,则年龄增加1。当达到设定值时,荣升老年代。
- 动态对象年龄判定,当然也不是必须要达到设置值时才会进入老年代,考虑到极限情况,当一个年龄层的对象总数大于Survivor一半的时候,该年龄被判定为高龄,这之上(包含)的对象都将进入老年代。
- 空间分配担保,在发生Minor GC之前,老年代会检查剩余空间是否能容纳新生代所有对象,如果不行,则查看是否允许担保失败(-XX:HandlePromotionFailure),如果允许,则根据历史经验判断,老年代是否能容纳以往进入老年代对象的平均大小,如果可以,则进行一次有风险的Minor GC,如果不行或者不允许担保失败,那么则进行Full GC。
3.9 两个Survivor区
新生代,一次只用Eden和一个survivor。比如使用Eden和survivor0,发生回收后,将剩余的对象复制到survivor1,并清除Eden和survivor0,下次使用Eden和survivor1。这样的好处是,避免产生内存碎片