深入理解java虚拟机读书笔记三

LittleMay      2022-06-10     260

关键词:

垃圾回收需要解决的三个问题是:

  1. 哪些内存需要回收
  2. 何时回收
  3. 如何回收

哪些内存需要回收

对于Java内存运行时区域,程序计数器\虚拟机栈\本地方法栈三个部分是线程私有的,随线程而生,随线程而灭.因此这几个区域的内存分配和回收都具有确定性,当方法或者线程结束时,内存会自然回收.
因此通常指的垃圾回收是针对方法区两个部分:只有运行时,才能知道究竟会创建哪些对象,创建多少个对象,分配和回收是动态的.
确定了回收的区域后,就需要判定区域中哪些对象需要回收,通常有两种方法来判定:

  • 引用计数法,当对象的引用计数器变为0时,则被回收.Python中使用的就是此种方法,缺点是如果对象之间相互引用,则无法被回收;
  • 可达性分析算法,采用一系列GC Roots根对象根据引用关系搜索,如果某个对象不可达,则将其回收.Java中采用的是此种方法.

GC Roots是一系列对象,固定可以作为GC Roots的对象包括:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 常量引用的对象
  • JNI 引用的对象
  • 虚拟机内部的引用对象,例如基本类型的Class对象,常驻异常对象等
  • synchronized持有的对象
  • 反映虚拟机内部情况的JMXBeanJVMTI中注册的回调、本地代码缓存等
    除此之外,还可以有其他对象临时加入.

书上说的比较难懂,按照书里的描述,root应该是一个对象,而按照其他地方的描述,譬如R大在知乎上的回答是这样说的:一组活跃的对象引用(是引用,不是对象,因为对象处在堆里,是要被回收的区域).参照:
java的gc为什么要分代? - RednaxelaFX的回答 - 知乎.而我认为root是引用比较好理解一点,比如int p = new Person(),如果再设置p=null,那么最初p指向的Person对象就会标记为不可达,从而被回收.或许不用纠结具体的文字,只要是该对象的引用是活跃的,那么回收它一定会影响运行,因此可以将活跃的引用作为root,参照:GC root

何时回收

由此可以看出,我们一直以"引用"来衡量对象是否可达.为了让引用具有除了被引用,未被引用这两种状态之外有更加多的状态(比如单纯的说一个对象被引用,但是在内存比较紧张的情况下,是否可以将其也回收掉),自JDK 1.2后,引用的概念变得更加丰富,按照引用强度,可以分为四种:

      强引用>软引用>弱引用>虚引用
  • 强引用(Stongly Reference): 永远不会被回收
  • 软引用(Soft Reference): 内存不足时,会被回收
  • 弱引用(Weak Reference): 无论内存是否充足,都会被回收
  • 虚引用(Phantom Reference): 不影响对象生存周期,也不能获得对象,无论内存是否充足,都会被回收,唯一的用途是在回收时获得通知.

参考资料: 理解Java的强引用、软引用、弱引用和虚引用

当在可达性分析算法中被判定为不可达的对象,还至少需要经历两次标记过程:

  1. 经历第一次标记后,进行第一次筛选,筛选是否需要执行finalize()方法.由于finalize()方法只能被调用一次,因此,没有覆盖该方法的,或者已经被调用过的,不会被筛选出来.
  2. 被筛选出来的对象是需要执行finalize方法的,它们会被放置在F-Queue队列中,并等待着被虚拟机自动建立的低调度优先级的线程去执行finalize方法.所以如果对象在finalize时让自己重新被引用链上的某个对象引用,那么便可以逃脱被回收的命运.

因此,使用finalize要慎重,尽量不要使用它.因为它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序.

如果说堆中的垃圾回收比较清晰,那么方法区的垃圾回收就复杂得多.虚拟机规范提到可以不在方法区中实现垃圾收集.因为它的回收条件较为苛刻:
方法区主要回收废弃常量和不再使用的类型.废弃常量的判定较为简单:当一个常量不再被引用时,可以被清除出常量池.而不再使用的类型的判定条件就比较严苛:

  • 该类所有实例都被回收
  • 加载该类的类加载器也被回收
  • 对应的java.lang.Class对象没有被引用,无法通过反射访问该类的方法

