JVM知识点整理

基于尚硅谷2020版宋红康JVM****

JVM体系结构

运行时数据区 (Runtime Data Area)

元空间 (Metaspace)

  • Java8之前叫方法区 (Method Area)或永久代 (Permanent Generation )
  • 包含:
    • 类的全限定名,如java.io.FileOutputStream
    • 类的直接超类的全限定名,如java.io.OutputStream
    • 类是类类型还是接口类型
    • 类的访问修饰符
    • 直接超接口的全限定名的有序列表,如``java.io.Closeable,java.io.Flushable`
    • 该类型的运行时常量池:类型、方法、属性的符号引用,基本数据类型的常量值。由类加载器从字节码文件的类/接口常量池加载得到
    • 属性
    • 方法
    • 类静态变量
    • 指向ClassLoader类的引用
    • 指向Class类的引用
    • 方法表:存储包括从超类继承而来的,该类型对象可以调用的方法的直接引用,与Java的动态绑定机制相关(类似C++的虚表)
  • 方法区是多线程共享的。方法区会进行垃圾回收。
  • 和堆一样,元空间也会发生java.lang.OutOfMemoryError. 常见的导致元空间OOM的情况有:(1) 加载大量第三方jar包;(2) Tomcat部署的工程过多;(3) 大量动态地生成反射类
  • 元空间使用本地内存而不是虚拟机内存,-XX:MetaspaceSize设置元空间的初始大小,-XX:MaxMetaspaceSize设置元空间大小的最大值。每次发生GC,元空间的大小会根据GC后剩余空间变化,但不会超过-XX:MaxMetaspaceSize设定的最大值
  • JVM方法区的演进细节
    • jdk1.6及以前,有永久代,静态变量和字符串常量池存放在永久代
    • jdk1.7,有永久代,但逐步『去永久代』,静态变量和字符串常量池放在堆中
    • jdk1.8以后,无永久代,类型信息、字段、方法、常量等保存在本地内存的元空间,静态变量和字符串常量池仍在堆
    • 为什么永久代要被元空间替换?(1) 为永久代设置空间大小是很困难的:永久代小了经常触发Full GC, 影响性能,甚至出现OOM;永久代大了,影响其他部分;(2) 永久代难以调优。官方解释:要合并HotSpot和JRockit,JRockit没有永久代,使用JRockit的开发运维人员已经习惯了不设置永久代

虚拟机栈 (Stack)

Java栈是线程私有的,用于存放方法中的局部变量、操作数以及异常数据。当线程调用某个方法时,JVM会根据方法区的该方法字节码组建一个栈帧,并将该栈帧压入线程的Java栈。方法执行完毕,JVM会弹出该栈帧并释放。

栈帧结构
  • 局部变量表
    • 局部变量表是一个数组,存储方法参数和方法内定义的局部变量
    • 局部变量表所需容量大小在编译期间就已经确定
    • 栈帧的大小主要由局部变量表的大小影响,栈帧的大小决定了递归调用的次数
    • 基本存储单元是slot(变量槽)
    • 32位以内的类型占用一个slot,64位的类型占用2个slot
    • 如果当前栈帧是由构造方法或实例方法创建的,那么该对象引用this变量会放在index=0的slot处
    • 局部变量表中的变量是垃圾回收根节点,只要被局部变量表直接或间接引用的对象都不会被回收
  • 操作数栈(表达式栈)
    • 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
    • 新栈帧创建时,操作数栈是空的
    • 入栈指令例:bipush;出栈指令例:istore, iadd
    • 栈顶缓存机制可以部分克服栈式执行引擎带来的指令数过多的问题
  • 动态链接(指向运行时常量池的方法引用)
    • 静态链接:方法调用目标在编译期间就可以确定
    • 动态链接:方法调用目标在编译期间无法被确定,只能在运行时确定。动态链接是多态的体现
  • 方法返回地址
    • 存放调用该方法的pc寄存器的值,即方法的下一条指令的地址

问题:举例栈溢出(StackOverflowError)?调整栈大小(-Xss),一定能保证不出现栈溢出吗?

答:无限递归导致的栈溢出,栈再大都没用

问题2:垃圾回收是否涉及虚拟机栈?

答:不涉及

问题3:方法中定义的局部变量是否线程安全?

答:是线程安全的。局部变量位于栈,栈是线程私有的

本地方法栈 (Native Method Stack)

与Java栈类似,只不过Java栈处理的是Java方法,本地方法栈处理的是native方法

堆 (Heap)

  • 存储运行时创建(new)的对象,由垃圾回收线程负责回收
  • 堆中对象存储的是该对象以及对象所有超类的实例数据
  • 一个对象的引用可能同时在很多地方出现,比如Java栈、堆、方法区
  • 堆中对象还关联一个对象的锁数据信息以及线程的等待集合,与Java的线程同步机制有关
  • 一个JVM实例只存在一个堆内存
  • -Xms设置堆区的初始内存 (-X: JVM运行参数,ms: memory start),-Xmx设置堆区的最大内存 (mx: memory max)。默认情况下堆区初始内存为机器内存/64, 堆区初始内存为机器内存/4
  • Runtime.getRuntime()得到运行时的单例对象
堆的内部结构
  • 新生代:伊甸园区 + [幸存者0区 (From区) + 幸存者1区 (To区)]
  • 老年代
  • -XX:NewRatio=2,设置老年代内存与新生代内存的比例,默认为2:1
  • -XX:SurvivorRatio=8,设置伊甸园区与单个幸存者区的比例,默认为8:1:1
  • -Xmn,设置新生代的内存空间大小(如果设置与-XX:NewRatio冲突,则以-Xmn为准)
  • 几乎所有对象在伊甸园区new出来
  • 绝大部分对象在新生代被销毁
TLAB: Thread Local Allocation Buffer
  • 堆空间是线程共享的,因此存在线程安全问题,锁机制避免线程安全问题,但是会降低内存分配的性能
  • TLAB是位于伊甸园区线程私有的一块堆内存用于对象分配(注意『线程私有』仅仅是对于对象分配而言的,分配出来的对象仍然是对所有线程可见的,只是别的线程无法在该线程的专属空间内分配对象而已)
  • TLAB使得每个线程拥有私有的分配指针,避免了分配内存的同步互斥问题,提高了效率
  • 当一个TLAB用满,重新向伊甸园区申请一块TLAB,这个过程是需要同步互斥的:从这个意义上来说,TLAB的存在将申请内存分配对象的过程『打包』了,减少了线程申请堆内存引发的同步次数

程序计数器 (PC寄存器)

PC是线程私有的,其内容是下一条被执行的指定地址

  • 虚拟机栈、本地方法栈、程序计数器是线程私有的,方法区和堆是线程共享的

问题:为什么需要使用PC寄存器记录当前线程的执行地址?PC寄存器为什么是线程私有的?

答:因为有线程切换要保存和恢复现场

执行引擎

Java的每一个线程都是一个独立的虚拟机执行引擎。JVM是进程级别的,执行引擎是线程级别的

本地方法接口 (JNI) & 本地方法库

调用非Java实现的方法(naive方法)的接口

类加载子系统

详见下方

类加载过程

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

链接

验证

  • 确保class文件的字节流符合当前虚拟机要求,保证被加载类的正确性
  • 主要包含4种验证:文件格式验证、元数据验证、字节码验证、符号引用验证

准备

  • 为类变量分配内存并设置该类变量的默认初始值
  • 不包含final修饰的static,因为常量在编译阶段就已经分配

解析

  • 将常量池内的符号引用转换为直接引用

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中

  • 直接引用:直接引用可以是

    (1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

    (2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

    (3)一个能间接定位到目标的句柄

    直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同

初始化

  • 初始化阶段就是执行类构造器方法<clinit>()的过程
  • 此方法不需要定义,是javac编译器自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并而来
  • 构造器方法中指定按语句在源文件中出现的顺序执行
  • 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
  • 虚拟机保证一个类的<clinit>()方法在多线程下被同步加锁

类加载器

引导类加载器 (Bootstrap ClassLoader)

  • 使用C/C++实现
  • 用于加载Java的核心类库($JAVA_HOME/jre/lib/rt.jar以及resouces.jarsun.boot.class.path路径下的内容),提供JVM自身所需要的类
  • 继承自java.lang.ClassLoader
  • 负责加载扩展类和系统类加载器,并指定为它们的父类加载器

扩展类加载器 (Extension ClassLoader)

  • Java语言编写,由sum.misc.Launcher$ExtClassLoader实现
  • 继承自ClassLoader
  • 父类加载器为引导类加载器
  • java.ext.dirs系统属性所指定的目录加载类库,或从$JAVA_HOME/jre/lib/ext目录下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载

系统类加载器 (应用程序类加载器, Application ClassLoader)

  • Java语言编写,由sum.misc.Launcher$AppClassLoader实现
  • 继承自ClassLoader
  • 父类加载器为扩展类加载器
  • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 是程序中默认的类加载器
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

双亲委派模式

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行
  2. 如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终达到顶层的引导类加载器
  3. 如果父类加载器可以完成类加载任务,就成功返回,否则委派给子类加载器加载

优势

  1. 避免类的重复加载
  2. 保护程序安全,防止核心API被随意篡改

沙箱安全机制

Java安全模型的核心是Java沙箱。沙箱是一个限制程序运行的环境,沙箱机制将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码堆本地系统资源的访问。通过这样的措施来保证堆代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制对系统资源的访问,包括CPU、内存、文件系统、网络。

对象分配

对象分配过程

  1. new出来的对象放在伊甸园区
  2. 当伊甸园区空间满时,程序又要创建对象,JVM的垃圾回收器对伊甸园区进行垃圾回收 (Minor GC), 将伊甸园区和幸存者区中不再被其他对象所引用的对象销毁,再加载新的对象放到伊甸园区
  3. 将伊甸园区中剩余对象移动到幸存者0区,进入幸存者区后对象有年龄标记,初始为1
  4. 之后幸存者区的对象如果在触发GCN时没有被回收,则一直在幸存者0区和幸存者1区之间来回移动
  5. 当对象年龄标记到达阈值15(通过-XX:MaxTenuringThreshold=15设置,默认为15),将对象从幸存者区移动到老年代
  • 幸存者0区和幸存者1区:复制之后就交换,谁空谁是幸存者0区
  • 新对象分配,伊甸园区放不下,触发Minor GC
  • 超大对象新生代放不下,考虑放老年代,如果老年代放不下,触发Major GC;如果老年代仍放不下,触发OOM
  • Minor GC时,伊甸园区对象移动到幸存者区时,如果幸存者区放不下,直接放到老年代,无需等到15岁的阈值

逃逸分析

  • 标量替换:标量指不可进一步被分解的量,在Java中就是基本数据类型;聚合量可以分解为标量的组合。若通过逃逸分析确定对象不会被方法外访问,且对象可以被进一步分解,JVM将对象分解为若干个方法使用的成员变量,在栈上分配
  • 栈上分配:如果经过逃逸分析 (Escape Analysis) 后发现,一个对象并没有逃逸出方法的话,就有可能在栈上分配该对象,从而减少GC. 目前Hotspot虚拟机里的栈上分配就是通过上面说的标量替换实现的,若关闭标量替换 (-XX:-EliminateAllocations),则未发生方法逃逸的对象还是在堆上分配的
  • 同步消除:通过逃逸分析发现一个对象没有被其他线程访问,则去掉对象上的同步锁

对象实例化

创建对象的方式

  • new
    • 变形1:单例模式的静态方法创建对象
    • 变形2:工厂模型的xxxFactory的静态方法
  • Class的newInstance:反射,只能调用空参的构造器,权限必须是public
  • Constructor的newInstance(xxx): 反射的方式,可以调用空参、带参的构造器,权限没有要求
  • 使用clone():需要当前类实现Cloneable接口
  • 反序列化:从文件、网络中获取一个对象的二进制流
  • 第三方库Objenesis

对象内存布局

对象头

  • 运行时元数据:哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
  • 类型指针:指向类元数据InstanceKlass,确定该对象所属的类型
  • 如果是数组对象,还有数组的长度

实例数据

  • 代码中定义的各种类型的字段,包括从父类继承下来的和本身的字段
  • 规则:
    • 父类中的字段出现在子类之前
    • 同级别的字段,相同宽度的字段总是分配在一起
    • 默认情况,子类的窄变量可能插入到父类变量的空隙

对齐填充

因为JVM要求java的对象占的内存大小应该是8bit的倍数,而对象头总是8bit的倍数,所以当实例数据不是8bit的倍数的时候实例数据后面会有若干个字节用于把对象的大小补齐至8bit的倍数。

直接内存

  • Java堆外、直接向系统申请的内存区域
  • 通常,访问直接内存的速度会优于Java堆,因此通常用于读写比较频繁的场合
  • NIO和JVM元空间都使用了直接内存
  • 当直接内存超过最大限制也会导致OOM
  • 直接内存可以通过参数-XX:MaxDirectMemorySize=$SIZE设置大小
  • 缺点:(1) 分配和回收成本较高;(2) 不受JVM内存回收管理

String

注:从jdk9开始,String的底层实现从char[]改为byte[]

字符串常量池

字符串常量池 (String Pool) 是一个固定大小的Hashtable, jdk7的默认值大小是60013, 使用-XX:StringTableSize可设置String Pool的大小。

jdk7开始,字符串常量池从永久代移到了堆区。为什么要从永久代移到堆区?(1) 永久代默认比较小;(2) 永久代很少GC

字符串拼接操作符 +

  • +的两端都是常量或者常量的引用(包括final修饰的变量),+的结果在编译期就会确定
  • +的某一端是变量时,构造StringBuilder对象并使用append方法进行拼接,用toString方法构造新的String对象返回
  • 当存在大量拼接需求时,用StringBuilder替代字符串拼接操作符+

字符串intern方法

  • 如果字符串已经在字符串常量池中,则返回常量池中的引用;否则将字符串添加到常量池并返回引用

垃圾回收

  • 什么是垃圾?垃圾是指运行程序中没有任何指针指向的对象

  • 标记阶段:引用计数算法、可达性分析算法

  • 对象的finalization机制

  • 清除阶段:标记-清除算法、复制算法、标记压缩算法

  • 分代收集算法

  • 增量收集算法、分区算法

标记阶段

判断内存中哪些是存活对象,哪些是死亡对象。当一个对象不再被任何的存活对象继续引用时,就是死亡对象。标记阶段有两种算法,分别是引用计数可达性分析

引用计数

  • 对每个对象保存一个引用计数器属性,记录对象被引用的次数
  • 优点:实现简单,判定效率高,回收没有延迟
  • 缺点:无法处理循环引用的情况(内存泄漏)
  • Java没有选用引用计数算法作为对象标记算法,Python使用的是引用计数

可达性分析

  • GC Roots (根集合): 一组必须活跃的引用
  • 以GC Roots为起点,从上至下搜索被GC Roots所引用的目标对象是否可达
  • 从GC Roots不可达的对象就是可以回收的对象
  • GC Roots包括以下类型:
    • 虚拟机栈中引用的对象
    • 本地方法栈中引用的对象
    • 方法区中类的静态属性引用的对象
    • 方法区中常量引用的对象
    • 所有被同步锁synchronized持有的对象
    • Java虚拟机内部的引用:常驻的异常对象,基本数据类型对应的Class对象,系统类加载器
  • 可达性分析必须在一个能保证一致性的快照中进行,这是导致GC时发生"Stop The World"的原因

finalize方法

对象被回收之前的自定义处理逻辑,一般用于对象回收时的资源释放,例如关闭文件、套接字、数据库连接

不建议主动调用finalize方法,finalize方法由GC自动调用

清除阶段

标记-清除算法

  • 标记:从GC Roots开始遍历,标记所有被引用的对象
  • 清除:遍历堆内存,若对象没有被标记为可达,则回收
  • 缺点:效率不高(两次全遍历);在GC的时候要停止整个程序;产生内存碎片
  • 清除不是真的清除,是维护一个空闲列表,把回收的内存块放入空闲列表
  • 时间开销与管理的内存区域成正比

复制算法

  • 两份内存区域,每次使用一份,GC时将可达对象复制到另一块区域
  • 提高效率(空间换时间)
  • 解决内存碎片问题
  • 新生代大部分对象『朝生夕死』,用复制算法比较合适。实际新生代from区和to区就是使用的复制算法
  • 时间开销与存活对象的数量成正比

标记-压缩算法

  • 标记:同『标记-清除算法』

  • 清除:将所有存活对象压缩到内存一端,按顺序排放

  • 老年代使用标记-压缩算法

  • 时间开销与存活的对象的数据成正比

增量收集算法

  • 垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一部分内存空间
  • 缺点:减少了系统的停顿时间,但是由于线程切换和上下文转换的消耗,会使得垃圾回收的总成本上升,造成系统吞吐量下降

分区算法

将堆空间划分为连续的不同小区间,每个区间独立使用,独立回收

内存泄漏

  • 定义:对象不会被程序用到了,但是又不能被GC回收,成为内存泄漏。长期持续的内存泄漏可能导致OOM
  • 例子:
    • 单例对象持有外部对象的引用:单例对象的生命周期就是应用程序的生命周期,导致持有的外部对象也一直不能释放
    • 未关闭的外部资源的连接:数据库连接,网络 (socket) 连接,io连接

安全点与安全区域

安全点

  • 定义:JVM可以执行GC的指令位置,此时对象的引用关系是确定的
  • 一般安全点是:
    • 循环末尾
    • 方法返回前
    • 方法调用后
    • 抛出异常的位置
  • 如何在 GC 发生时,所有线程都跑到最近的 Safe Point 上再停下来?两种方式:
    • 抢断式中断:在GC发生时,首先中断所有线程,如果发现线程未执行到安全点,就恢复线程让其运行到 安全点上。现在已经没有虚拟机采用抢断式中断
    • 主动式中断:在GC发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。

安全区域

  • 一段代码片段中,对象的引用关系不会发生变化,在这个区域的任何位置开始GC都是安全的
  • 安全区域是为了处理如下的情况:线程处于Sleep状态或Blocked状态,无法响应JVM的中断请求,也无法自己运行到安全点去挂起,因此需要安全区域来解决这种情况

引用的类型

Java的4种引用,强引用、软引用、弱引用、虚引用,它们的引用强度依次减弱

强引用

  • new出来的对象的引用
  • 只要强引用存在,GC就不能回收

软引用

  • 内存不足即回收
  • 应用:内存敏感的高速缓存 (MyBatis)
  • java.lang.ref.SoftReference

弱引用

  • GC发现弱引用即回收
  • 只被弱引用关联的对象只能生存到下一次GC
  • 应用场景也是缓存
  • java.lang.ref.WeakReference

虚引用

  • 虚引用是否存在,不会决定对象的生命周期
  • 设置虚引用的目的是跟踪对象的回收过程
  • 必须和引用队列配合 (java.lang.ref.ReferenceQueue) 使用
  • java.lang.ref.PhantomReference

垃圾回收器

垃圾回收器的分类

  • 线程数:串行、并行
  • 工作模式:并发式、独占式
  • 碎片处理:压缩式、非压缩式
  • 区域:新生代垃圾回收器、老年代垃圾回收器

####垃圾回收器的评价指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例(在服务器程序中重要)
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间(在交互式程序中重要)
  • 内存占用:JVM堆区所占的内存大小
  • 三项指标相互制约

经典垃圾回收器的组合对应关系

  • 新生代收集器:Serial, ParNew, Parallel Scavenge
  • 老年代收集器:Serial Old, Parallel Old, CMS
  • 整堆收集器:G1
  • 垃圾收集器的组合关系

JVM知识点整理

  • 从jdk9开始,G1成为默认垃圾回收器

Serial垃圾回收器

  • 串行回收
  • 复制算法
  • Stop The World机制
  • HotSpot虚拟机Client模式下默认的新生代垃圾回收器
  • 优势:单CPU环境下,没有线程切换的开销

Serial Old垃圾回收器

  • Serial垃圾回收器在老年代的对应版本

  • 串行回收

  • 标记-压缩算法

  • Stop The World机制

  • HotSpot虚拟机Client模式下默认的老年代垃圾回收器

ParNew垃圾回收器

  • Serial垃圾回收器的多线程实现
  • 相对于Serial,缩短了Stop The World的时间

Parallel Scavenge垃圾回收器

  • 并行回收
  • 复制算法
  • Stop The World机制
  • 面向可控制的吞吐量的垃圾回收器
  • 自适应的调节策略
    • 新生代大小
    • Eden和Survivor区的比例
    • 晋升老年代的对象年龄
  • 适合后台运算而不需要太多交互的任务,例如批处理、科学计算

Parallel Old垃圾回收器

  • Parallel Scavenge垃圾回收器在老年代的对应版本
  • 并行回收
  • 标记-压缩算法
  • Stop The World机制

CMS垃圾回收器

  • Concurrent-Mark-Sweep

  • 面向低延迟的垃圾回收器

  • 工作原理

    • 初始标记:仅仅标记出GC Roots能直接关联到的对象,会发生Stop The World,但是速度非常快
    • 并发标记:从GC Roots直接关联对象遍历整个对象图,耗时较长但是不会发生Stop The World
    • 重新标记:修正并发标记阶段因为用户线程继续运行而导致的变动,通常比初始标记的耗时稍长,也会发生Stop The World
    • 并发清理:删除标记阶段已经死亡的对象,由于不需要移动对象(标记-清除算法),因此该步骤与用户线程并发执行

JVM知识点整理

  • 由于在垃圾回收阶段的大部分时间用户线程没有中断,因此CMS回收过程中,还应该确保用户线程有足够的内存可用。因此,CMS垃圾回收器不能像其他垃圾回收器一样等到老年代几乎完全用完再回收,而是当老年代使用率得到某一个阈值,就要开始回收。如果CMS垃圾回收器运行期间预留的内存无法满足程序的需要,就会触发"Concurrent Mode Failure",此时虚拟机会临时启用Serial Old垃圾回收器作为老年代的垃圾回收器
  • 由于使用标记-清除算法,因此会产生内存碎片,因此为新对象分配空间时,不能使用指针碰撞方法,只能使用空闲列表方法
  • 为什么CMS不使用标记-压缩算法?答:因为清除阶段要保证用户线程能够继续执行,所以不能修改对象的地址
  • 优点:
    • 低延迟
  • 缺点:
    • 内存碎片
    • 对CPU资源非常敏感:在并发阶段虽然不会造成用户线程停顿,但是会造成用户线程变慢, 吞吐量下降
    • 无法处理浮动垃圾:并发标记阶段新产生的垃圾无法被标记,导致这些垃圾无法被即时回收
  • jdk9中CMS被标记为Deprecated,jdk14删除了CMS

G1垃圾回收器

  • Garbage First:侧重回收垃圾量最大的region

  • 区域化分代式垃圾回收器

  • 可控的暂停时间下尽可能提高吞吐量

  • 面向服务端的垃圾回收器,主要针对多核CPU以及大内存容量的机器

  • jdk9以后的默认垃圾回收器,可以用于新生代以及老年代的回收

  • 特点

    • 并行与并发:并行(多个GC线程同时工作)+ 并发(GC与用户线程交替进行)
    • 分代收集:堆空间分为若干个region,每个region分别对应Eden区、Survivor区、Old区
    • 空间整合:region之间是复制算法,因此整体上可以看成标记-压缩算法,可以避免内存碎片
    • 可预测的停顿时间模型:每次根据允许的垃圾回收时间,优先回收价值最大的regions
  • 缺点:G1垃圾回收器的内存占用以及额外负载都比CMS高,在小内存应用上CMS优于G1,在大内存上G1优于CMS,分界点大约在6-8G

  • 工作流程

    • 新生代GC
    • 老年代并发标记
    • 混合回收
    • 如果失败,使用单线程、独占式、高强度的Full GC
  • 记忆集 (Remembered Set):检查跨region引用,避免全表扫描,GC效率过低

7种经典垃圾回收器总结

JVM知识点整理

ZGC

  • 目标:在对吞吐量影响不大的前提下,实现在任意堆内存大小下把垃圾回收的停顿时间控制在十毫秒以下
  • 步骤
    • 并发标记
    • 并发预备重分配
    • 并发重分配
    • 并发重映射

相关文章: