目录
引导类加载器(bootstrap class loader)
扩展类加载器(extensions class loader)
热点代码和栈上替换(OSR:on stack replacement)
垃圾标记阶段:可达性分析算法(也叫跟搜索算法,追踪性垃圾收集)
SecureClassLoader和URLClassLoader分析
内存结构概述
总体图
扩展一下
类的加载
类加载过程概述
类加载器
类的加载过程-加载(loading)
类的加载过程-链接(linking)
类的加载过程-初始化(initialization)
类的加载过程-init函数:
- 对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法
- init是instance实例构造器,对非静态变量解析初始化
如果没有显示定义构造函数,也会自动生成init的构造函数
类的加载过程-clinit函数:
- 是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
- clinit是class类构造器对静态变量,静态代码块进行初始化。
class文件中,看到自动有个clinit的函数(如果没有静态变量或者类的赋值,就不会有这个函数)
调用clinit之前会调用父类的clinit,看下面的示例,调用Son的clinit之前会调用Father的clinit,所以B的值是2
类的加载过程-非法的前向引用
先定义再引用没问题,单如果先引用再定义,会报错:非法的前向引用
类的加载器
概要
引导类加载器(bootstrap class loader)
扩展类加载器(extensions class loader)
系统类加载器(system class loader)
自定义类加载器
类的加载-双亲委派机制
双亲委派原理
一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成
双亲委派机制作用
- 防止重复加载同一个
.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。避免重复加载 - 保证核心
.class
不能被篡改。通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
沙箱保护机制
如下图代码,自己创建了java.lang.String的类,根据双亲委派机制,会由引导加载器加载核心JAR,在核心JAR中没有main方法,结果报错
另外就算不是String类,新建个别的类,只要在这个java.lang的路径下,也会报错,这是出于安全防护的原因
类的加载-判断是否是同一个类
运行时数据区
概述
程序计数器(PC寄存器)
举例说明
虚拟机栈
设计体系-基于栈的架构
基于Stack的操作数都是保存在Stack数据结构中,栈指针去取出数据、将结果存入栈.
1 int a = 33
2 int b = 44
3 int c = a + b
编译出来指令是基于操作数栈的,如下
1 iload_0 //操作数栈读取局部变量的第1个slot
2 iload_1 //操作数栈读取局部变量的第2个slot
3 iadd //将栈顶的两个slot相加
4 istore_2 //保存到局部变量中第3个slo
图形化来说明指令执行的过程
从指令层面上,以上一共执行了4步。但其实指令在解释为机器码的过程中,一个指令或许会引起CPU多个操作步骤。如istore,除了将操作数栈顶的值放到局部变量表之外,还执行了出栈的操作,这对cpu来说是两步操作
我们再从物理层面上来看这个结构
可以发现基于栈的虚拟机,除了和cpu交互之外,就没有和物理层面上有挂钩的点。这样的好处在于:对硬件的要求不高,很大程度上消除对硬件的依赖。
同时,操作数栈是先入后出栈,每次对数据的处理都是通过栈顶的出栈入栈,而局部变量表也是一个类数组的机构。因此使得基于栈的虚拟机指令不需要与地址相关,因此也被叫做0地址指令。
设计体系-基于寄存器的架构
基于寄存器的操作数是存放在CPU的寄存器,指令包含操作数的地址. 劣势(去掉了入栈和出栈的操作,执行得更快,减少了重复计算,指令要长)
一样的代码
1 int a = 33
2 int b = 44
3 int c = a + b
这次编译出来的指令是基于寄存器的
1 add ax bx //其中AX寄存器的值为33,BX寄存器的值为44,将结果放入AX
这里只产生了1条指令。这里我们直接看看他们的物理结构,有助于我们了解背后的实现:
可以看到基于寄存器的JVM没有了操作数栈的概念,而局部量表中也不会直接存储数值,而是持有寄存器的地址。
我们回头看看上面的指令,将ax,与bx两个寄存器上的值相加后将结果储存在ax。因为没有了操作数栈的概念,只需要直接对寄存器地址进行操作。
基于寄存器的虚拟机不采用在内存中处理暂存数据的方式,而在局部变量表中与寄存器建立映射关系。这样的好处在于:因为不用维护栈的关系,所以使得指令数减少。
基于栈虚拟机vs基于寄存器虚拟机
移植性
基于寄存器的虚拟机虽然采用了寄存器地址换算来达到不与硬件信息挂钩,可对寄存器的数量有一定要求的(寄存器也是超贵的)。而基于栈的虚拟机就不要求寄存器了,因此在可移植性上,基于栈的虚拟机是明显更好的。
指令差异
指令长度:在基于栈的JVM中,一个指令占一个字节。而基于寄存器的指令因为要指定地址,所以所需要的长度也更大,一般占两个字节。
指令数:而基于栈的JVM因为要维护出栈入栈,所以执行同样的操作,需要的指令较多,间接使得执行效率较差。而基于寄存器的则不需要考虑。因此网上也很多人用指令较为紧密来形容基于寄存器的指令,使得执行效率也更高。
java虚拟机基于栈架构设计,而不是基于寄存器的
好处是跨平台,不依赖底层硬件,坏处是速度相对慢
虚拟机栈的基本概念
栈的存储
虚拟机栈-局部变量表
虚拟机栈-操作数栈
操作数栈和局部变量表的解析,看下面代码