如何回收

当前虚拟机的垃圾回收大多数遵循了"分代回收"的理论.由此可以得出一个设计原则:将堆划分出不同的区域,然后将回收对象依据熬过垃圾收集过程的次数分配到不同的区域中.不同区域的回收频率不一样,可以兼顾时间开销和内存空间的有效利用.
具体实现时,至少会有两代:新生代(Young Generation)和老年代(Old Generation):新生代经历了回收后存活的对象,会逐步晋升到老年代.针对特定区域的收集因此也可以被分为:

  • 部分收集Partial GC: 不是完整收集整个堆的垃圾
    • 新生代收集(Minor GC/ Young GC): 新生代的垃圾收集
    • 老年代收集(Major GC/ Old GC): 老年代的垃圾收集, 只有CMS收集器有此行为
    • 混合收集(Mixed GC): 收集整个新生代和部分老年代,只有G1收集器有此行为
  • 整堆收集(Full GC): 收集整个堆和方法区

对象并不是孤立的,对象之间会存在跨代引用.为了避免在新生代中进行了扫描后,又需要在老年代中进行扫描来确认扫描的结果准确性,引入了记忆集(Remembered Set,从非收集区域指向收集区域),用以记录老年代哪一块内存引用了新生代的对象.这样当进行Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描,避免了扫描整个老年代.

如果记录跨代指针的所有细节,空间占用和维护成本都变得十分高昂.因此,可以采取更粗粒度的记录,比如采用内存分块的方式记录,只要内存块中存在跨代引用,就可以标记为dirty,从而可以轻易筛选出哪些内存块中包含跨代指针.也可称之为卡表(Card Table).卡表是记忆集的一种实现.参照:jvm的card table数据结构

回收算法

  • 标记-清除(Mark Sweep):最基础的算法,后续算法大多以此为基础.
    分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
    主要缺点是:1. 随着要回收对象的增加,标记和清除的效率都会降低; 2. 会导致内存碎片化.当需要分配较大对象无法找到足够的连续内存时,不得不提前触发另一次垃圾收集动作.

CMS收集器采用了这种思想.

  • 标记-复制(Mark Copy): 现在虚拟机大多采用此回收新生代.
    它将可用内存按容量划分为两块, 每次只使用其中的一块. 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉.对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象, 而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可.这样实现简单,运行高效.缺点是浪费的空间比较大.

Serial/ParNew收集器都采用了这种思想.将新生代分为一块较大的Eden区域和两块较小的Survivor。每次分配内存只使用Eden和其中一块Survivor。当另一块Survivor区域不足以容纳存活对象时,将会触发分配担保(Handle Promotion),存活对象直接进入老年代。

  • 标记-整理(Mark Compact): 老年代有大量存活的对象,Mark Copy就不适用了.所以针对老年代的特点,可以采用此种算法.
    其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存.因为老年代存在大量的存活对象,对存活对象的移动将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序(stop the world)才能进行。

Parallel Scavenge收集器基于了这种思想.

算法细节

HotSpot为例,垃圾回收开始于根节点枚举,而所有收集器在扫描根节点集合时都需要暂停用户线程,因此如何快速地,正确地枚举出根节点至关重要:
比如通过OopMap(扫描时可以得知哪些位置是引用)的协助快速完成GC Roots枚举,而不用去查询所有执行上下文和全局的引用位置,然而OopMap可能被很多指令影响,因此引入了安全点,安全区域:

  • 安全点: 线程只有到达安全点才能够暂停进行垃圾收集.

当到达安全点时,可以将OopMap看做是当前内存的快照.这个快照既不能出现的太频繁,也不能很久也不生成,也就是说安全点位置的选取需要进行特别地考虑.

  • 安全区域: 当线程处于休眠或者阻塞状态时,无法进入安全点挂起自己,所以引入了安全区域.线程需要离开安全区域时,需要检查虚拟机是否完成了根节点枚举.

同时,还有上文提到的记忆集,它也可以缩减GC Root的扫描范围.虚拟机采用写屏障(Write Barrier)的方式来即时地让卡表元素变成dirty.

