JVM

背景

由于最近在查一个OOM问题,所以借机读起了《深入理解Java虚拟机》第三版。简单做个记录。

垃圾收集算法

分代收集理论

大多数垃圾收集器的都应用的理论。

弱分代假说(年轻代):绝大多数朝生夕灭的对象。
强分代假说(老年代):熬过越多次垃圾收集就越难消亡。
跨代引用假说

Partial GC(部分收集):

  • Minor GC/Young GC(年轻代收集)
  • Major GC/Old GC(老年代收集)
  • Mixed GC(混合收集):JDK 11 ,只有G1有,针对年轻代和部分老年代。
    Full GC(整堆收集):整个堆和方法区
标记-清除算法

标记需要回收的对象,然后清除。这是最基础算法,后续的其它算法都是针对其缺点的改进而来的。所以其实放在这个目录纬度可能有点不太合适,不过不纠结了。

缺点:

  1. 随着对象增加而效率降低。
  2. 内存碎片
标记-复制算法

年轻代内存均分两个区域,一个使用完了,就将还存活的对象复制到另一个。避免内存碎片。

缺点:

  1. 内存缩小了一半 。
  2. 存活对象越多,复制的开销越大,效率降低。
  3. 需要担保机制,保证剩下的区域也不够时,也能正常运行,即需要依赖其它内存区域(年老代)。

Appel式回收:优化了标记-复制算法,使内存的分配更加合理,比如分成了1个Eden、2个Survivor,Eden、Survivor默认比例为8:1,一开始使用1个Eden、1个Survivor,即剩下未使用的内存只占10%。

标记-整理算法

主要针对年老代。标记后,让存活对象往内存的一端移动,然后再清理除此以外的内存。

缺点:

  1. 移动就意味着需要更新引用地址,增加了回收的复杂度。

所以需要权衡(如果移动则回收更复杂,如果不移动则内存分配更复杂)。比如如果更关注吞吐量则选择“移动”,如果更关注延迟则“不移动”。

算法细节

上面说的是一些理论,现在说一些细节,垃圾收集的前置知识。

可达性分析:追踪通过“根”对象的一系列参考链可以到达哪些对象。算法中定义了几个GC Root对象。这些根对象在GC时不会被JVM回收,然后通过这些对象像分支一样向外扩展。引用的对象表示它们仍在使用中,不会被使用。

根结点枚举

即找出所有的GC Roots,且该过程是STW。所谓GC根是垃圾收集器专用的对象。垃圾收集器收集那些不是GC根并且不能通过GC根的引用访问的对象。

固定的可做为GC Roots的节点主要在全局性的引用(常量,类静态属性)与执行上下文(栈桢的本地变量表),

OopMap
快速完成根结点枚举的实现方式:OopMap。

一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,

在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。

这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找

所以OopMap的作用就是存储内存中哪些位置存储了对象引用。

安全点

OopMap记录的位置(不是随时随地都记录,不然空间占用又是个大问题)
控制用户执行程序不能任意位置停顿下来执行垃圾收集。而是强制要求在安全点才能停顿。

这些特定安全点的主要位置如下:

  • 在方法返回之前
  • 调用方法后
  • 抛出异常的位置
  • 循环结束

咋个保证到了位置就能停呢,HotSpot采用主动式中断方式,即不直接操作线程而是设置一个标志位,线程轮询这个标志位,当为真时,则线程主动中断挂起。
为了使轮询高效,HotSpot使用内存保护陷阱的方式,把轮询操作精简为一条指令,当应该听顿时,设置对应的内存页为只读,则该指令会产生一个自陷异常信号,使线程能挂起。

安全区域

扩展拉伸了的安全点。用于程序不执行的时候,即没法触发安全点的情况,该区域里引用关系不会发生变化,因此只要在区域内就能进行垃圾收集。

记忆集与卡表

跨代引用的记录方案,避免为了找跨代引用关系时扫描整个老年代。
记忆集:用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡表:记忆集的具体的实现。

写屏障

为的是维护卡表的元素,比如其它代的对象引用了本区域的对象,即赋值操作,这个时候需要更新卡表,但是怎么保证卡表的更新呢,特别是在已经编译为机器码时。
写屏障就是机器码层面的一个手段,在每个赋值操作形成一个AOP切面,赋值前后都属于写屏障,这个时候就能把更新卡表放在赋值之后了,从而保证卡表的更新。

并发可达性分析

三色标记

标记对象的状态。用于后续是否被回收的输入条件。
白色: 未被GC访问过的对象。
灰色: 被GC访问过,但是至少还存在一个引用没有被扫描。
黑色: 被GC访问过,且所有引用都被扫描过。
并发扫描对象消失问题
即把原本存活的对象标记为已消亡。

解决办法:

  1. 增量更新(插入引用)
  2. 原始快照(删除引用)

垃圾收集器

G1

里程碑式的收集器,源于开创了基于Region的堆内存布局,可对任何区域进行回收即不区分老年代、新生代。

虽然还是遵循分代理论,但是目标范围不再像以前的收集器,分隔新生代、老年代或者整个堆,而是可以对任何区域进行回收,因为Region布局里的每个区域都可作为新生代或者老年代的空间。

即新生代、老年代不再是固定的。而是一系列Region的动态集合(不需要连续)。

G1追求的不是一次扫描整个新生代或者老年代甚至整个堆,G1通过计算只会扫某些Region,理论上来讲只要收集的速度跟上分配的速度那就能完美一直运行。

JVM Architecture

https://docs.oracle.com/javase/specs/jvms/se11/jvms11.pdf

img

img

JVM中的类的加载器主要有三种:*启动类加载器**拓展类加载器**应用类加载器。*

**启动类加载器(Bootstrap classLoader):**又称为引导类加载器,由C++编写,无法通过程序得到。主要负责加载JAVA中的一些核心类库,主要是位于/lib/rt.jar中。

**拓展类加载器(Extension classLoader):**主要加载JAVA中的一些拓展类,位于/lib/ext中,是启动类加载器的子类。

应用类加载器(System classLoader): 又称为系统类加载器,主要用于加载CLASSPATH路径下我们自己写的类,是拓展类加载器的子类。

https://www.freecodecamp.org/news/jvm-tutorial-java-virtual-machine-architecture-explained-for-beginners/

Types of Java Garbage Collectors

https://www.cnblogs.com/woshimrf/p/jvm-garbage.html

https://javapapers.com/java/types-of-java-garbage-collectors/

img

img

https://www.infoq.cn/article/3wyretkqrhivtw4frmr3

Java 如何有效地避免OOM:善于利用软引用和弱引用

本文引用的内容,如有侵权请联系我删除,给您带来的不便我很抱歉。

Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide:https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573

Java Garbage Collection handbook :https://plumbr.io/handbook/garbage-collection-algorithms-implementations/g1

Shenandoah GC:https://shipilev.net/talks/javazone-Sep2018-shenandoah.pdf

https://stuefe.de/posts/metaspace/what-is-compressed-class-space/
https://shipilev.net/jvm/anatomy-quarks/23-compressed-references/
https://stuefe.de/posts/metaspace/analyze-metaspace-with-jcmd/

Understanding the Java Memory Model and Garbage Collection