虚拟机栈-动态链接
虚拟机栈-方法返回地址
栈顶缓存技术
方法的调用
本地方法
什么是本地方法
为什么用本地方法
本地方法栈
堆
概述
堆内存细分-新生区,养老区,元空间/永久区
堆空间结构
堆空间设置
-Xms和-Xmx 堆空间的设置,只涉及到新生代和老年代,不影响元空间
用代码查看
修改新生代和老年代的比例
内存分配过程
内存分配策略
- 有限分配伊甸园区
- 大对象直接分配到老年代
- 动态对象年龄判断
- 空间分配担保
上个图说明
对象分配的特殊情况
TLAB
GC-minor gc,major gc,full gc
minor-gc
major gc
full gc
GC代码
逃逸分析
标量替换
简单地说,就是用标量替换聚合量。这样做的好处是如果创建的对象并未用到其中的全部变量,则可以节省一定的内存。对于代码执行而言,无需去找对象的引用,也会更快一些
-
标量是指不可分割的量,如java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量;
-
如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换;
-
如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是在栈上创建若干个成员变量;
通过-XX:+EliminateAllocations可以开启标量替换, -XX:+PrintEliminateAllocations查看标量替换情况。
方法区
方法区基础概念
方法区存储内容
方法区存储-类型信息:
方法区存储-域信息
方法区存储-方法信息
方法区存储-运行时常量池
方法区在哪里
方法区内部结构
方法区的垃圾回收
方法区的演进变化
JDK8以后静态变量和stringtable(字符串常量池)在堆中,而不是在元空间中
栈,堆,方法区交互
对象的实例化,内存布局
对象实例化步骤
对象内存布局
对象访问定位
主要通过句柄访问和直接指针
句柄访问
直接指针
堆小结
JVM中字符串常量池StringTable在内存中形式分析
字符串常量池(StringTable)
JVM中翻译字符串常量池(StringTable)为“String类型常量表”更合适,常量表它存储以双引号包住的String对象的地址引用,而不是String对象本身。通过StringTable可以实现相同内容的字符串共享。 在Java 8,StringTable是存放在内存堆中。(注意与Constant Pool的区别)。
以下两类会纳入“字符串常量表StringTable”管理:
- 已双引号包住形式申明的字符串String对象
- String对象intern()方法获得的String对象
而:其他方式如new String方式得到的String对象,不会纳入“字符串常量表StringTable”管理
下面分别展示和分析说明。
背景知识:通过java.lang.String的源代码分析,String类的内部是采用char [] 数组存放字符串的内容的。 (JDK9中实现已改为byte[]。鉴于JDK8使用者较多,下文仍以JDK8版本进行讨论)
-
双引号赋值
以上代码 执行过程:
- 由于是String类型,会有“字符串常量表StringTable”管理。 搜索已有的“字符串常量表”(上图中橙色区最下面的多个矩形)看看是否已存在值内容为“11”字符串对象。若存在直接执行第4步; 如不存在则执行第2,3步,4步(这里假设不存在)。
- 由于双引号“11”他是一个String的对象,既先创建new一个String对象(图中橙色区左上角的白色椭圆形)。而String对象需要一个char[]数组,则创建一个数组对象OOP既typeArrayOopDesc对象里面的内容为字符“11”。 然后把String对象的char[]指向这个typeArrayOopDesc对象。 到这步完成双引号“11”对象的创建。
- 将新建立的String对象加入到“字符串常量表”。 既“字符串常量表”中的一个元素,指向新new的String对象(上图中橙色区两个向上的蓝色箭头)。 到这步完成新String对象加入“字符串常量表”。
- 将刚刚纳入“字符串常量表StringTable”管理的String对象地址,赋值给变量s1(上图左侧的蓝色箭头)。
- new String方式
以上代码 执行过程:
- 由于是String类型,会有“字符串常量表StringTable”管理。 搜索已有的“字符串常量表”(上图中橙色区最下面的多个矩形)看看是否已存在值内容为“11”字符串对象。若存在直接执行第4步; 如不存在则执行第2,3步,4步(这里假设不存在)。
- 由于双引号“11”他是一个String的对象,既先创建new一个String对象(上图中橙色区中间的白色椭圆形)。而String对象需要一个char[]数组,则创建一个数组对象OOP既typeArrayOopDesc对象里面的内容为字符“11”。 然后把String对象的char[]指向这个typeArrayOopDesc对象。 到这步完成双引号“11”对象的创建。
- 将新建立的String对象加入到“字符串常量表”。 既“字符串常量表”中的一个元素,指向新new的String对象(上图中橙色区左下角的两个向上的蓝色箭头)。 到这步完成新String对象加入“字符串常量表”。
- 执行上面代码中“new String”。 (可查看java.lang.String的源代码)既 创建一个新的String对象(上图中橙色区左上角的白色椭圆形),然后将刚刚纳入“字符串常量表StringTable”管理的String对象地址,赋值个这个新的String对象(上图中橙色区右上角的蓝色箭头)。
- 把第4步中创建的String对象,赋值给变量s1(上图左侧的蓝色箭头)。
两句双引号(内容相同)
以上第一句代码 执行过程:
- 由于是String类型,会有“字符串常量表StringTable”管理。 搜索已有的“字符串常量表”(上图中橙色区最下面的多个矩形)看看是否已存在值内容为“11”字符串对象。若存在直接执行第4步; 如不存在则执行第2,3步,4步(这里假设不存在)。
- 由于双引号“11”他是一个String的对象,既先创建new一个String对象(图中橙色区左上角的白色椭圆形)。而String对象需要一个char[]数组,则创建一个数组对象OOP既typeArrayOopDesc对象里面的内容为字符“11”。 然后把String对象的char[]指向这个typeArrayOopDesc对象。 到这步完成双引号“11”对象的创建。
- 将新建立的String对象加入到“字符串常量表”。 既“字符串常量表”中的一个元素,指向新new的String对象(上图中橙色区两个向上的蓝色箭头)。 到这步完成新String对象加入“字符串常量表”。
- 在新new的String对象地址,赋值给变量s1(上图左侧下方蓝色箭头)。
以上第二句代码 执行过程:
- 由于是String类型,需要纳入“字符串常量表StringTable”管理。 搜索已有的“字符串常量表”(上图中橙色区最下面的多个矩形)看看是否已存在值内容为“11”字符串对象。(这里已经存在)。
- 从“字符串常量表StringTable”中找到的String对象地址,赋值给变量s2(上图左侧上方蓝色箭头)。
两个new String
以上第一句代码 执行过程:
- 由于是String类型,会有“字符串常量表StringTable”管理。 搜索已有的“字符串常量表”(上图中橙色区最下面的多个矩形)看看是否已存在值内容为“11”字符串对象。若存在直接执行第4步; 如不存在则执行第2,3步,4步(这里假设不存在)。
- 由于双引号“11”他是一个String的对象,既先创建new一个String对象(上图中橙色区最下面的白色椭圆形)。而String对象需要一个char[]数组,则创建一个数组对象OOP既typeArrayOopDesc对象里面的内容为字符“11”。 然后把String对象的char[]指向这个typeArrayOopDesc对象。 到这步完成双引号“11”对象的创建。
- 将新建立的String对象加入到“字符串常量表”。 既“字符串常量表”中的一个元素,指向新new的String对象(上图中橙色区左下角的两个蓝色箭头)。 到这步完成新String对象加入“字符串常量表”。
- 执行上面代码中“new String”。 (可查看java.lang.String的源代码)既 创建一个新的String对象(上图中橙色区左上角下方的白色椭圆),然后将刚刚纳入“字符串常量表StringTable”管理的String对象地址,赋值个这个新的String对象(上图中橙色区左上角下方的白色椭圆)。
- 把第4步中创建的String对象,赋值给变量s1(上图左侧下方蓝色箭头)。
以上第二句代码 执行过程:
- 由于是String类型,会有“字符串常量表StringTable”管理。 搜索已有的“字符串常量表”(上图中橙色区最下面的多个矩形)看看是否已存在值内容为“11”字符串对象。(这里已经存在)。
- 执行上面代码中“new String”。 (可查看java.lang.String的源代码)既 创建一个新的String对象(上图中橙色区左上角上方的白色椭圆),然后从“字符串常量表”StringTable中找到的String对象地址,赋值个这个新的String对象(上图中橙色区左上角上方的白色椭圆)。
- 把第2步中创建的String对象,赋值给变量s2(上图左侧上方蓝色箭头)。
代码和示意图
双引号+变量
出现了变量,就会在堆中申请空间,底层使用StringBuilder
两个双引号相加
如果两个字符串常量(双引号的方式)相加,或者两个final变量相加,编译器会优化成一个字符串常量
比如
"a"+"b"在编译器中会变成"ab",字符串常量池中只有"ab"而没有"a"和"b"
“a”+变量和StringBuilder的区别
在字符串拼接有两种方式,方法1:“a”+变量,方法二:a = new StringBuilder,a.append("字符串"),对比两种方法
“a”+变量:
- 底层每次都需要新new StringBuilder和toString,速度会很慢
- 生成更多的变量,内存占据更多,更容易引起GC
StringBuilder的append:
- 只生成一个变量,速度快,在字符串拼接时应该用这种方法
- 如果大量的append,应该在new StringBuilder(容量)时显示设置大一些容量,因为StringBuilder底层采用数组方式存储,如果append字符串很多,会出现数组扩容,也会产生一些废弃的变量和内存
intern的分析
String.intern()是一个Native方法,底层调用C++的 StringTable::intern
方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。
package com.ctrip.ttd.whywhy;
class Test {
public static void main(String args[]) {
String s1 = new StringBuilder().append("String").append("Test").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder().append("ja").append("va").toString();
System.out.println(s2.intern() == s2);
}
}
在 JDK6 和 JDK7 中结果不一样:
1、JDK6的执行结果:false false
对于这个结果很好理解。在JDK6中,常量池在永久代分配内存,永久代和Java堆的内存是物理隔离的,执行intern方法时,如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用,所以需要谨慎使用intern方法,避免常量池中字符串过多,导致性能变慢,甚至发生PermGen内存溢出。
2、JDK7的执行结果:true false
对于这个结果就有点懵了。在JDK7中,常量池已经在Java堆上分配内存,执行intern方法时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回,所以在JDK7中,可以重新考虑使用intern方法,减少String对象所占的内存空间。
对于变量s1,常量池中没有 "StringTest" 字符串,s1.intern() 和 s1都是指向Java对象上的String对象。
对于变量s2,常量池中一开始就已经存在 "java" 字符串,所以 s2.intern() 返回常量池中 "java" 字符串的引用。
执行引擎
概述
解释器和JIT(Just in time)编译器
为什么解释器和编译器共同存在
案例:热机状态流量切换到冷机状态,导致服务宕机
热点代码和栈上替换(OSR:on stack replacement)
热度衰减
方法调用计数器
回边计数器
C1和C2编译器
垃圾回收
什么是垃圾
垃圾算法
垃圾标记阶段-对象存活判断
垃圾标记阶段:引用计数算法
注意,由于无法处理循环依赖,JAVA没有使用这种算法。不过python通过手动解除和弱引用的方法解决了循环依赖的问题,python使用了引用计数算法
循环引用
垃圾标记阶段:可达性分析算法(也叫跟搜索算法,追踪性垃圾收集)
GC ROOTS
对象的finalization机制
具体过程
垃圾清除阶段
垃圾清除阶段:标记-清除算法
垃圾清除阶段:复制算法
注意,如果垃圾对象很多(即可达对象很少),此方法有效,因为可达对象少代表复制的数据少。如果可达对象很多(垃圾少),复制之后发现全都复制过去了,消耗资源太大
大部分的对象都是朝生夕死,回收率很高,所以在幸存者1和幸存者2区用的是复制算法
垃圾清除阶段:标记-压缩算法
三种垃圾清除算法对比
分代收集算法
增量收集算法
分区算法
三色标记算法
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。
- 黑色:根对象,或者该对象与它的子对象都被扫描
- 灰色:对象本身被扫描,但还没扫描完该对象中的子对象
- 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
当GC开始扫描对象时,按照如下图步骤进行对象的扫描:
根对象被置为黑色,子对象被置为灰色。
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。
这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题
我们看下面一种情况,当垃圾收集器扫描到下面情况时:
这时候应用程序执行了以下操作:
A.c=C
B.c=null
这样,对象的状态图变成如下情形:
这时候垃圾收集器再标记扫描的时候就会下图成这样:
很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:
- 在插入的时候记录对象
- 在删除的时候记录对象
刚好这对应CMS和G1的2种不同实现方式:
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
1,在开始标记的时候生成一个快照图标记存活对象
2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
3,可能存在游离的垃圾,将在下次被收集
引用:强引用,软引用,弱引用,虚引用
安全点
安全区域
垃圾回收器
7款经典垃圾回收器和垃圾分代之间的关系
各垃圾回首器间组合关系,哪些新生代垃圾回收器和老年代垃圾回收器的组合
GC两个很重要的指标:吞吐量和暂停时间
serial串行回收器
ParNew并行回收器
Parallel回收器
吞吐量优先,跟ParNew的底层框架也不一样
CMS回收器
低延迟
CMS过程
G1回收器
G1回收器设计想法
G1回收器的回收过程
G1回收器优势
G1回收器缺点
适用场景
垃圾回收器对比
跨代引用
跨代引用是指新生代中存在对老年代对象的引用,或者老年代中存在对新生代的引用所示,红色的线表示由虚拟机栈中发出的引用。显然B--->A、E--->F都是跨代引用:
跨代引用存在问题
YGC时,为了找到年轻代中的存活对象,不得不遍历整个老年代;反之亦然。这种方案存在极大的性能浪费。因为跨代引用是极少的,为了找出那么一点点跨代引用,却得遍历整个老年代!
卡表(Card Table)
为了支持高频率的新生代的回收,虚拟机使用一种叫做卡表(Card Table)的数据结构,卡表作为一个比特位的集合,每一个比特位可以用来表示年老代的某一区域中的所有对象是否持有新生代对象的引用。
这样新生代在GC时,可以不用花大量的时间扫描所有年老代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有卡表的标记位为1时,才需要扫描给定区域的年老代对象。而卡表位为0的所在区域的年老代对象,一定不包含有对新生代的引用。
卡表中每一个位表示年老代4K的空间,卡表记录未0的年老代区域没有任何对象指向新生代,只有卡表位为1的区域才有对象包含新生代引用,因此在新生代GC时,只需要扫描卡表位为1所在的年老代空间。使用这种方式,可以大大加快新生代的回收速度。
RememberedSet
RememberedSet 用于处理这类问题:比如说,新生代 gc (它发生得非常频繁)。一般来说, gc 过程是这样的:首先枚举根节点。根节点有可能在新生代中,也有可能在老年代中。这里由于我们只想收集新生代(换句话说,不想收集老年代),所以没有必要对位于老年代的 GC Roots 做全面的可达性分析。
每个Region有一个Rset,在可达性分析时,Rset会作为GC Roots的一部分用于分析哪些是垃圾,哪些是可用的
当 Region 被引用较多的情况,RSet 占用空间会上升,因此对 RSet 的记录划分了三种存储粒度:
- 稀疏表(Sparse):直接通过哈希表来存储,key 是 region index,value 是 card 数组(记录 card index)
- 细粒度(Fine):当一个 region 的 card 数量超过阈值时,退化为一个 bitmap,每一位对应一个 card(index)
- 粗粒度(Coarse):当被引用的 region 数量超过阈值时,退化为只记录 regin 引用情况,由 bitmap 存储,每一位对应一个 region(index)
写屏障(Write Barrier)
我们每次对引用进行改变时,我们在程序中并没有手动去维护卡表的信息,那么卡表信息的维护到底是如何进行的呢,这就依赖于我们的写屏障功能。
写屏障可以理解为对于我们引用类型字段复制的AOP操作。在赋前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的部分的写屏障叫作写后屏障(PostWrite Barrier)
STAB详解
概述
STAB全称Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:
- 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉,即灰色节点的子节点。
- 灰:对象被标记了,但是它的field还没有被标记或标记完。
- 黑:对象被标记了,且它的所有field也被标记完了。
由于并发阶段的存在,那就有可能在并行运行期间之前的标记过的对象的引用关系可能被改变,就会出现白对象漏标的情况,这种情况发生的前提是:
- 把一个白对象的引用存到黑对象的字段里,如果这个情况发生,因为标记为黑色的对象认为是扫描完成的,不会再对它进行扫描。
- 某个白对象失去了所有能从灰对象到达它的引用路径。
对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?
如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误。
SATB算法机制中,会在GC开始时先创建一个对象快照,在并发标记时所有快照中当时的存活对象就认为是存活的,标记过程中新分配的对象也会被标记为存活对象,不会被回收。这种机制能够很好解决新创建对象漏标的情况。STAB核心的两个结构就是两个Bitmap。
Bitmap分别存储在每个Region中,并发标记过程里的两个重要的变量:preTAMS(pre-top-at-mark-start,代表着Region上一次完成标记的位置) 以及nextTAMS(next-top-at-mark-start,随着标记的进行会不断移动,一开始在top位置)。SATB通过控制两个变量的移动来进行标记,移动规则如下:
- 假设第n轮并发标记开始,将该Region当前的Top指针赋值给nextTAMS,在并发标记标记期间,分配的对象都在[ nextTAMS, Top ]之间,SATB能够确保这部分的对象都会被标记,默认都是存活的。
- 当并发标记结束时,将nextTAMS所在的地址赋值给previousTAMS,SATB给[ Bottom, previousTAMS ]之间的对象创建一个快照Bitmap,所有垃圾对象能通过快照被识别出来。
- 第n+1轮并发标记开始,过程和第n轮一样。
如下示意图显示了两轮并发标记的过程:
- A阶段,初始标记阶段,需要STW,将扫描Region的Top值赋值给nextTAMS。
- A-B阶段:并发标记阶段。
- B阶段,并发标记结束阶段,此时并发标记阶段生成的新对象都会被分配在[nextTAMS,Top]之间,这些对象会被定义为“隐式对象”,同时
_next_mark_bitmap
也开始存储nextTAMS标记的对象的地址。 - C阶段,清除阶段,
_next_mark_bitmap
和_prev_mark_bitmap
会进行交换,同时清理[ Bottom, previousTAMS ]之间被标记的所有对象,对于“隐式对象”会在下次垃圾收集过程进行回收(如第F步),这也是SATB存在弊端,会一定程度产生未能在本次标记中识别的浮动垃圾。
SATB利用pre-write barrier,将所有即将被修改引用关系的白对象旧引用记录下来,最后以这些旧引用为根重新扫描一遍,以解决白对象引用被修改产生的漏标问题。
在引用修改时把原引用保存到satb_mark_queue中,每个线程都自带一个satb_mark_queue。在下一次的并发标记阶段,会依次处理satb_mark_queue中的对象,确保这部分对象在本轮GC中是存活的。
如果被修改引用的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。
字节码
字节码文件
概要
代码和生成的字节码
class文件格式
class文件-常量池
常量池分为常量池计数器和常量池表
- 常量池计数器用于表示有多少个元素
- 常量池表分为字面量和符号引用
常量类型
class文件-访问标识
class文件-类索引,父类索引,接口索引
class文件-字段表集合
class文件-方法表集合
class文件-属性表集合
里面的属性有一个是CODE,下面是CODE属性的详解
class语法分析
变量入栈指令load
常量入栈指令const,push,ldc
eg:
出栈指令store
算数指令
创建指令
字段访问指令
数组操作指令
方法调用指令
方法返回指令
操作数栈管理指令
控制转移指令
异常处理指令

同步控制指令
类的加载过程
概述
类的加载
链接
验证
准备阶段
解析阶段
初始化
卸载
关系图
类的加载器
源码分析
classloader分析
代码分析
SecureClassLoader和URLClassLoader分析
forName和loadClass
破坏双亲委派机制
线程上下文类加载器
热替换实现
代码示例
改成
编译一下, 第一段代码不重启的时候,加载了修改后的类,输出如下
沙箱安全机制
参考资料
基于栈虚拟机vs基于寄存器虚拟机_Ray_Sir_Java的博客-优快云博客
JVM中字符串常量池StringTable在内存中形式分析_zyplanke的专栏-优快云博客
深入理解java虚拟机