1.JVM 的组成#
- 类加载器:加载.class 文件进入 jvm 内存
- 运行时数据区:主要由程序计数器、虚拟机栈、本地方法栈、方法区、堆组成
- 执行引擎:对 JVM 指令进行解析,翻译成机器码,解析完成后提交到操作系统中
- 本地库接口:供 java 调用的融合了不同开发语言的原生库
- 本地方法库:Java 本地方法的具体实现
2. 类加载器#
2.1 分类
- 根加载器:加载本地方法类
- 扩展加载器:加载 jdk 内部实现的扩展类
- 系统加载器:加载程序中的类文件
2.2 双亲委派机制
加载一个 class 文件至 jvm 内存中时,子类加载器会将加载请求派送至父类加载器,由父类加载器首先进行加载,如果父类加载器无法加载 class 文件则由子类加载器加载。
系统加载器扩展加载器根加载器
3. 执行引擎#
3.1 翻译#
- 即时编译器:对于程序中的热点代码,即时编译器会保存对应代码的机器码,在后续遇到相同代码的时候使用保存的机器码进行替代。
- 字节码解释器:将字节码解释成机器码
4. 运行时数据区#
4.1 堆#
4.1.1 组成部分
- 新生代:包含 eden、survivor(to、from)两个区
- 老年代
4.1.2 垃圾回收机制
如何判断垃圾
- 引用计数法:为每一个对象分配一个引用值,表示该对象被引用的次数,会造成循环引用问题。
- 可达性分析:从 gc roots 开始向下遍历,没有被遍历到的对象则是垃圾;
gc roots 包括虚拟机栈中栈帧引用的局部变量,本地方法栈中引用的局部变量,方法区中类的静态变量,方法区中常量池中的变量以及 synchronized 加锁对象。
如何回收垃圾
标记清除算法、标记复制算法、标记整理算法、分代收集算法
分类
新生代垃圾回收器:serial、parallel new、parallel scavenge
老年代垃圾回收器:serial old、parallel old、cms
混合:G1
什么时候开启垃圾回收
- young GC
当 young gen 中的 eden 区分配满的时候触发。注意 young GC 中有部分存活对象会晋升到 old gen,所以 young GC 后 old gen 的占用量通常会有所升高。 - major GC
当老年代区达到一定阈值的时候进行(一般只有 cms 才执行 major gc?) - full GC
- 当创建一个大对象,Eden 区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发 Full GC
- 当元空间满的时候,也会出发 Full GC
- 在新生代回收内存时,由 Eden 区和 Survivor From 区把存活的对象向 Survivor To 区复制时,对象大小大于 Survivor To 空间的可用内存,则把多出的对象转存到老年代 (这个过程称为分配担保);这个时候老年代的可用内存小于该对象大小,则会判断是否允许担保失败;如果允许,那么就会判断老年代最大可用连续空间是否大于历次晋升到老年代的对象大小,如果大于,则尝试一次 young gc;否则,便会触发 Full GC
- 调用 System.gc ()
CMS
- 三色标记法
- 主要流程
- 初始标记:会 stw,对老年区中的 gc roots 以及年轻区中直接指向的老年区对象标记为灰色。
- 并发标记:不会 stw,对标记为灰色的对象进行遍历,遍历的对象标记为灰色,入口对象标记为黑色,重复以上步骤。
- 并发预处理:不会 stw,对并发标记中产生的 dirty card 进行重新标记。
- 重新标记:会 stw,我认为这里主要是解决错检问题,cms 会设置一个写屏障将修改了内部引用的对象重新标记为灰色,重新标记对这些灰色对象进行遍历,防止错检。
- 并发清理:不会 stw,清除所有白色对象。
- 缺点
- 占用 cpu 资源
- 产生浮动垃圾:并发清理阶段用户仍在产生垃圾,需要下次 gc 进行清理
- 内存碎片:CMS 采用标记清除算法,会产生大量内存碎片
- 并发失败:由于浮动垃圾的存在,因此 CMS 必须预留一部分空间来装载这些新产生的垃圾。CMS 不能像 Serial Old 收集器那样,等到 Old 区填满了再来清理。在 JDK5 时,CMS 会在老年代使用了 68% 的空间时激活,预留了 32% 的空间来装载浮动垃圾,这是一个比较偏保守的配置。如果实际引用中,老年代增长的不是太快,可以通过
-XX:CMSInitiatingOccupancyFraction
参数适当调高这个值。到了 JDK6,触发的阈值就被提升至 92%,只预留了 8% 的空间来装载浮动垃圾。如果 CMS 预留的内存无法容纳浮动垃圾,那么就会导致「并发失败」,这时 JVM 不得不触发预备方案,启用 Serial Old 收集器来回收 Old 区,这时停顿时间就变得更长了。
G1
- 分区
G1 采用了分区 (Region) 的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1 并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 - XX=n 可指定分区大小 (1MB~32MB,且必须是 2 的幂),默认将整堆划分为 2048 个分区。
在 G1 中,还有一种特殊的区域,叫 Humongous 区域。 如果一个对象占用的空间超过了分区容量 50% 以上,G1 收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC。 - 对象分配策略
- TLAB (Thread Local Allocation Buffer) 线程本地分配缓冲区:
TLAB 为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在 Eden 空间中,每一个线程都有一个固定的分区用于分配对象,即一个 TLAB。分配对象时,线程之间不再需要进行任何的同步。 - Eden 区中分配:
对 TLAB 空间中无法分配的对象,JVM 会尝试在 Eden 空间中进行分配。如果 Eden 空间无法容纳该对象,就只能在老年代中进行分配空间。 - Humongous 区分配
-
young gc 过程
- 初始标记:会 stw,标记所有的 gc roots 直接关联的对象(即,gc roots 本身)
- 处理 & 更新 rset:处理 RSet 的信息并且扫描,将老年代对象持有年轻代对象的相关引用都加入到 GC Roots 下
- 清理阶段:stw,打包所有 young region 为 cset,如果预测回收时间小于给定的容忍时间,那么 g1 会增加 eden 区的 region 数,不进行清理;否则,如果预测回收时间大于给定容忍时间,那么 g1 会执行标记复制操作,将所有存活对象复制到空的 survivor 中。
-
mixed gc 过程
-
初始标记:该过程是和 young GC 的暂停过程一起的
-
根区域扫描
-
并发标记阶段:出现了引用修改(不包含新分配内存给对象),那么写屏障会把这些引用的原始值捕获下来,记录在 log buffer 中。而后再处理。后续的所有的标记,都是从原来的值出发,而不是从新的值出发的。处理新创建的对象,G1 采用了不同的方式。G1 用了两个 TAMS 变量了判断新创建的对象。一个叫做 previous TAMS,一个叫做 next TAMS。位于两者之间的对象就是新分配的对象。
-
重新标记:stw,处理每个线程遗留的 SATB write barrier 的对象引用。
-
清理阶段