Skip to content

JVM

字数: 0 字 时长: 0 分钟

第 1 章 入门

1.1 什么是 JVM

定义:Java Virtual Machine - Java 程序的运行环境(Java 二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

Java 源代码通过 javac 编译成字节码后需要经过 ClassLoader(类加载器)才能够被加载到 JVM 里去运行,类都是放在方法区部分,类创建的实例对象是放在堆,而堆里的对象在调用方法时又会用到虚拟机栈、程序计数器、本地方法栈,方法在执行时的每行代码是由解释器来逐行执行,方法里的热点代码(被频繁调用的代码)会由即时编译器进行优化后执行,而垃圾回收器会把堆中没有被引用的对象进行垃圾回收,而有些代码 Java 不容易实现,比如需要调用操作系统的功能,就要借助本地方法接口来调用操作系统

第 2 章 内存结构

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

2.1 程序计数器

2.1.1 定义

Program Counter Register 程序计数器(寄存器)

  • 作用:是记住下一条 JVM 字节码指令的执行地址
  • 特点:
    • 是线程私有的
    • 不会存在内存溢出

2.1.2 作用

记住下一条 JVM 字节码指令的执行地址

当解释器执行第一条指令时会把下一条指令的地址 3 记录到程序计数器,当第一条指令执行完后,解释器从程序计数器拿到下一条指令的地址 3 执行,当执行 3 时,会把下一条指令的地址 4 记录到程序计数器

程序计数器是线程私有的:

2.2 虚拟机栈

2.2.1 定义

Java Virtual Machine Stacks,Java 虚拟机栈

  • 每个线程运行时所需要的内存称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析:

  • 垃圾回收是否涉及栈内存?不会,因为垃圾回收回收的是堆内存的对象
  • 栈内存分配越大越好吗?不是
  • 方法内的局部变量是否线程安全?
    • 如果方法内的局部变量引用了对象(如果是基本数据类型那没事),如果局部变量没有逃离方法的作用范围,它是线程安全的,否则不是线程安全的

其中 m1 是线程安全的 m2 不是线程安全的,因为 m2 线程和 main 线程共用同一个对象 m3 不是线程安全的,因为 m3 线程 ruturn 了对象,会导致其它线程可能可以拿到这个对象导致线程不安全

2.2.2 栈内存溢出

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

2.2.3 线程运行诊断

案例 1:CPU 占用过多

  • 用 top 定位哪个进程对 CPU 的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id 定位具体是哪个线程对 CPU 占用过高
  • jstack 进程id
    • 可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号

案例 2:程序运行很长时间没有结果

有一种情况是线程死锁导致等了很长时间都没有结果,排查:jstack 进程id

2.3 本地方法栈

给本地方法的运行提供一个内存的空间(本地方法栈)

2.4 堆

2.4.1 定义

Heap 堆:

  • 通过 new 关键字,创建对象都会使用堆内存

特点:

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

2.4.2 堆内存溢出

2.4.3 堆内存诊断

  1. jps 工具:查看当前系统中有哪些 Java 进程
  2. jmap 工具:查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具:图形界面的多功能的监测工具,可以连续监测

2.5 方法区

2.5.1 定义

2.5.2 组成

1.6 的方法区组成,使用永久代作为方法区的实现

1.8 的方法区组成

2.5.3 方法区内存溢出

演示方法区内存溢出:

  • JDK8 之前会导致永久代内存溢出

  • JDK8 之后会导致元空间内存溢出

2.5.4 常量池与运行时常量池

  • 常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池:常量池是 .class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

2.5.5 运行时常量池 StringTable

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder
  • 字符串常量拼接的原理是编译期间优化
  • 可以使用 intern 方法主动将串池中还没有的字符串对象放入串池
    • JDK8 将这个字符串对象尝试放入串池,如果有则并不会放入;如果没有则放入串池,会把串池中的对象返回
    • JDK6 将这个字符串对象尝试放入串池,如果有则并不会放入;如果没有会把此对象复制一份放入串池,会把串池中的对象返回
    • [[intern 方法详解]]

这里 x2 调用 intern 方法,因为 x1 已经把 "cd" 写入到 StringTable 了,所以这里 intern 方法就不写了,所以 x2 还是引用的堆中的对象

这里调换了位置,x2 调用 intern 方法时常量池还没有 "cd",所以 intern 方法会把堆中的对象放入常量池

2.5.6 StringTable 位置

2.5.6 StringTable 垃圾回收

2.5.7 StringTable 性能调优

StringTable 串池与 HashTable 类似,所以性能取决于桶的个数,桶越多,哈希碰撞的概率越低,性能越好,所以对于 StringTable 的调优实际是对桶的个数的调优

  • 调整桶个数:-XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

2.6 直接内存

2.6.1 定义

Direct Memory

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

2.6.2 分配和回收原理

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

第 3 章 垃圾回收

3.1 如何判断对象可以回收

3.1.1 引用计数法

给 Java 对象添加一个引用计数器,每当有一个地方引用它时,计数器 +1;引用失效则 -1

当计数器不为 0 时,判断该对象存活,否则判断为死亡

缺点:无法解决对象间相互循环引用的问题

3.1.2 可达性分析算法

Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到表示可以回收

含三个步骤:

  • 可达性分析
  • 第一次标记和筛选
  • 第二次标记和筛选
  1. 可达性分析

将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索

  • 可作为 GC Root 的对象有:
    • Java 虚拟机栈(栈帧的本地变量表)中引用的对象
    • 本地方法栈中 JNI 引用对象
    • 方法区中常量、类静态属性引用的对象

当一个对象到 GC Roots 没有任何引用链相连时,则判断该对象不可达

注意:可达性分析仅仅只是判断对象是否可达,但还不足以判断对象是否存活/死亡,当在可达性分析中判断不可达的对象会被放在即将回收的集合里

要判断一个对象真正死亡,还需要经理两个阶段:

  • 第一次标记和筛选
  • 第二次标记和筛选
  1. 第一次标记和筛选

对象在可达性分析中被判断为不可达后,会被第一次标记和筛选

  • 不筛选:继续留在即将回收的集合里,等待回收
  • 筛选:从即将回收的集合取出

筛选标准:该对象是否有必要执行 [[finalize()]] 方法

  • 若有必要执行(人为设置),则筛选出来,进入下一阶段
  • 若没有必要执行,判断该对象死亡,不筛选并等待回收。当对象无 finalize() 方法或 finalize() 已被虚拟机调用过,则视为没必要执行
  1. 第二次标记和筛选

当对象经过了第一次的标记和筛选,会被进行第二次标记和筛选

该对象会被放到一个 F-Queue 队列中,并由虚拟机自动建立优先级低的 Finalizer 线程去执行队列中该对象的 finalize() 方法

  • finalize() 方法只会被执行一次
  • 但并不承诺等待 finalize() 运行结束,这是为了防止 finalize() 执行缓慢/停止使得 F-Queue 队列其它对象永久等待

在执行 finalize() 过程中,若对象依然没有与引用链上的 GCRoots 直接关联或间接关联(即关联上与 GC Roots 关联的对象),那么该对象将被判断死亡,不筛选并等待回收

3.1.3 五种引用

  1. 强引用

只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收

  1. 软引用

仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象。可以配合引用队列来释放软引用自身,即(弱引用同理)软引用自身也是一个对象,软引用对象被回收后,软引用自身会被放入引用队列等待回收,因为软引用自身也会占用一定内存,如果要对它们自身占用的内存进行释放,需要使用引用队列来找到它们

就剩一个对象了,其它 null 值的软引用被回收了

  1. 弱引用

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身

  1. 虚引用

必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

创建 ByteBuffer 实现类对象时,就会创建一个名为 Cleaner 的虚引用对象,ByteBuffer 会分配一个直接内存,并会把直接内存地址传递给虚引用对象,将来如果 ByteBuffer 对象不被强引用引用,就会被垃圾回收,但是它分配的直接内存没有被回收,直接内存不受 Java 的垃圾回收管理,所以当 ByteBuffer 被回收时会让虚引用对象进入引用队列,虚引用所在的这个引用队列会由 Reference Handler 线程调用虚引用相关方法(Unsafe.freeMemory)释放直接内存

  1. 终结器引用

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程(优先级很低,被执行的几率很低,会导致线程迟迟不能调用,内存迟迟不能被释放)通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

3.1.4 引用类型与可达性分析算法关系

[[引用类型与可达性分析算法关系]]

3.2 垃圾回收算法

3.2.1 标记清除算法

  • 速度较快
  • 会造成内存碎片

3.2.2 标记整理

  • 速度慢
  • 没有内存碎片

3.2.3 复制

  • 不会有内存碎片
  • 需要占用双倍内存空间

To 区始终空闲着,当发生垃圾回收时,不会回收的对象会被复制到 To 区,然后删除 From 区的所有对象,然后 From 区变为 To 区,To 区变为 From 区,保证 To 区始终空闲

3.3 分代垃圾回收

前面提到的三种垃圾回收算法都各有用处,都会体现在不同的场景下,具体的体现就是在分代垃圾回收机制

刚开始新建的元素会进入伊甸园区,当伊甸园区空间不足时会发生 Minor GC(会 Stop The World,STW),将有标记的对象复制到幸存 To 区并将该对象的寿命 +1,然后清空伊甸园区的对象,交换 From 区和 To 区,保证 To 区一直是空的

伊甸园清空后就可以继续加入新建的对象了,当伊甸园区又要满时,发生第二次 Minor GC,检查伊甸园区的对象和幸存区 From 的对象,将不被回收的对象放入到幸存 To 区,寿命 +1,回收伊甸园区和幸存 From 区的对象,交换 From 区和 To 区

当幸存区的对象的寿命达到一定的阈值时会被放入到老年代中

当老年代空间不足,会先尝试触发 Minor GC,如果之后空间仍不足,那么触发 Full GC

3.3.1 相关 VM 参数

这里有一点:如果有一个大对象,新生代放不下,那就会直接放入老年代,如下图:

这里有一点:如果 OOM 是发生在线程里的,那么主线程会被退出吗(不会):

3.4 垃圾回收器策略

  1. 串行
  • 单线程
  • 堆内存较小,适合个人电脑
  1. 吞吐量优先
  • 多线程
  • 堆内存较大,多核 CPU
  1. 响应时间优先
  • 多线程
  • 堆内存较大,多核 CPU
  • 尽可能让单次 STW 的时间最短

垃圾回收器设计中的核心矛盾:吞吐量、延迟、内存占用

  • 吞吐量:应用程序线程运行时间占总时间的比例,吞吐量 = 应用运行时间 / (应用运行时间 + GC 时间)
  • 延迟:也称为停顿时间,指垃圾回收过程中导致应用程序线程暂停的时长,单次停顿的时长和频率是关键
  • 内存占用:JVM 堆内存的大小

3.4.1 串行垃圾回收器

打开串行垃圾回收器命令:-XX:+UseSerialGC=Serial+SerialOld 其中 Serial 作用在新生代,使用复制算法,SerialOld 作用在老年代,使用标记整理算法

串行回收器是最古老、最基础的单线程垃圾回收器,它的串行体现在两个方面;

  • 垃圾收集线程是单线程的:无论是在新生代还是老年代,它只启用一条垃圾收集线程进行工作
  • 垃圾回收过程是 STW(Stop-The-World,STW) 的,直到回收完毕

工作细节(以 Serial(负责新生代回收) + Serial Old(负责老年代回收,两者都是串行,只不过算法不同) 组合为例):

  • 新生代回收:采用复制算法,在伊甸园区满时触发,将伊甸园区和一个幸存者区中存活的对象复制到另一个幸存者区,并清空伊甸园区和之前的幸存者区
  • 老年代回收:采用标记-整理算法,在老年代空间不足时触发,过程包括:
    • 标记:标记出所有仍然存活的对象
    • 整理:将存活的对象向内存空间的一端移动,压缩排列,避免内存碎片
    • 清除:清理掉边界以外的所有内存
  • 整个过程中,应用程序线程完全停止

特点与用途:

  • 优点:
    • 实现简单,额外内存开销最小(没有线程交互的开销)
    • 在单核 CPU 或资源极度受限的环境下,效率可能反而最高
  • 缺点:
    • STW 停顿时间较长且不可预测,对用户体验伤害极大

启用参数:-XX:+UseSerialGC = Serial + SerialOld

3.4.2 吞吐量优先回收器

其典型代表是 Parallel Scavenge 收集器,它的核心目标是最大化应用程序的吞吐量

  • 并行:指它使用多条线程并行的进行垃圾回收(此时会 STW,只允许垃圾回收线程并行执行),以此充分利用多核 CPU 的计算能力,极大的缩短了垃圾回收的总耗时,从而提高了吞吐量
  • 吞吐量优先:设计者不关心单次 GC 停顿的绝对时间有多长,只关心 GC 的总时间尽可能少,让应用程序线程能最大限度的工作

工作细节(以 Parallel Scavenge + Parallel Old 组合为例):

  • 新生代收集:多线程复制算法
  • 老年代收集:多线程标记-整理算法
  • 其工作流程与串行收集器类似,但关键区别在于每一步的标记、复制、整理工作都是由多条线程并行完成的

特点:

  • 优点:在多核 CPU 上,GC 效率极高,能最大限度的减少 GC 的总时间,从而显著提升吞吐量
  • 缺点:仍然是 STW 的收集器,单次 GC 的停顿时间依然可能很长,不适合交互式应用

参数:

  • 开启参数:-XX:+UseParallelGC / -XX:+UseParallelOldGC,开启一个就会自动开启另一个
  • -XX:ParallelGCThreads= :设置并行 GC 的线程数
  • -XX:MaxGCPauseMillis= :期望的最大 GC 停顿时间,GC 会尝试调整堆大小等参数来努力达到,但可能会以牺牲吞吐量为代价
  • -XX:GCTimeRatio= :直接设置吞吐量目标(0-100 的整数,默认 99,即吞吐量 = 1 - 1/(1 + GCTimeRatio)

3.4.3 响应时间优先回收器

其典型代表是 CMS 和现在的 G1、ZGC、Shenandoah,它的核心目标是尽可能的降低单次 GC 的停顿时间(延迟),使应用程序停顿变得几乎无感

  • 并发:这是实现低延迟的关键,指垃圾回收线程可以与应用程序线程同时工作,大部分 GC 工作不再需要暂停应用线程
  • 响应时间优先:设计者关心的时每次请求能否被快速响应,因此必须将单词 STW 的时间控制在毫秒甚至亚毫秒级别,哪怕这会牺牲一些总的吞吐量或占用更多的 CPU 资源

工作细节(以 CMS 和 G1 为例):

  • CMS:JDK 9 开始被标记为废弃,JDK14 中移除
    • 初始标记:STW,但只标记 GC Roots 直接关联的对象,速度极快
    • 并发标记:并发,GC 线程(多线程)与用户线程一起运行,从 GC Roots 开始进行可达性分析,此过程最长,但无需停顿
    • 重新标记:STW,修正并发标记期间因用户线程运行而产生变动的标记记录,停顿时间比初始标记稍长,但远短于并发标记时间
    • 并发清除:并发,清理垃圾对象
    • 缺点:对 CPU 资源敏感,无法处理浮动垃圾,使用标记-清除算法会产生内存碎片
  • G1:JDK9+ 的默认 GC,定位是替代 CMS
    • 它将堆内存划分为多个大小固定的区,避免了全堆的收集
    • 其核心思想是:优先回收垃圾最多的Region,从而在有限的时间内(通过 MaxGCPauseMillis 参数设定)获取最高的回收效率
    • 虽然仍有一些 STW 阶段(如初始标记、最终标记),但其停顿时间模型是可预测的,它能尽可能的保证每次停顿的时间都不超过用户设定的期望值

启用参数:

  • -XX:+UseConcMarkSweepGC,CMS 已过时
  • -XX:+UseG1GC,G1 JDK9+ 默认
  • -XX:+UseZGC
  • -XX:+UseShenandoahGC
  • -XX:MaxGCPauseMillis=200:为 G1/ZGC 等设置一个期望的最大停顿时间目标
  • -XX:+UseConcMarkSweepGC-XX:+UseParNewGCSerialOld
  • -XX:ParallelGCThreads=n-XX:ConcGCThreads=threads
  • -XX:CMSInitiatingOccupancyFraction=percent
  • -XX:+CMSScavengeBeforeRemark

3.5 G1 垃圾回收器

[[G1 详解]]

  • 同时注重吞吐量和低延迟,默认的暂停目标是 200ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是标记 + 整理算法,两个区域之间是复制算法

相关 JVM 参数:

-XX:+UseG1GC-XX:G1HeapRegionSize=size-XX:MaxGCPauseMillis=time

整体上是标记+整理算法,两个区域之间是复制算法

Young Collection:新生代收集

Young Collection + Concurrent Mark:新生代收集 + 并发标记

Mixed Collection:混合收集

这三者是循环使用的

3.5.1 第一阶段 Young Collection

会 STW

当伊甸园区逐渐被占满,就会出发 Young Collection

会把幸存的对象放入到幸存区

再工作一段时间,当幸存区的对象也比较多了,或者幸存区的对象也到年龄阈值了,触发新生代的垃圾回收,幸存区有一部分对象会晋升到老年代,不够年龄的会复制到另一个幸存区,新生代的也会复制到幸存区

3.5.2 第二阶段 Young Collection + CM(并发标记)

  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,才会进行并发标记(不会 STW),由下面的 JVM 参数决定:-XX:InitiatingHeapOccupancyPercent=percent (默认 45%)

3.5.3 第三阶段 Mixed Collection

会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW

-XX:MaxGCPauseMillis=ms

在全面垃圾回收阶段 E 区可以存活的对象会被复制到 S 区,另外 S 区可以存活的对象会被复制到另一个 S 区,另外还有一些会晋升到 O 区,那么没被复制的就可以被删除了,O 区可以存活的对象会被复制到另一个 O 区,但是这里不是全部都复制,为了保证规定的最大暂停时间,G1 会从老年代里选取最有价值的对象(这几个区域如果回收那么释放的内存空间是最大的)先复制(即优先回收垃圾最多的区域),如果可以保证,那就全复制,然后清理垃圾对象

3.5.4 Young Collection 跨代引用

新生代回收的跨代引用(老年代引用新生代)问题:

新生代回收过程:找到根对象,根对象可达性分析找到存活对象,存活对象进行复制,复制到幸存区

这里就有一个问题:我们要找到新生代对象的根对象,根据根对象要进行查找,根对象有一部分是来自老年代的,老年代通常存活的对象非常多,如果我们要遍历整个老年代去找根对象显然效率非常低,因此采用的是一种卡表的技术,把老年代的区域再进行细分,分成一个个 card,每个 card 大约 512k,如果老年代其中有一个 card 里的一个对象引用了新生代的对象,那么这个对应的 card 就标记为脏 card,这样的好处就是将来就不用去找整个老年代,在做 GC Root 遍历的时候不用找整个老年代,而是只关注于那些脏 card 的区域就可以了,减少搜索范围,如下图:

卡表与 Remembered Set:

其中粉红色的区域就是脏卡区域,它们其中都有对象引用了 E 区的对象,E 区这边会有 Remembered Set 去记录那些从外部对我的引用(即记录都有哪些脏卡),将来对新生代进行垃圾回收的时候,可以先通过 Remembered Set 知道它对应的哪些脏卡,然后在脏卡区去遍历 GC Root,减少了 GC Root 的时间

3.5.5 Remark 重新标记阶段

上图是并发标记阶段时对象的处理状态,黑色的表示处理完成的,有引用在引用它们,是可以存活的对象;灰色的表示正在处理当中的;白色的是尚未处理的,最终灰色的会变成黑色,因为有对象引用,下面的白色最终会变成黑色,因为有对象引用,但上面的白色最终还是白色,因为没有对象引用,最终会被垃圾回收

看一种情况:

并发标记阶段,此时处理到了 B,因为 B 被引用,所有会变成黑色,当处理到 C 的时候,因为是并发标记,同时会有用户线程对对象的引用做修改,此时用户线程把 C 的引用断了,此时处理 C 的时候发现 C 与 B 已经没有联系了,那么处理到 C 的时候会把 C 标记为白色,整个并发标记结束后因为 C 是白色,最终就会被当成垃圾清除

另一种情况:在 C 和 B 处理完了以后,并发标记还没有结束,这时用户线程又改变了 C 的引用地址,把 C 对象当成了 A 的一个属性,如下图:

这时候问题就来了,因为 C 已经是被处理过了,认为已经是白色了,A 也是黑色,以后就不会处理它了,因为已经处理过了,这样整个并发标记结束后 C 就被漏了,所以要对对象的引用做进一步的检查,就是 Remark 重新标记阶段,为了防止这样的现象发生

具体的做法:当对象的引用发生改变时,JVM 就会给它加一个写屏障,就会把 C 加入一个队列并把 C 变成灰色,当整个并发标记结束了,进入重新标记阶段,重新标记阶段会 STW,让其它的用户线程都暂停,重新标记的线程就会把队列中的线程一个个取出来再做一次检查,发现你是灰色的就会做进一步处理,这时发现 C 对象被引用着,就会改为黑色,这样 C 对象就不会被误当成垃圾被回收

3.5.6 G1 优化

3.5.6.1 JDK 8u20 版本中的字符串去重优化
  • 优点:节省大量内存
  • 缺点:略微多占用了 CPU 时间,新生代回收时间略微增加

  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1 并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
  • 注意,与 String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串表
3.5.6.2 JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

3.5.6.3 JDK 8u60 回收巨型对象

  • 一个对象大于 region 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用(即脏 card 引用),这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉
3.5.6.4 JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整:
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空挡空间

3.6 不同的垃圾回收对应的 minor gc 和 full gc

  • SerialGC(串行回收)
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC(并行回收)
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS(并发回收)
    • 新生代内促不足发生的垃圾收集 - minor gc
    • 老年代内存不足,分两种情况,并发失败了之后才叫 full gc,并发没有失败还处于并发收集阶段不会 full gc
  • G1(并发回收)
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足,分两种情况,G1 的老年代内存垃圾回收有一个阈值,当老年代内存达到整个堆内存的 45%(默认),会触发并发标记阶段以及后续的混合收集阶段,这两个阶段的工作过程中,如果回收速度是高于新用户线程产生的垃圾的速度,这个时候不叫 full gc,这个时候还处于并发垃圾收集的阶段,虽然会 STW,但是时间很短,还称不上 full gc;当垃圾回收速度慢于产生的速度,这时候并发收集就失败了,跟 CMS 类似,这时候就退化为串行收集,这时候就叫 full gc,会 STW,时间长

3.7 垃圾回收调优

3.7.1 调优领域

  • 内存
  • 锁竞争
  • CPU 占用
  • IO
  • GC

3.7.2 GC 调优确定目标

追求低延迟还是高吞吐量,从而选择合适的回收器:

低延迟:CMS、G1、ZGC 高吞吐量:ParallelGC

3.7.3 GC 调优需要考虑的问题

最快的 GC 是不发生 GC

  • 查看 Full GC 前后的内存占用,考虑下面几个问题
    • 数据是不是太多?
      • 比如有:resultSet = statement.executeQuery("select * from 大表") 查询过多数据,加载到内存中的数据就过多
    • 数据表示是否太臃肿?
      • 对象图
      • 对象大小
    • 是否存在内存泄漏?
      • 比如有 static Map map = 一直往 map 里存数据,比如缓存,会导致内存泄漏
      • 可以用软引用或者弱引用
      • 也可以使用第三方缓存实现,比如 Redis

3.7.4 新生代调优

新生代的特点:

  • 所有的 new 操作的内存分配很快,TLAB(Thread-Local Allocation Buffer)
  • 死亡对象的回收代价是零(复制算法)
  • 大部分对象用过即死
  • Minor GC 的时间远远低于 Full GC

新生代越大越好吗?不是,最优的大小是:新生代能容纳所有 并发量 * (请求 - 相应) 的数据

幸存区最优大小,大到能保留:当前活跃对象 + 需要晋升对象

晋升阈值配置要得当,让长时间存活的对象尽快晋升:-XX:MaxTenuringThreshold=threshold 调整最大晋升阈值、-XX:+PrintTenuringDistribution 打印详细信息

3.7.5 老年代调优

以 CMS 为例:

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经很好了,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4~1/3
    • -XX:CMSInitiatingOccupancyFraction=percent

第 4 章 类加载与字节码技术

4.1 类文件结构

有源码:

java
public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("hello world");
	}
}

