《深入理解JVM》读书笔记
2021-02-01 14:11:30 39 举报
AI智能生成
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
作者其他创作
大纲/内容
虚拟机执行子系统
类文件结构
Class类文件的结构
魔数
0xCAFEBABE
确定这个文件是否为一个能被虚拟机接受的Class文件
版本号
主
次
常量池
分类
字面量
文本字符串
被声明为final的常量值
符号引用
被模块导出或者开放的包
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量
结构类型
访问标志(access_flags)
类索引父类索引接口索引集合
指向CONSTANT_Class_info
字段表集合
图
字段表结构
字段访问标志
字段的简单名称
字段和方法的描述符
属性表
Deprecated
ConstantValue
静态变量初始值
Signature
泛型签名
运行时注解相关属性
方法表集合
方法表结构
方法访问标志
简单名称
描述符
特征签名
Exceptions
MethodParameters
记录方法的各个形参名称和信息
Code
attribute_name_index
指向CONSTANT_Utf8_info型常量
“Code”
attribute_length
max_stack
操作数栈深度的最大值
max_locals
包括
方法参数(包括实例方法中的隐藏参数“this”)
显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常)
方法体中定义的局部变量
流给返回值的槽
单位是变量槽(Slot)
重用变量槽
非static方法第一个变量槽存放this
code_length
code
编译后生成的字节码指令
Exception table
LineNumberTable
Java源码行号与字节码行号(字节码的偏移量)之间的对应关系
LocalVariableTable
描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系
LocalVariableTypeTable
使用字段的特征签名来完成泛型的描述
列举出方法中可能抛出的受查异常
类的属性表集合
属性表结构
属性
EnclosingMethod
仅当一个类为局部类或匿名类时才有
标示这个类的外围方法
InnerClasses
内部类列表
SourceFile
源文件名称
描述泛型参数化类型
BootStrapMethods
保存InvokeDynamic指令引用的引导方法限定符
模块化相关属性
字节码指令简介
最基本的执行模型
do { 自动计算PC寄存器的值加1; 根据PC寄存器指示的位置,从字节码流中取出操作码; if (字节码存在操作数) 从字节码流中取出操作数; 执行操作码所定义的操作;} while (字节码流长度 > 0);
指令
加载和存储指令
局部变量加载到操作栈
load
从操作数栈存储到局部变量表
store
常量加载到操作数栈
const
push
ldc
扩充局部变量表的访问索引
wide
运算指令
类型转换指令
对象创建与访问指令
创建类实例
new
创建数组
newarray
访问类字段和实例字段
把一个数组元素加载到操作数栈
Taload
将一个操作数栈的值储存到数组元素中
Tastore
取数组长度
arraylength
检查类实例类型
instanceof
checkcast
操作数栈管理指令
出栈
pop
复制
dup
互换
swap
控制转移指令
方法调用和返回指令
invokevirtual
调用对象的实例方法
解析过程
找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常
否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
invokeinterface
调用接口方法
invokespecial
调用一些需要特殊处理的实例方法
invokestatic
调用类静态方法
invokedynamic
在运行时动态解析出、调用调用点限定符所引用的方法
return
异常处理指令
athrow
同步指令
monitorenter
monitorexit
虚拟机类加载机制
类加载的时机
生命周期
初始化的时机:主动引用
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时
使用new关键字实例化对象的时候
读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
调用一个类型的静态方法的时候
使用java.lang.reflect包的方法对类型进行反射调用时
当初始化类的时候,发现其父类还没有进行过初始化
当虚拟机启动时会先初始化main方法主类
当使用JDK 7新加入的动态语言支持时
当一个接口中定义了default方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
被动引用:不会引起初始化,但不确实是否会引起加载等步骤
通过子类引用父类的静态字段,不会导致子类初始化
通过数组定义来引用类,不会触发此类的初始化
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
接口初始化
编译器会为接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量
一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化
类加载的全过程
链接
加载
步骤
1)通过一个类的全限定名来获取定义此类的二进制字节流。
来源
从ZIP压缩包中读取
JAR、EAR、WAR
从网络中获取
Web Applet
运行时计算生成
动态代理
由其他文件生成
JSP生成
从数据库中读取
从加密文件中获取
防Class文件被反编译的保护措施
非数组类型的加载
Java虚拟机里内置的引导类加载器
用户自定义的类加载器
重写一个类加载器的findClass()或loadClass()方法
数组类型的加载
不通过类加载器创建,由Java虚拟机直接在内存中动态构造出来
如果数组组件类型是引用类型,那就递归采用类加载过程去加载这个组件类型数组将被标识在加载该组件类型的类加载器的类名称空间上
如果数组的组件类型不是引用类型,Java虚拟机将会把数组标记为与引导类加载器关联
数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为public
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
目的
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
1.文件格式验证
保证输入的字节流能正确地解析并存储于方法区之内
是否以魔数0xCAFEBABE开头
主、次版本号是否在当前Java虚拟机接受范围之内
常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据
Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
2.元数据验证
对字节码描述的信息进行语义分析
是否有父类
父类是否继承了不允许被继承的类(被final修饰的类)
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
3.字节码验证
通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
停机问题
4.符号引用验证
发生在解析阶段中虚拟机将符号引用转化为直接引用的时候
是否缺少或者被禁止访问它依赖的某些外类、方法、字段等资源
关闭
-Xverify:none
准备
正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值
如果是 static final 常量,那么会被初始化为 ConstantValue 属性的值
解析
Java虚拟机将常量池内的符号引用替换为直接引用
以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
直接引用
是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
1.类或接口的解析
如果符号引用的目标不是一个数组类型,那虚拟机将会把符号引用的全限定名传递给当前类加载器去加载这个类型
如果符号引用的目标是一个数组类型并且数组的元素类型为对象,那虚拟机那将会按照第一点的规则加载数组元素类型接着由虚拟机生成一个代表该数组维度和元素的数组对象
进行符号引用验证,确认当前类具备对符号引用的目标类型的访问权限
2.字段解析
解析所属的类或接口
如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
否则,查找失败,抛出java.lang.NoSuchFieldError异常
如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
3.方法解析
如果在类的方法表中发现class_index中索引的C是个接口的话,那就直接抛出java.lang.IncompatibleClassChangeError
在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError
否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError
如果查找过程成功返回了引用,将会对这个方法进行权限验证,如果发现不具备对方法的访问权限,将抛出java.lang.IllegalAccessError异常
4.接口方法解析
如果在接口方法表中发现class_index中的索引C是个类而不是接口,那么就直接抛出java.lang.IncompatibleClassChangeError
在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
在接口C的父接口中递归查找,直到java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。
JDK 9中增加了接口的静态私有方法,也有了模块化的访问约束
初始化
执行类构造器<clinit>()方法的过程
类静态变量的赋值
静态语句块
静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
先执行父类的<clinit>()方法
如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法
如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待
其他线程唤醒后则不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会被初始化一次
类加载器
通过一个类的全限定名来获取描述该类的二进制字节流
任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性
每一个类加载器,都拥有一个独立的类名称空间
启动类加载器(Bootstrap Class Loader)
<JAVA_HOME>\\lib
-Xbootclasspath参数所指定的路径中存放的
Java虚拟机能够识别的类库(通过名字)
无法直接获取实例
扩展类加载器(Extension Class Loader)sun.misc.Launcher$ExtClassLoader
<JAVA_HOME>\\lib\\ext
被java.ext.dirs系统变量所指定的路径中所有的类库
应用程序类加载器(Application Class Loader)、系统类加载器sun.misc.Launcher$AppClassLoader
加载用户类路径(ClassPath)上所有的类库
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成
Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,利于结构稳定
避免类重复加载,保护程序,防止API被篡改
打破
子类直接覆盖loadClass()而不是findClass()
原本loadClass()方法的逻辑是,如果父类加载失败,会自动调用自己的findClass()方法来完成加载
线程上下文类加载器
代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
java9模块化
虚拟机字节码执行引擎
运行时栈帧结构
局部变量表
存放方法参数和方法内部定义的局部变量
操作数栈
动态连接
方法返回地址
附加信息
方法调用
非虚方法的调用
静态方法
私有方法
实例构造器
父类方法
final修饰的方法
静态的过程,在编译期间就完全确定
在类加载的时候就可以把符号引用解析为该方法的直接引用
分派
静态分派
依赖静态类型来决定方法执行版本的分派动作
方法重载
发生在解析阶段
动态分派
方法重写
原理
虚方法表
单分派
根据一个宗量对目标方法进行选择
动态
多分派
根据多于一个宗量对目标方法进行选择
静态
动态类型语言
java.lang.invoke包
方法句柄
类加载及执行子系统的案例与实战
Web服务器
部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离
部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享
服务器所使用的类库应该与应用程序的类库互相独立
Tomcat
目录
/common
类库可被Tomcat和所有的Web应用程序共同使用
/server
类库可被Tomcat使用,对所有的Web应用程序都不可见
/shared
类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见
/WebApp/WEB-INF
类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应用程序都不可见
加载器
结构
WebApp类加载器和JSP类加载器通常还会存在多个实例每一个Web应用程序对应一个WebApp类加载器每一个JSP文件对应一个JasperLoader类加载器
当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的HotSwap功能
字节码生成技术与动态代理
高效并发
Java内存模型与线程
主内存与工作内存
内存间交互操作
原子操作
lock
作用于主内存的变量,它把一个变量标识为一条线程独占的状态
unlock
read
作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use
作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
assign
作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
write
作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
规定
不允许read和load、store和write操作之一单独出现
不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存
不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中
不允许在工作内存中直接使用一个未被初始化的变量,对一个变量实施use、store操作之前,必须先执行assign和load操作
一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值
如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)
volatile型变量
作用
可见性
禁止指令重排序
JVM原理
写之前加storestore屏障
写之后加storeload屏障
读之后加loadload屏障、loadstore屏障
汇编原理
赋值操作后增加lock前缀指令
将本处理器的缓存立即写入内存
别的处理器或者别的内核无效化(Invalidate)其缓存(MESI协议)
重排序时不能把后面的指令重排序到内存屏障之前的位置
long和double型变量
原子性的实现
基本数据的读写
synchronized
可见性的实现
volatile
final
有序性的实现
先行发生原则
程序次序规则
在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作
管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作
volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作
线程启动规则
Thread对象的start()方法先行发生于此线程的每一个动作
线程终止规则
线程中的所有操作都先行发生于对此线程的终止检测
线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
传递性
线程
实现方式
使用内核线程实现(1:1实现)
使用用户线程实现(1:N实现)
使用用户线程加轻量级进程混合实现(N:M实现)
java线程
内核线程实现
抢占式
协程
协同式调度的用户线程
纤程
有栈协程的一种特例
线程安全与锁优化
锁优化
自旋锁
避免线程切换的开销
自适应的自旋
自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的
锁消除
逃逸分析
锁粗化
轻量级锁
在代码即将进入同步块的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝
虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位将转变为“00”
如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了
如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态
用CAS操作把对象当前的Mark Word和线程中复制的DisplacedMark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程
偏向锁
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式
使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作
-XX:-UseBiasedLocking
深入理解JVM
自动内存管理
java内存区域与OOM
运行时数据区域
线程共享
方法区
类型的元信息
类名
访问修饰符
字段描述
方法描述
异常信息
运行时常量池
Class文件中的常量池表中的各种字面量和符号引用、及翻译出来的直接引用
具有动态性
Srring#intern
JDK6
重新创建新对象
JDK7
引用原有对象
Class变量和静态变量
JIT代码缓存
堆
存放对象实例以及数组
分配缓冲区(TLAB)
线程私有
提高对象分配效率,解决并发分配问题
内存分配的首选
线程私有(隔离)
虚拟机栈
每个java方法被执行的时候创建栈帧
储存于局部变量槽Slot中
类型
基本数据类型
对象引用
指向对象起始地址的引用指针
指向一个代表对象的句柄
指向其他与此对象相关的位置
returnAddress类型
指向了一条字节码指令的地址
方法参数
局部变量
栈顶缓存
动态链接
指向运行时常量池中该栈帧所属方法的符号引用
为了支持当前方法的代码能够实现动态链接 ( Dynamic Linking). 比如: invokedvnamic指令
执行完毕就出栈
本地方法栈
程序计数器PC
当前执行的虚拟机字节码指令地址
空
例外
直接内存
使用Native函数库直接分配堆外内存
通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作
ByteBuffer
访问速度比堆快
NIO
HotSpot虚拟机中的对象
普通对象的创建过程
字节码new指令
检查这个指令的参数是否能在常量池中定位到一个类的符号引用
检查这个符号引用代表的类是否已被加载、解析和初始化过
没有
先执行相应的类加载过程
把一块确定大小的内存块从Java堆中划分出来,为新生对象分配内存
分配方式
指针碰撞
并发问题
CAS失败重试
本地线程分配缓冲(TLAB)
先在线程的本地缓冲区中分配
分配新的缓存区时才需要同步锁定
-XX:+/-UseTLAB
空闲列表
须将分配到的内存空间(但不包括对象头)都初始化为零值
保证了对象的实例字段在Java代码中可以不赋初始值就直接使用
对对象进行必要的设置
是哪个类的实例
如何才能找到类的元数据信息
对象的哈希码
对象的GC分代年龄
进入java程序的创建步骤,Class文件中的<init>()方法,并把堆内对象的首地址赋给引用变量
实例变量初始值
初始化代码块
构造方法
存放在对象的对象头
对象的内存布局
对象头
对象自身的运行时数据
类型指针
虚拟机通过这个指针来确定该对象是哪个类的实例
记录数组长度的数据
实例数据
程序代码里面所定义的各种类型的字段内容
存储顺序
相同宽度的字段总是被分配到一起
在父类中定义的变量会出现在子类之前
子类之中较窄的变量也允许插入父类变量的空隙之中
+XX:CompactFields参数值为true(默认就为true)
对齐填充
占位符
对象的访问定位
通过栈上的reference数据来操作堆上的具体对象
访问方式
使用句柄
堆中将可能会划分出一块内存来作为句柄池
reference中存储的就是对象的句柄地址
在对象被移动时只会改变句柄中的实例数据指针
直接指针
reference中存储的直接就是对象地址
速度更快
OOM
堆溢出
-Xms
-Xmx
-XX:+HeapDumpOnOutOf-MemoryError
虚拟机栈和本地方法栈溢出
-Xss
-Xoss
方法区和运行时常量池溢出
元空间
-XX:MaxMetaspaceSize
-XX:MetaspaceSize
·-XX:MinMetaspaceFreeRatio
直接内存溢出
-XX:MaxDirectMemorySize
垃圾收集器与内存分配策略
引用类型
强引用
Object obj=new Object()
软引用
SoftReference类
图片缓存
弱引用
WeakReference类
ThreadLocal
WeakHashmap
虚引用
PhantomReference类
配合ReferenceQueue使用
终结引用
FinalReference类
对象存活判断算法
引用计数算法
可达性分析算法
GC Roots的对象
在虚拟机栈(栈帧中的本地变量表)中引用的对象
譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
在方法区中类静态属性引用的对象
譬如Java类的引用类型静态变量
在方法区中常量引用的对象
譬如字符串常量池(String Table)里的引用
static final对象
在本地方法栈中JNI(即通常所说的Native方法)引用的对象
Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
所有被同步锁(synchronized关键字)持有的对象
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
...
判断对象是否需要死亡
调用finalize()方法再来判断
对象将会被放置在一个名为F-Queue的队列之中
在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法
若在finalize()方法中重新被引用,则不会死亡
finalize()方法只能调用一次,下次必定死亡
回收方法区
废弃的常量
没有被引用
不再使用的类型
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法
分代收集理论
分代假说
弱分代假说
绝大多数对象都是朝生夕灭的
强分代假说
熬过越多次垃圾收集过程的对象就越难以消亡
跨代引用假说
跨代引用相对于同代引用来说仅占极少数
收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储
堆分区
新生代
记忆集
老年代
算法
标记-清除(Mark-Sweep)
首先标记出所有需要回收的对象
在标记完成后,统一回收掉所有被标记的对象(或相反)
缺点
执行效率不稳定
大量可回收对象时
内存空间的碎片化问题
STW
标记-复制
一般
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面
然后再把已使用过的内存空间一次清理掉
优点
大量可回收对象时执行效率高
无碎片
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销
可用内存缩小为了原来的一半
改进:Appel式回收
把新生代分为一块较大的Eden空间和两块较小的Survivor空间
每次分配内存只使用Eden和其中一块Survivor
发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上
然后直接清理掉Eden和已用过的那块Survivor空间
标记-整理(Mark-Compact)
标记
让所有存活的对象都向内存空间一端移动
直接清理掉边界以外的内存
暂停用户线程
需要调整引用的地址
移动过程中STW
只有CMS有Major GC
HotSpot算法细节实现
根节点枚举
数据结构OopMap
类加载完成后,会把对象内什么偏移量上是什么类型的数据计算出来
在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用
直接从OopMap得到哪些地方存放着对象引用
都必须要暂停用户线程
安全点
前景问题
导致OopMap内容变化的指令非常多
为每一条指令都生成对应的OopMap需要大量的额外存储空间
所以只在“特定的位置”记录了这些信息
位置
循环跳转
异常跳转
中断方式
抢先式中断
主动式中断
当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起
当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位
轮询标志的地方就是安全点
加上所有创建对象和其他需要在Java堆上分配内存的地方
通过内存保护陷阱的方式,产生陷入中断来实现
安全区域
安全点问题
线程处于Sleep状态或者Blocked状态无法响应虚拟机的中断请求
定义
在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域
虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了
当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段)
如果完成则继续,否则等待
记忆集与卡表
记忆集定义
用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
记录精度
字长精度
对象精度
每个记录精确到一个对象,该对象里有字段含有跨代指针
卡精度
每个记录精确到一块内存区域,该区域内有对象含有跨代指针
卡表
形式:字节数组
卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1
哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描
写屏障
维护卡表状态
卡表何时变脏
引用类型字段赋值的那一刻
如何使卡表变脏
解释执行
插入字节码
即时编译
写屏障(Write Barrier)切面
解决高并发场景下的伪共享
先查再改,不脏才写
-XX:+UseCondCardMark
并发的可达性分析
对象消失的条件
赋值器插入了一条或多条从黑色对象到白色对象的新引用
赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
对应的解决方法
增量更新
黑色对象新插入了指向白色对象的引用之后就变回灰色对象
原始快照
灰色对象要删除指向白色对象的引用关系时记录快照
并发扫描结束后再将这些记录过的引用关系中的灰色对象为根,重新扫描一次
也就是,根本不会删除这个白色对象??
无论是对引用关系记录的插入还是删除,虚拟机的记录操作都通过写屏障实现
垃圾收集器
经典垃圾收集器
Serial收集器
单线程串行回收
额外内存消耗(Memory Footprint)最小
ParNew收集器
Serial收集器的多线程并行回收版本
Parallel Scavenge收集器
多线程并行回收
目标是达到一个可控制的吞吐量(Throughput),最高效率地利用处理器资源
最大垃圾收集停顿时间-XX:MaxGCPauseMillis
吞吐量大小-XX:GCTimeRatio
自适应的调节策略-XX:+UseAdaptiveSizePolicy
Serial Old收集器
标记-整理
CMS的后备回收方案
Parallel Old收集器
CMS收集器
基于标记-清除算法
目标为获取最短回收停顿时间
1)初始标记(CMS initial mark)
标记GC Roots能直接关联到的对象
2)并发标记(CMS concurrent mark)
从GC Roots的直接关联对象开始遍历整个对象图
耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)
删除掉标记阶段判断的已经死亡的对象
对处理器资源非常敏感
默认启动的回收线程数是(处理器核心数量+3)/4
无法处理“浮动垃圾”,不能等到老年代满了再进行GC,有可能出现“Concurrent Mode Failure”失败进而冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集
大量空间碎片,最终仍会STW
-XX:+UseConcMarkSweepGC
全功能
Garbage First G1收集器
特点
面向局部收集的设计思路和基于Region的内存布局形式
面向堆内存任何部分来组成回收集进行回收
把连续的Java堆划分为多个大小相等的独立区域(Region)
-XX:G1HeapRegionSize
取值范围为1MB~32MB,且应为2的N次幂
默认2048个
每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间
多个连续的Humongous Region
存储超过了整个Region容量的超级大对象
大多数情况下作为老年代的一部分来进行看待
将Region作为单次回收的最小单元
垃圾收集思路
记忆集:哈希表
Key是别的Region的起始地址
Value是一个集合,里面存储的元素是卡表的索引号
Region数量多,占用内存更大
解决并发标记问题
两个TAMS指针
并发回收过程中的新对象分配
并发回收时新分配的对象地址都必须要在这两个指针位置以上
默认都是存活的,不纳入回收范围
解决了“只增不减”的情况
可靠的停顿预测模型
衰减均值(Decaying Average)
记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析出平均值、标准偏差、置信度等统计信息
1)初始标记(Initial Marking)
修改TAMS指针的值
借用进行Minor GC的时候同步完成的
扫描根
更新记忆集
处理记忆集
清理、复制对象
处理引用
2)并发标记(Concurrent Marking)
重新处理SATB记录下的在并发时有引用变动的对象
3)最终标记(Final Marking)
修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的SATB记录
4)筛选回收(Live Data Counting and Evacuation)
多线程并行
更新Region的统计数据
对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划
自由选择任意多个Region构成回收集,把存活对象复制到空的Region中,清理掉整个旧Region的全部空间
从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现
三种回收模式
Minor GC
Mixed GC
Full GC
占用内存大、执行负载高,适用于大堆、有延迟要求的场景
参数
-XX: InitiatingHeapOccupancyPercent
设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
低延迟垃圾收集器
Shenandoah收集器
与G1区别
回收阶段多线程
默认不使用分代收集
改记忆集为连接矩阵
工作过程:九个阶段
初始标记
Stop The World
并发标记
最终标记
短暂的停顿
SATB
并发清理
清理那些整个区域内连一个存活对象都没有找到的Region
并发回收
把回收集里面的存活对象先复制一份到其他未被使用的Region之中
读屏障和“Brooks Pointers”转发指针解决用户线程并发
初始引用更新
准备把堆中所有指向旧对象的引用修正到复制后的新地址
建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务
非常短暂的停顿
并发引用更新
真正开始进行引用更新操作
按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值
最终引用更新
修正存在于GC Roots中的引用
最后一次停顿,停顿时间只与GC Roots的数量相关
整个回收集中所有的Region已再无存活对象
再调用一次并发清理过程来回收这些Region的内存空间
转发指针
对象移动与用户程序并发的前景问题
传统做法
在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问到归属于旧对象的内存空间就会产生自陷中断,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上
用户态频繁切换到核心态,代价太大
但如果能有来自操作系统内核的支持的话,就不是没有办法解决,业界公认最优秀的Azul C4收集器就使用了这种方案
在原有对象头最前面统一增加一个新的引用字段
在正常不处于并发移动的情况下,该引用指向对象自己
当对象拥有了一份新的副本时,只需要修改旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上
解决并发写入问题:CAS
1)收集器线程复制了新的对象副本;2)用户线程更新对象的某个字段;3)收集器线程更新转发指针的引用值为新副本地址。
两个线程不能交替进行
要覆盖全部对象访问操作,不得不同时设置读、写屏障去拦截
引用访问屏障
ZGC收集器
目标
在尽可能对吞吐量影响不太大的前提下 ,在任意堆内存大小下垃圾收集的停顿时间限制在十毫秒以内
无分代
内存布局
动态Region
动态创建和销毁
动态的区域容量大小
并发整理算法
染色指针
目的:判断对象是否被移动过
直接将少量额外的信息存储在指针上
存储信息
引用对象的三色标记状态
是否进入了重分配集(即被移动过)
是否只能通过finalize()方法才能被访问到
一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理
但是转发表还得留着不能释放掉
以大幅减少在垃圾收集过程中内存屏障的使用数量
尤其是写屏障
可扩展的存储结构
工作过程
更新染色指针中的Marked 0、Marked 1标志位
并发预备重分配
根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集
类卸载以及弱引用的处理
并发重分配
把重分配集中的存活对象复制到新的Region上
为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系
指针的“自愈”能力
并发重映射
修正整个堆中指向重分配集中旧对象的所有引用
合并到了下一次垃圾收集循环中的并发标记阶段里去完成
节省了一次遍历对象图的开销
之后就释放转发表
Epsilon收集器
只管理内存,不回收垃圾
适用于小应用
日志
-Xlog
内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配
当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
如果已有的对象全部无法放入 Survivor 空间,会通过分配担保机制提前转移至老年代中
大对象直接进入老年代
-XX:PretenureSizeThreshold
长期存活的对象将进入老年代
每个对象定义了一个对象年龄(Age)计数器,存储在对象头中
每熬过一次Minor GC,年龄就增加1岁
-XX:MaxTenuringThreshold
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半
年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保
发生Minor GC之前虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
如果这个条件成立,那这一次Minor GC可以确保是安全的
如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败
如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的
如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC
虚拟机性能监控、故障处理工具
基础故障处理工具
jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
监视虚拟机各种运行状态信息
jinfo:Java配置信息工具
实时查看和调整虚拟机各项参数
jinfo -flag [] [jid]
jmap:Java内存映像工具
生成堆转储快照
查询finalize执行队列、Java堆和方法区的详细信息
jhat:虚拟机堆转储快照分析工具
分析jmap生成的堆转储快照
jstack:Java堆栈跟踪工具
生成虚拟机当前时刻的线程快照
可视化故障处理工具
JHSDB:基于服务性代理的调试工具
JConsole:Java监视与管理控制台
VisualVM:多合-故障处理工具
Java Mission Control:可持续在线的监控工具
程序编译与代码优化
前端编译与优化
Javac编译器
编译过程
1)准备过程
初始化插入式注解处理器
2)解析与填充符号表过程
词法、语法分析
将源代码的字符流转变为标记集合,构造出抽象语法树
填充符号表
产生符号地址和符号信息
语义检查
产生中间代码
符号名地址分配的直接依据
3)插入式注解处理器的注解处理过程
插入式注解处理器的执行阶段
4)语义分析与字节码生成过程
标注检查
对语法的静态信息进行检查
常量折叠
数据流及控制流分析
对程序动态运行过程进行检查
解语法糖
将简化代码编写的语法糖还原为原有的形式
字节码生成
将前面各个步骤所生成的信息转化成字节码
语法糖
类型擦除式泛型
自动装箱、拆箱与遍历循环
条件编译
后端编译与优化
即时编译器
任务
热点代码编译成本地机器码,并以各种手段尽可能地进行代码优化
客户端编译器C1
服务端编译器C2
Graal编译器
分层编译
0
程序纯解释执行,并且解释器不开启性能监控功能
1
使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能
2
仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能
3
仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息
4
使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化
编译对象与触发条件
对象
被多次调用的方法
被多次执行的循环体
条件判定
基于采样的热点探测
周期性地检查各个线程的调用栈顶
基于计数器的热点探测
为每个方法(甚至是代码块)建立计数器,统计方法的执行次数
方法调用计数器
-XX:CompileThreshold
-XX:-UseCounterDecay
热度衰减
-XX:CounterHalfLifeTime
回边计数器
客户端编译器
将字节码构造成一种高级中间代码表示(HIR,即与目标机器指令集无关的中间表示)
从HIR中产生低级中间代码表示(LIR,即与目标机器指令集相关的中间表示)
使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码
服务端编译器
图
提前编译器
编译器优化技术
方法内联
类型继承关系分析
确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息
内联缓存
栈上分配
标量替换
同步消除
公共子表达式消除
数组边界检查消除
0 条评论
回复 删除
下一页