类似于切面编程,可以在赋值的前后,进行额外的操作.虽然会产生额外的开销,但是胜过在进行Minor GC时扫描整个老年代.多个卡表元素(一个卡表元素占1个字节)会共享一个缓存行,因此不同线程的操作对卡表的影响是可能会带来伪共享问题的.

假设已经获得了GC Roots后,需要根据可达性算法来获得引用链来判定对象是否存活,也就是标记阶段.它要求全过程都基于能保障一致性的快照上才能进行分析,意味着需要全程冻结用户线程的运行.

如果不一致,可能会有两种后果:1. 将死亡的对象标记为存活;2.将存活的对象标记为死亡,而这种错误是致命的

标记阶段是所有追踪式垃圾收集算法的特征,当堆变大,标记阶段显然会因此变长,因此在这个阶段,降低线程的停顿时间也能带来很大的增益:

  • 增量更新: 将并发扫描时新增加的引用记录下来,等扫描结束后,再重新扫描一遍(让增加了引用的对象重新扫描一遍,保证了它引用的对象是存活的).

CMS基于此做并发标记

  • 原始快照(SATB): 将并发扫描时删除的引用记录下来,等扫描结束后,再重新扫描一遍引用记录(保证此次垃圾回收时被本该存活的对象,由于引用被删除仍然是存活的).

G1/Shenandoah基于此做并发标记

这两种解决方案都是基于写屏障实现的.通常采用三色法描述会更加直观.参照:JVM-垃圾回收-三色标记算法.

经典垃圾收集器

对于如何发起内存回收(根节点枚举,并发扫描标记),如何加速回收(降低停顿),如何保证回收正确性(增量更新和原始快照),前文已经有简单介绍.但垃圾回收的具体动作因不同的垃圾收集器而异,以HotSpot虚拟机为例,垃圾收集器有以下几种:

  • Serial/Serial Old: 分别代表了新生代和老年代的收集器.其中新生代采用复制算法,老年代采用整理算法.但它只采用一条收集线程完成垃圾收集,并且收集时必须暂停其他所有工作线程.即使如此,它相比其他单线程收集器,具有简单高效的优点,同时也是额外内存消耗最小的.
    Serial/Serial Old垃圾收集器
  • ParNew: Serial的多线程版本,是应用于新生代的收集器
    ParNew/Serial Old收集器
  • Parallel Scavenge/Parallel Old: Parallel Scavenge是基于复制算法实现的多线程新生代收集器,与ParNew不同在于它的关注点在吞吐量(处理器运行用户代码时间和处理器总消耗时间)上,也被成为吞吐量优先收集器.Parallel Old是它的老年代版本.

Parallel Old出现之前,Parallel Scavenge只能与Serial Old一起搭配

Parallel Scavenge/Parallel Old垃圾收集器

  • CMS: 基于标记清除算法实现的老年代收集器,它有四个阶段: 初始标记,并发标记,重新标记,并发清除.其中初始标记和重新标记都需要stop the world,但初始标记只标记GC Roots能直接关联到的对象,而重新标记可以利用增量更新来对并发标记时产生的变动进行修正。相较于耗时最长的并发标记和并发清除,时间要短得多。而并发标记和并发清除,虽然耗时长,但可以与用户线程一起工作。

CMS也被称为并发低停顿收集器,但是也仍然具有明显的缺点:
1.对处理器资源敏感(并发会占用处理器能力);2.无法处理浮动垃圾(垃圾回收时用户线程仍在进行,因此也需要预留内存空间给用户线程)。如果没有足够的内存空间,将临时启用serial old来对老年代进行收集; 3.采用标记清除算法,容易产生大量碎片。

CMS收集器

  • G1:与前面所有的收集器不同,它采用的是Mixed GC模式,也就是说,它可以面向堆任何部分来进行回收,而不是只关注某一块区域(新生代或者老年代)。将内存划分为大小相等的独立区域(Region),每一个Region都根据需要扮演新生代或者老年代的角色。同时还有一个专门存储大对象的Humongous区域。

虽然G1仍然保留了新生代和老年代的概念,但它们不再是固定的,是一系列不要求连续的区域集合。