编译为 HelloWorld.class 后是这个样子的:

根据 JVM 规范,类文件规范结构如下:

4.1.1 魔数

  1. u4 magic 占四个字节 魔数

0~3 字节,表示它是否是 class 类型的文件

4.1.2 版本

  1. u2 minor_version u2 major_version 占两个字节 小版本号 占两个字节 主版本号

4~7 字节,表示类的版本,其中 00 34(十六进制 十进制是 52) 表示是 Java 8

4.1.3 常量池

  1. u2 constant_pool_count cp_info constant_pool[constant_pool_count-1] 常量池部分

8~9 字节,表示常量池长度,00 23(十进制是 35) 表示常量池有 #1~#34 项,注意 #0 项不计入,也没有值

#10a(十进制 10) 表示一个 CONSTANT_Methodref 信息,00 0600 15(21) 表示它引用了常量池中 #6#21 项来获得这个方法的所属类和方法名,这样就知道在常量池里有一项是定义了方法,方法的类名和方法名再通过查常量池的另外两项就能知道

其中 0a(十进制 10) 所以对应上表中 CONSTANT_MEthodref 是方法引用的信息

4.1.4 访问标识与继承信息

4.1.5 Field 信息

表示成员变量数量,本类为 0

4.1.6 Method 信息

4.1.7 附加属性

4.2 字节码指令

4.2.1 javap 工具

自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件

4.2.2 图解方法执行流程

4.2.3 练习 - 分析 i++

4.2.4 条件判断指令

几点说明:

  • byte、short、char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码

源码:

字节码:

思考:以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?

4.2.5 循环控制指令

其实循环控制还是前面介绍的那些指令,例如 while 循环:

字节码是:

再比如 do while 循环:

字节码是:

最后再看 for 循环:

字节码是:

注意:比较 while 和 for 的字节码,是一摸一样的

练习:判断结果,从字节码角度分析下列代码运行的结果:

4.2.6 构造方法

4.2.7 方法调用

4.2.8 多态的原理

4.3 编译期处理

4.4 类加载阶段

4.5 类加载器

4.6 运行期优化

Released under the MIT License.

本站访客数 人次 本站总访问量