因为Region是回收的最小单元,因此每次回收都是Region的整数倍,所以G1可以建立一个可以预测的停顿时间模型,根据回收可以获得的内存大小以及所需的经验时间,计算回收价值大的区域,也就是Garbage First
G1运作过程大致可以分为四个步骤:初始标记,并发标记,最终标记,筛选回收。除了并发标记,其他步骤都需要暂停用户线程。与CMS不同的是,它最终标记采用的是前文提到的SATB,并且不会产生内存空间碎片。
G1收集器

《深入理解jvm虚拟机》读书笔记

前言:《深入理解JVM虚拟机》是JAVA的经典著作之一,因为内容更偏向底层,比较枯燥难啃,所以之前一直没有好好的阅读过。最近因为刚好有空,又有了新目标。所以打算和《构架师的12项修炼》一起看,这样荤素搭配,吃饭不... 查看详情

深入理解java虚拟机-读书笔记(代码片段)

第1章走近Java第2章Java内存区域与内存溢出异常第3章垃圾回收器与内存分配策略第4章虚拟机性能监控与故障处理工具第1章走近JavaJava程序设计语言、Java虚拟机、JavaAPI类库统称为JDK。Java技术体系分为4个平台:JavaCard:Apple... 查看详情

读书笔记-深入理解jvm虚拟机-1.oom初探

Java堆OOM(Out-Of-Memory)异常执行例如以下程序,爆出异常java.lang.OutOfMemoryError:Javaheapspace/***VMArgs:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError*@authorzzm*/publicclassHeapOOM{ staticclassOOMObject{ } publicsta 查看详情

深入理解java虚拟机读书笔记1--java内存区域

  Java在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途、创建和销毁的时间,有一些是随虚拟机的启动而创建,随虚拟机的退出而销毁,有些则是与线程一一对应,随线程的... 查看详情

《深入理解java虚拟机》读书笔记:晚期(运行期)优化

文章目录正文一、HotSpot虚拟机内的即时编译器1、解释器与编译器(1)解释器、编译器(2)C1、C2编译器(3)混合模式、解释模式与编译模式(4)分层编译2、编译对象与触发条件(1)热点... 查看详情

深入理解java虚拟机读书笔记---运行时数据区域

运行时数据区域1.程序计数器   程序计数器(ProgramCounterRegister)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行... 查看详情

深入理解jvm虚拟机读书笔记——类的加载机制(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化&#... 查看详情

深入理解jvm虚拟机读书笔记——类的加载机制(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化&#... 查看详情

深入理解jvm虚拟机读书笔记——运行时数据区(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。跨平台性是Java语言的重要特性,而这一特性本质上就是通过JVM虚拟机来实现的。下面就来... 查看详情

深入理解jvm虚拟机读书笔记——锁优化(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号:兴趣使然的草帽路飞,回复001获取。1.Java语言中的线程安全按照线程安全的“安全程度”由强至弱来排... 查看详情

深入理解jvm虚拟机读书笔记——锁优化(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号:兴趣使然的草帽路飞,回复001获取。1.Java语言中的线程安全按照线程安全的“安全程度”由强至弱来排... 查看详情

深入理解jvm虚拟机读书笔记——运行时数据区(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》跨平台性是Java语言的重要特性,而这一特性本质上就是通过JVM虚拟机来实现的。下面就来通过深入学习JVM来进一步增加我们对Java这门编程语言的... 查看详情

深入理解jvm虚拟机读书笔记——内存模型与线程(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。1.Java内存模型JMM概述:Java内存模型指的是JMM,而不是运行时数据区哦~Java语言为了保证... 查看详情

深入理解jvm虚拟机读书笔记——内存模型与线程(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。1.Java内存模型JMM概述:Java内存模型指的是JMM,而不是运行时数据区哦~Java语言为了保证... 查看详情

深入理解jvm虚拟机读书笔记——垃圾回收器(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机... 查看详情

深入理解jvm虚拟机读书笔记——垃圾回收器(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机... 查看详情

深入理解jvm虚拟机读书笔记——对象的创建与内存布局(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》1.对象的创建过程在Java语言层面,创建对象一般是借助new关键字去实现:Useruser=newUser();而在虚拟机中,对象的创建过程如下:当Jav... 查看详情

深入理解jvm虚拟机读书笔记——运行时栈帧结构(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。Java虚拟机以方法作为最基本的执行单元,“栈帧”(StackFrame)则是用于支持虚拟... 查看详情