JVM
2021-05-12 11:04:22 17 举报
AI智能生成
根据尚硅谷宋红康老师视频和可见汇总,视频B站能搜到
作者其他创作
大纲/内容
Class文件
文件结构
01-魔数
02-Class版本号
03-常量池:存储所有常量
04-访问标识
05-类索引、父类索引、接口索引
06-字段表集合
07-方法表集合
08-属性表集合
字节码指令
05-对象的创建与访问指令
类加载子系统
类加载器
类加载器
引导类加载器
BootStrapClassLoader
启动类加载器
启动类加载器
C++实现,不能通过getParent获取,String类是由引导类加载器加载的
Java的核心类库(rt.jar、resource.jar、sun.boot.class.path路径下的内容)都由引导类加载器加载
Java的核心类库(rt.jar、resource.jar、sun.boot.class.path路径下的内容)都由引导类加载器加载
并不继承ClassLoader,没有父类加载器
加载扩展类和应用程序类加载器,并指定为它们的父类加载器
出于安全考虑,BootStrap只加载包名为java、javax、sun等开头的类
自定义类加载器
所有派生于抽象类ClassLoader的类加载器
所有派生于抽象类ClassLoader的类加载器
ExtensionClassLoader
扩展类加载器
扩展类加载器
java语言编写
派生于ClassLoader类
父类为启动类加载器
从java.ext.dirs系统属性所指定的目录下加载库,或从jdk安装目录的jre/lib/ext子目录下加载类库
AppClassLoader
应用程序类加载器
应用程序类加载器
java语言编写
派生于ClassLoader
父类加载器为扩展类加载器
负责加载环境变量classpath或系统属性,java.class.path指定路径下的类库
该类是程序中默认的类加载器,一般来说,Java应用的类都由它完成加载
通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
用户自定义的类加载器
起因
隔离加载类
修改类的加载方式
扩展加载源
防止源码泄漏
实现步骤
继承ClassLoader
自定义逻辑写在findClass()方法中
如果需求不复杂,可以直接继承URLClassLoader类,这样可以避免自己编写findClass()方法及其获取字节码流的方式
ClassLoader
抽象类
除了启动类加载器都继承它
获取ClassLoader的方式
clazz.getClassLoader()
获取当前类的
Thread.currentThread().getContextClassLoader()
获取当前线程上下文的
ClassLoader.getSysytemClassLoader
获取系统的
DriverManager.getCallerClassLoader()
获取调用者的
双亲委派机制
原理
- 一个类加载器收到类加载的请求,不会自己先去加载,而是会把这个请求委托给父类加载器执行
2.如果父类加载器还存在其父类加载器,继续向上委托,依次递归,最终到启动类加载器
3.如果父类完成类加载,就成功返回;如果无法完成,字加载器才会自己尝试去加载
优势
避免类的重复加载
保护程序安全,防止核心API被随意篡改
沙箱安全机制
类加载
加载
Loading
Loading
- 通过一个类的全限定名获取定义此类的二进制字节流
2.将字节流所代表的的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
ps:加载.class文件的方式
本地系统中直接加载
通过网络获取,eg:Web Applet
从zip压缩包中读取,成为日后jar、war格式的基础
运行时计算生成,eg:动态代理
由其他文件生成,eg:Jsp应用
从专有数据库提取.class文件
从加密文件中获取,典型的放Class文件被反编译的保护措施
链接
Linking
Linking
验证
Verification
Verification
确保Class文件内信息符合虚拟机要求,保证加载类的正确性
四种验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
Preparation
Preparation
为类变量分配内存并设置该变量的默认初始值,即零值
这里不包含final修饰的static,因为final在编译时就会分配了,准备阶段会显式初始化
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着变量一起分配到Java堆中
解析
Resolution
Resolution
将常量池内的符号引用转换为直接引用的过程
解析操作往往会伴随着JVM执行完初始化之后再执行
符号引用就是 一组符号来描述所引用的目标。
符号引用的字面量形式明确定义在《Java虚拟机规范》的class文件格式中。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
符号引用的字面量形式明确定义在《Java虚拟机规范》的class文件格式中。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、Constant_Methodref_info等
初始化
Initialization
Initialization
初始化阶段就是执行类构造器方法<clint>()的过程
此方法不需要定义,是javac编译器自动收集类中的所有变量的赋值动作和静态代码块中的语句合并而来
构造器方法中指令按语句在源文件中出现的顺序执行
<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())
若该类有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
其它
两个class对象是否为同一个类
类的完整类名必须一致,包括包名
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
对类加载器的引用
类的主动使用与被动使用
主动使用
创建类的实例
访问某个类或接口的静态变量,或对该静态变量赋值
调用类的静态方法
反射(Class.forName(“com.xx.Test"))
初始化一个类的子类
Java虚拟机启动时被标明为启动类的类
JDK7开始提供动态语言支持
被动使用
除了主动使用的7中情况,其他使用Java类的方式都被看作对类的被动使用,都不会导致类的初始化
运行时数据区
简图
运行时数据区
线程共享
程序计数器
简图
定义
每个线程独立拥有
存储指令相关的现场信息,非物理寄存器
存储指向下一条指令的地址,也即将要执行的指令代码。
由执行引擎读取下一条指令
特点
- 占内存空间小,几乎可以忽略不计,运行速度很快
- 每个线程都有程序计数器,线程私有,生命周期与线程保持一致
- 任何时间都有一个方法在执行,也就是当前方法,程序计数器会存储当前线程正在运行的当前方法的指令地址;如果是native方法,则是未指定值
- 它是程序控制流的指示器,分支、循环、异常处理,线程恢复等基础功能都需要依赖这个计数器完成
- 字节码解释器工作时就是通过改变程序计数器存储的值选取下一条将要执行的字节码指令
- 它是唯一一个在java虚拟机规范中没有规定任何OutOfmemoryError的区域
作用
cpu在各个线程不停切换,切换回来时,可以知道继续从哪执行
JVM字节解释器通过改变程序计数器的值来明确下一条要执行的是什么字节指令
CPU时间片
即CPU分配给各个程序的时间,每个线程被分配一个时间段,称为它的时间片
宏观上:打开多个程序并行不悖,并行
微观上:是CPU在各个程序间疯狂切换,一次执行一部分,轮流执行,串行
虚拟机栈
概述
Java的指令是根据栈设计的,不同的平台架构不同,所以不能基于寄存器设计
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,同样的功能需要更多指令
栈管运行,堆管存储
是什么
每个线程在创建的时候都会创建一个虚拟机栈,其内部保存着一个个栈帧,对应值一次次的方法调用
线程私有的
生命周期
和线程一致
作用
主管java程序的运行
保存方法的局部变量,部分运行结果,并参与方法的运行和返回
存储
每个线程都有自己的栈,栈中的数据以栈帧的形式存储
在这个线程上,每个执行的方法对应着自己的栈帧
栈帧是个内存区块,是一个数据集,维系着方法执行中的各种数据
特点
快速高效的分配存储方式,速度仅次于程序计数器
JVM直接对栈的操作只有两个
每个方法执行,进栈
方法执行结束,出栈
不存在垃圾回收问题
不需要GC,但可能会OOM
原理
JVM直接对Java栈的操作只有“出栈”和“入栈”两个操作
执行的始终是最顶端的栈帧
如果一个方法调用了别的方法会创建一个新的栈帧在最顶端
最顶端的栈帧如果被别的方法调用会在返回时把执行结果返回调用它的栈帧
执行引擎的所有字节码指令只对当前栈帧进行操作
不同线程中的栈帧是不能相互引用的,即不能在一个栈帧中引用另一个线程的栈帧
一条活动的线程中,一个时间点,只有一个活动的栈帧
Java方法有两种返回方式,不管哪种方式都会被弹出栈
正常函数返回,return指令
抛出异常
栈帧
局部变量表
什么用
- 定义了一个数字数组,主要用于存储方法参数和定义在方法内的局部变量
基本数据类型
对象引用
returnAddress
- 因为是在线程的栈帧里,所以不存在数据安全问题
- 局部变量表的大小编译期就确定下来的,保存在方法的code的“局部变量最大槽数”,方法运行期间大小不改变
- 方法调用嵌套的次数由栈的大小决定。一般来说,栈越大,方法可嵌套的次数越多
- 局部变量表中的变量只在当前调用方法中有效。在方法执行时,虚拟机通过局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表随之销毁
slot
参数值的存放总是从局部变量数组的index 0 开始,到index -1结束
局部变量表中,最基础的存储单位是slot(槽)
局部变量表中
32位以内的类型占一个slot
byte,short,char会转为int
Boolean会转为int
int
returnAddress
64位的类型占两个slot
long
double
jvm为每个slot分配一个访问索引,通过这个索引即可访问到局部变量表中的局部变量值
对于占两个槽(64位变量)的,访问它的第一个索引即可
实例方法被调用时,它的参数和局部变量会按照顺序复制到一个个slot中
如果当前方法是构造方法,或实例方法,那index 0 处的slot放的是参数this
栈帧找那个的局部变量表中的槽位是可以重复利用的
如果一个局部变量用完没用了,那下面要用到的局部变量可能会复用它的槽位
节省资源
变量
类变量与局部变量
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配
类变量有两次初始化机会
第一次:链接的准备阶段,执行系统初始化,赋予零值
第二次:初始化阶段,赋予程序员在代码中定义的值
和类变量不同,局部变量表不存在系统初始化,所以一旦定义看局部变量就得出初始化,不然不能用
变量的分类
按数据类型分
基础数据类型
引用数据类型
按位置
成员变量
都经历过初始化的默认赋值
类变量
linking的prepare阶段,赋予零值
initial阶段,赋值
static修饰,即静态变量
存放在方法区中(也在堆中)
优先于对象存在
被所有对象共享
随着类的消失而消失
实例变量
随着实例的创建而创建
放在堆中
没有static修饰
随着对象的消失而消失
局部变量
使用前必须显式赋值
实例在堆中
引用在局部变量表中
补充
栈帧中,与性能调优关系最密切的就是局部变量表。在方法执行时虚拟机利用局部变量表完成方法的传递
局部变量表中的变量是重要的垃圾回收节点,只要被局部变量表中的变量直接或间接引用的对象都不会被回收。
操作数栈
每个栈帧都有的,随着栈帧的创建而创建,刚创建时是空的
方法执行时,根据字节码指令,往栈中写入或提取数据,入栈或出栈
复制
交换
求和
....
栈的深度是明确的,编译期就确定的,就是字节码方法的code的max_stack的值
操作数栈并非通过索引访问数据的,只是通过出栈和入栈完成对数据的一次操作
如果被调用的方法有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新pc寄存器下一条指令
解释引擎是基于栈实现的,说的就是操作数栈
操作数栈中的数据类型必须与字节码指令中的数据类型严格匹配,由编译器在编译阶段进行验证,同时在类加载的类检验阶段的数据流分析阶段再次验证
栈顶缓存技术
将栈顶元素全部缓存在物理CPU的寄存器中,以降低对内存的读写次数,提升执行引擎的运行效率
动态连接
作用
将符号引用转换为调用方法的直接引用
指向运行时常量池的该栈帧所属方法的引用
方法的调用
链接
静态链接
字节码文件被装载进jvm中时,如果被调用的方法编译期可知,且运行期不变。这种情况下调用方法的符号引用转换为直接引用的过程称为静态链接
动态链接
如果被调用的方法编译期无法确定下来,只能在程序运行期将符号引用转换为直接引用。由于这种转换具有动态性,因此称之为动态链接
绑定
绑定是字段,方法或类的符号引用被替换为直接引用的过程,只发生一次
早期绑定
目标方法编译期可知,能确定下来,且运行期保持不变
晚期绑定
编译期无法确定下来,需要程序运行期根据实际的类型绑定相关的方法
方法
非虚方法
在编译期就能确定方法的调用版本,这个版本在运行期不可变,这个方法就是非虚方法
静态方法
私有方法
final方法
实例构造器
父类方法
虚方法
其它的就是虚方法
指令
普通指令
invokestatic
调用静态方法,解析是确定唯一版本
invokespecial
调用<init>方法,父类方法,解析阶段确定唯一版本
子主题
invokevirtual
invokeinterface
动态指令
invokedynamic
动态解析出需要调用的方法,然后执行
JDK7有的
重写的本质
找到操作数栈的第一个元素执行的对象的实例类型
如果找到的实例类型与常量的描述符,名称,访问权限校验,如果都符合,就返回这个方法的直接引用,结束
上面的不符合就找父类,以此类推
如果找不到,就抛出异常java.lang.AbstractMethodError
虚方法表
动态分派
影响程序执行效率
为了提高性能,JVM在类的方法区建了一个虚方法表,使用索引表代替查找
存放着各个方法的实际入口
虚方法表在类加载的链接的准备阶段创建并初始化,类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕
方法返回地址
作用
存放调用该方法的PC寄存器的值
方法正常退出时,调用者的的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址
通过异常退出的,返回地址通过异常表确定,栈帧中一般不会保存这些信息
本质
方法的退出就是栈帧出栈,此时需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器等,让调用者继续执行下去
正常完成出口与异常完成出口的区别在于:通过异常完成的出口退出不会给它的上层调用者产生任何返回值
返回指令
根据方法返回值类型而定
ireturn
boolean
short
int
char
byte
lreturn
long
freturn
dreturn
areturn
return
void
实例化初始化方法
类和接口的初始化方法
一些附加信息
与java虚拟机实现相关的
本地方法栈
本地方法接口
是什么
一个native method 就是一个java调用非java代码编写的接口
为什么用
与java环境外交互
与操作系统交互
做什么
管理本地方法的调用
当一个线程调用本地方法时,它将不再受虚拟机限制,和虚拟机具有同样的权限
堆
概述
存放对象实例和数组,最大的一块内存区域。
元空间的字符串常量池,静态变量也保存在堆中
JVM启动时就创建,大小也确定,大小可参数配置
可以是物理内存上不连续,逻辑上连续
所有线程共享,还有每个线程的私有缓冲区的TLAB
垃圾回收的重点区域
内部结构
年轻代
Eden
大部分新生对象都会分配在Eden区
80%的对象朝生夕死
Survivor
0
一个里有对象,一个没对象
1
垃圾收集后会把from区的对象复制到to区,保证一个Survivor区始终为空
复制之后交换,谁空谁是to
理论默认比例8:1:1
会根据自适应策略调整
会根据自适应策略调整
-XX:SurvivorRatio=8
对象经历过一定次数的回收后,会晋升老年代,默认15次
-XX:MaxTenuringThurshold=15
Yong GC
垃圾回收频繁发生在年轻代
老年代
养老区内存不足时,触发Major GC
默认比例1:2
-XX:NewRatio=2
可以通过参数-Xms,-Xmx设置堆的初始内存和最大内存
一般这两个会设置成一个,就不用在垃圾回收后重新计算堆的大小了,提高性能
GC分类
Partial GC
新生代收集
Minor GC/Young GC
年轻代空间不足时会触发Minor GC,Survivor满时不会触发
因为大部分对象朝生夕灭,所以Minor GC很频繁
Minor GC会触发STW,暂停其它用户线程
老年代收集
Major GC/Old GC
目前只有 CMS GC会单独收集老年代
很多时候Major GC会和Full GC混用
Major GC至少会伴随着一次Minor GC
Major GC速度比Minor GC慢10倍以上,STW时间更长
混合回收
Mixed GC
收集整个新生代和部分老年代
G1
Full GC
收集整个Java堆和方法区的垃圾收集
触发条件
System.gc()
老年代空间不足
方法区空间不足
通过Minor GC进入老年代的平均大小大于老年代的可用内存
对象需要的内存大于Eden区可用内存,大于To区可用内存,大于old区可用内存
开发和调优中尽量避免Full GC
为啥分代
不同的对象生命周期不同,混在一起垃圾收集不方便
内存分配策略
优先分配到Eden区
大对象直接分配到old区
G1有个H区
长期存活的对象分配到old区
如果Survivor区相同年龄的对象超过Survivor的一半,那这个年龄或大于这个年龄的对象可以直接晋升老年代
TLAB
为什么有
堆是线程共享的,所有线程都可以访问到
对象的实例创建在JVM里十分频繁,并发情况下在堆中划分内存空间是不安全的
为了避免多个线程操作同一个地址,需要加锁等机制进而影响分配速度
是什么
每个线程的私有缓存区域
在Eden区
多线程分配内存时,可以避免非线程安全问题;还可以提升内存的吞吐量
JVM内存分配的首选
只占Eden区的1%
-XX:UseTLAB
启用TLAB
-XX:TLABWasteTargetPercent
设置TLAB占的百分比
如果TLAB分配失败,JVM会尝试加锁机制确保操作的原子性,在Eden区直接分配内存
分配过程
除了堆
逃逸分析
栈上分配
同步省略
分离对象或标量替换
方法区
概述
方法区、栈、堆都存了什么
独立于java堆的内存空间
各个线程共享
JVM启动时被创建,可以是物理内存不连续的,逻辑上连续,可以选择固定大小和可扩展;关闭JVM释放内存空间
大小决定了能存放多少个类(信息)
元空间
JDK8
不再使用虚拟机设置的内存,使用本地内存
参数
-XX:MetaspaceSize
设置元空间初始内存大小
64位的服务端jvm默认21M。这个是初始的高水平线,如果满了,会触发Full GC卸载没用的类。然后这个高水平线将会被重置。新的高水平线取决于GC后释放了多少元空间,如果释放的空间不足,在不高于MaxMetaspaceSize的情况下,会适当提高该值。如果释放的过多,会适当降低该值
如果初始的高水平线设置的过低,Full GC也会发生多次,上调水平线的情况就回发生多次,为了避免频繁的Full GC,建议将-XX:MetaspaceSize设置为一个相对高的值
-XX;MaxMataspaceSize
怎么解决OOM
保存快照分析是内存泄漏还是内存溢出
内存泄漏就查看泄漏对象到GC Roots的引用链,定位泄漏代码的位置,解决
不是内存泄漏,检查堆参数,检查某些对象是否生命周期过长,持有状态时间是否过长
方法区存了什么
类型信息
类、接口、枚举、注解
完整的有效名称(包名+类名)
直接父类的完整有效名
类的修饰符
接口列表
运行时常量池
常量池
在从class中
数量值
字符串值
类引用
方法引用
常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等
用于存放编译期生成的各种字面量和符号引用
常量池中的内容在类加载后存放到方法区的运行时常量池
在加载类或接口到虚拟机时,就会创建运行时常量池
JVM为每一个加载了的类维护一个运行时常量池。池中数据像数组项一样,通过索引访问
运行时常量池包含多种不同的常量,包括编译期已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或字段引用。
此时不再是符号引用了,已经转换为真实引用了
此时不再是符号引用了,已经转换为真实引用了
运行时常量池相较于class常量池的另一特点是
具有动态性
String.intern()
静态变量
non-final的类变量
静态变量和类关联在一起,随着类的加载而加载,称为类数据在逻辑上的一部分
类变量被所有实例共享,即使没有创建实例也可以访问
static-final
全局常量
每个全局常量在编译的时候就被分配了
JIT代码缓存
域信息
所有域的相关信息及声明顺序
域名称
域类型
域修饰符
方法信息
方法名称
返回类型(或void)
方法参数的数量和类型(按顺序)
方法的修饰符
方法的字节码(bytecode)、操作数栈、局部变量表及大小
异常表
异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移位置,被捕获的异常类在常量池中的索引
方法区演进
JDK1.6
永久代,静态变量在永久代
JDK1.7
永久代,字符串常量池,静态变量放堆中
JDK1.8
元空间,类型信息,字段,常量,方法放在本地内存的元空间,字符串常量池,静态变量仍放在堆中
为什么用元空间替换永久代
1.为永久代设置大小很难确定,还可能OOM
元空间与永久代最大的区别在于:元空间不在虚拟机内,使用本地内存
2.对永久代条用困难
String Table为什么要调整
因为永久代的回收效率很低,在Full GC时才会触发。开发中大量字符串被创建,不及时回收,会导致内存不足,放在堆中,能及时回收
静态引用对应的实体对象始终放在堆空间中
只要是对象实例必然在堆中分配
方法区的垃圾收集
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
方法区常量池中主要存放两类常量:字面量和符号引用
字面量
文本字符串
被声明为final的常量值
符号引用
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
只要常量池中的常量没被任何地方引用,就可以被回收
判断一个类是否不再被使用
1.该类的所有实例都已被回收,即堆中不再有任何该类即任何派生子类的实例
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGI,JSP的重加载等,否则通常很难达成
3.该类的java.lang.Class对象没有被任何地方引用,无法在任何地方通过反射访问该类的方法
总结
运行时数据区
引用-实例-类
对象
对象实例化
创建对象的方式
new
clone
Class.getInstance()
Costructor的newInstance(XXX)
反序列化
第三方库
对象实例化过程
1.判断对象对应的类是否加载、链接、初始化
2.为对象分配内存
如果内存规整
指针碰撞
如果内存不规整
虚拟机需要维护一个列表
空间列表分配
3.处理并发安全问题
采用CAS失败就重试保证更新的原子性
每个线程预留一个TLAB
4.初始化分配到的空间
所有属性设置默认值,保证对象实例字段在不赋值时也可以直接使用
5.设置对象的对象头
5.执行init方法
子主题
对象的内存布局
对象头
包含两部分
运行时元数据(Mark Word)
哈希值(hash code)
GC分代年龄
锁状态标志
线程持有的锁
偏性线程ID
偏性时间戳
类型指针——指向类元数据InstanceKlass。确定该对象所属的类型
ps:如果是数字,还需记录数组的长度
实例数据
说明——它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和自己拥有的)
规则
相同宽度的字段会被分配在一起
父类中定义的变量在出现子类之前
如果CompactFields参数为true(默认为true)
对齐填充
不是必须的,也没特别含义,仅仅起占位符的作用
总结
子主题
对象的访问定位
JVM如何通过栈帧的对象引用访问到其内部的对象实例呢
定位,通过栈上的reference访问
对象访问方式
句柄访问
好处
GC之后实例的位置变了,只需要改变指针指向的位置,引用不用变
句柄访问
直接指针
好处
没有中间商赚差价,直接访问到实例数据
直接指针
直接内存和执行引擎
直接内存
概述
不是运行时数据区的一部分,也不会《java虚拟机规范》中定义的内存区域
直接内存是在对外,直接向系统申请的内存空间
来源于NIO
访问直接内存速度优于访问堆,读写性能高
大小不受jvm配置参数限制,但java堆和直接内存的总和依旧受限于系统给出的内存大小
缺点
分配回收成本高
不受JVM内存回收管理
执行引擎
概述
示意图
执行引擎是java虚拟机的核心组成之一
虚拟机的执行引擎是软件自行实现的,因此可以不受物理条件的制约定制指令集和执行引擎的结构体系,能够执行那些不被硬件直接支持
JVM的主要任务是负责装载字节码到其内部
执行引擎的任务就是将字节码指令解释/编译为对应平台上的机器指令
充当了将高级语言翻译成机器指令的角色
工作过程
1)执行引擎执行什么字节码完全依赖于程序计数器
2)每当执行完一条指令时,程序计数器就会更新下一条需要被执行指令的地址
3)方法在执行过程中,执行引擎有可能通过存储在局部变量表中的对象引用准确定位到在Java堆中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息
编译和执行过程
解释器
当java虚拟机启动时会根据预定义的规范,对字节码指令逐行解释执行,将字节码中的每条指令翻译成对应平台的机器指令执行
JIT(Just In Time Compiler)编译器
直接将源代码编译成和本地平台相关的机器语言
现在JVM执行java代码时,通常解释执行和编译执行二者结合使用
机器码、指令、汇编语言
机器码
各种二进制编码表示的指令
执行速度最快
与CPU相关,不同的CPU机器指令也不一样
指令
就是机器指令中的0和1简化成相应的指令,人好读一些
指令集
不同平台支持的指令集是有差别的,每个平台支持的对应的指令,称之为对应平台的指令集
X86指令集
ARM指令集
汇编语言
助记符代替机器指令的操作码,地址符号或标号代替指令或操作数的地址
高级语言
计算机执行高级语言编写的程序时,需要把程序解释或编译成机器指令。完成这个过程的程序就叫解释程序或编译程序。
图示
字节码
一种中间状态的二进制代码
主要为了实现特定软件运行和运行环境、硬件环境无关
字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以执行的机器指令
解释器
工作机制
逐条翻译成机器指令
当一条结束,会根据程序计数器中记录的下一条要执行指令的地址进行解释执行
分类
字节码解释器
模板解释器
现状
JVM平台支持即时编译
贡献不可磨灭
JIT编译器
概述
现在HotSpot JVM使用的解释器与编译器混合使用的架构
编译器需要时间
编译器
前端编译器
把.java文件转换为.class的过程
javac
后端编译器(JIT)
把字节码转换为机器指令的过程
HotSpot VM的C1,C2编译器
-client
java虚拟机运行在client模式下,使用C1编译器
对字节码简单、可靠的优化,耗时短。更快的编译速度
优化策略
方法内联
将引用的函数代码编译到引用点出,这样可以减少栈帧的生成,减少参数传递以及跳转过程
去虚拟化
对唯一的实现类进行内联
冗余消除
在运行期间把一些不会执行的代码折叠掉
-server
java虚拟机运行在server模式下,使用C2编译器
耗时较长的优化,以及激进的优化。优化的代码执行效率更高
优化策略
标量替换
用标量值代替聚合对象的属性值
栈上分配
对于未逃逸的对象分配对象在栈而不是堆
同步消除
清除同步操作,通常是synchronized
分层编译策略
程序解释执行,可以触发C1编译,将字节码编译为机器码,简单的优化,也可以加上性能监控,C2会根据性能监控信息进行激进的优化
Java7后,在-server模式下,默认开启分层编译策略,C1,C2相互协作共同完成
静态提前编译器
直接把.java文件编译成机器指令的过程
(AOT编译器 Ahead Of Time Compiler)
JDK9引入实验性AOT编译工具jaotc
好处
Java虚拟机加载已经编译成二进制库,可以直接执行。不必等待即时编译器的预热,减少Java应用给人带来“第一次运行慢”的不良体验
缺点
破坏了java“一次编译,到处运行”,必须为每个不同硬件、OS编译对应的发行包
降低了Java链接过程的动态性,加载的代码在编译期就必须已知
还需要持续优化,最初只支持Linux x64 java base
热点代码
概述
是否启用JIT编译器将字节码编译成机器指令,根据代码被执行的频率而定
需要被编译成本地代码的字节码,称为“热点代码”
JIT对这些代码做出深度优化,编译成本地机器指令,提高执行性能
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以称为“热点代码”
热点探测功能
基于计数器的热点探测()
方法调用计数器
统计方法的调用次数
client:1500
server:10000
-XX:ComplieThreshold
当一个方法被调用时,会先看是不是被编译成本地代码了,如果成了,直接调用;如果没成,方法调用计数器+1,再看方法调用计数器与回边技计数器之和是不是超过了方法调用计数器的阈值,达到了,就编译成本地代码
示意图
热度衰减
在一定的限度时间内,如果方法的调用次数不足以让它被即时编译器编译,那这个方法调用计数器会减少一半
-XX:UserCounterDecay
设置使用热度衰减
-XX:CounterHalfLifeTime
设置衰减时间限制
回边计数器
统计循环体执行的循环次数
在代码中遇到控制流向后跳转的指令称为“回边”
建立回边计数器统计的目的是为了触发OSR编译
调用多少次
HotSpot VM设置执行方式
-Xint
完全使用解释器模式执行程序
-Xcomp
完全采用即时编译器执行程序。如果编译器出问题,解释器会介入执行
-Xmixed
采用解释器+即时编译器的混合模式执行程序
总结
一般来说,JIT编译出来的性能比解释器的高
C2启动时比C1慢,但编译完成后,C2编译器的执行速度远快于C1
String Table
基本特性
实现
字符串,用“”引起来表示
声明为final的,不可被继承
实现了Sreializable接口,表示字符串是支持序列化的
实现了Comparable接口,可以比较大小
String在jdk8以前内部定义了final char[ ] value用于存储字符串数据,jdk9改为byte[ ]加上编码标记
节约了一些空间
不可变性
当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
对现有的字符串进行拼接操作时,也需要重写指定内存区域赋值,不能使用原有的value进行赋值
当调用String的replace()修改指定字符或字符串时,也需要重写指定内存区域赋值,不能使用原有的value进行赋值
字符串常量池
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串的声明在字符串常量池中
字符串常量池中不会存放相同内容的字符串
String的String Pool是一个固定大小的HashTable
如果放进String Pool的String非常多,就会导致hash冲突,链表长,链表长了就导致调用string.intern()时很慢
可以使用-XX:StringtTableSize设置StringTable的长度
JDK6中StringTable的长度是固定的,默认大小长度是1009,StringTableSize设置没有要求
JDK7中,StringTable的长度是60013,StringTableSize设置没有要求
JDK8开始,设置StringTable的长度的话,1009是可设置的最小值
字符串拼接
常量与常量拼接,保存在字符串常量池中。原理是编译器优化
常量池中不会存在相同内容
只要其中一个是变量,结果就在堆中。原理同StringBuilder
如果拼接的结果调用intern()方法,就会检查在字符串常量池中有没有这个串儿对象,没有的话加里面,再返回串儿的地址
内存分配
常量池类似于一个java系统级别提供的缓存
8种基本数据类型和string都有。运行过程中速度更快、更节省内存
8种基本数据类型的常量池都是系统协调的
String类型的常量池比较特殊。主要使用两种方法
直接使用双引号“”声明出来的对象会直接存储在字符串常量池中
如果不是双引号声明的String对象,可以使用String提供的intern()方法
内存位置
JDK6及以前,保存在永久代
JDK7将字符串常量池的位置调整到了堆中
所有字符串都保存在堆中,和其它普通对象一样,这样在进行调优应用时仅需调整堆大小就可以了
可以使用string的intern()方法
JDK8元空间,字符串常量池依旧在堆中
intern()
如果不是双引号声明的string对象,可以使用String提供的intern()方法,如果已经存在于字符串常量池,就返回string对象在字符串常量池中的地址,如果没有就将当前字符串放入常量池中。
也就是说,如果在任意字符串上调用intern()方法,那么返回结果所指向的那个实例,必须和直接以常量形式出现的字符串实例完全相同
通俗点将,就是确保字符串在内存中只有一份拷贝,可以节约内存空间,加快字符串操作任务的执行速度。这个值会被存放在字符串内部池(String Intern Pool)
版本区别
JDK1.6
将字符串尝试放入字符串常量池中
如果常量池中已经有了,则并不会放入,直接返回串池中的地址
如果没有,会将此对象复制一份,放入串池,并返回串池中的地址
JDK1.7起
将字符串尝试放入字符串常量池中
如果常量池中已经有了,则并不会放入,直接返回串池中的地址题
如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
垃圾回收
G1的String去重操作
背景
堆存活数据集合中String对象占了25%
堆存活数据集合中重复的String对象有了13.5%
String平均长度45
堆上存在重复的Strig对象是对内存的一种浪费
实现
当垃圾收集器工作时,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象
如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象
使用一个hashtable记录所有的被String对象使用的不重复的char数组。当去重的时候,会去查hashtable,看堆上是否已经存在一个一模一样的char数组
如果存在,String对象会被调整引用那个数组,释放对原来数组的引用,最终会被垃圾收集器回收掉
如果查找失败,char数组会被插入hashtable,这样以后可以共享这个数组
命令
UseStringDeduplication
默认是不开启的
PrintStringDeduplicationStatistics
打印详细的去重统计信息
StringDeduplicationThreshold
达到这个年龄的String对象被认为是去重的候选对象
垃圾回收
什么是垃圾
运行程序中没有任何指针指向的对象。这个对象就是需要被回收的垃圾
为什么需要回收
不回收,内存迟早被用没
不GC不能保证程序的正常运行
Java垃圾回收机制
Java堆是垃圾收集的重点回收区域
频率
频繁收集Young区
较少收集Old区
基本不动Metaspace
垃圾回收算法
标记阶段
判断对象是否存活
当一个对象不再被任何存活对象引用时
引用计数算法
原理
每个对象保存一个整型的引用计数器。记录被引用的次数
只要这个对象被引用了,计数器+1;当引用失效,计数器-1
当引用计数器为0时,表示这个对象没有再被引用,可以回收
优点
易于实现,垃圾对象便于识别;判定效率高,回收没有延时性
缺点
需要单独的字段存储计数器。增加了空间的开销
更新计数器时,要计算+-法。增加了时间的开销
无法处理循环引用
java没用这个算法
可达性分析算法
原理
GC Roots根集合:一组活跃的引用
可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所链接的目标对象是否可达
使用可达性分析算法后,内存中存活的对象都会被根对象集合直接或间接的引用,搜素所走过的路径被称为引用链(Reference Chain)
如果目标对象没有任何引用链连接,则是不可达的,就意味着该对象已经死亡,可以被标记为垃圾对象
在可达性分析中,只有能被根对象集合直接或间接连接的对象才是存活对象
GC Roots
虚拟机栈中引用的对象
比如各个线程中方法中使用到的参数、局部变量等
本地方法栈中JNI(本地方法)引用的对象
方法区中类静态属性引用的对象
比如Java类的静态引用变量
方法区中常量引用的对象
比如字符串常量池里的引用
所有被synchronized锁持有的对象
Java虚拟机内部的引用
基本数据类型对应的Class引用对象,一些常驻的异常对象,系统类加载器
反应java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
如果一个指针保存了堆里面的对象,它自己又不在堆中,它就是一个Root
使用可达性分析算法分析工作必须在一个能保证一致性的快照中进行
所有GC进行是必须Stop the world
对象的finalization机制
Java提供了对象终止机制来允许开发人员提供对象销毁之前自定义逻辑处理
当垃圾回收期发现没有引用指向一个对象,垃圾回收此对象之前,会先调用这个对象的finalize()方法
finalize()方法允许在子类中被重写,用于在对象回收时进行资源释放
通常是资源释放和清理的工作
比如关闭文件、套接字和数据库连接等
不要主动调用finalize()方法
可能会导致对象复活
可能还会影响性能
不发生GC,就没有finalize()的机会
由于finalize的存在,虚拟机中的对象一般可能存在三种状态。
一个无法触及的对象可能在某一个条件下“复活”自己
一个无法触及的对象可能在某一个条件下“复活”自己
可触及的
从根节点开始,可以到达这个对象
可复活的
对象的所有引用都被释放,但是对象有可能在finalize()中复活
不可触及的
对象的finalize被调用,并且没被复活,那就进入不可触及的状态。
不可触及的对象不可能被复活,因为finalize()只能被调用一次
这个状态下的对象才会被回收
判断一个对象obj是否可回收,至少要经历两次标记状态
1.如果对象obj到GC Roots没有引用链,则进行第一次标记
2.进行筛选,看对象是否需要执行finalize()方法
①.如果obj没有重写finalize()方法,或者已经被调用过了,状态不可触及
②.如果重新了finalize,且还未被执行过,obj会被插入F-Queue队列。由一个虚拟机自动创建的,优先级较低的Finalizer线程触发其finalize()方法执行
③.finalize()方法是对象逃脱被回收的最后机会。GC对F-Queue中的对象进行第二次标记,如果finalize()方法中obj与引用链上任何一个对象建立了连接,obj就会被移除“即将回收”的集合。如果下次再次出现没有引用的情况,不会再触发finalize()方法,对象直接变成不可触及状态。一个对象的finalize()方法只会被调用一次
清除阶段
标记-清除算法
执行过程
当堆中的有效内存被耗尽,就回停止程序,然后进行两项操作
1.标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象
2.清除:Collector对堆内存从头到尾进行遍历,如果发现某个对象在其Header中没有标记为可达对象,则对其进行回收
缺点
效率不算高
在进行GC的时候,要停止整个程序,用户体验较差
这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
何为清除
这里所谓的清除并不是真的置空,而是吧需要清除的对象地址保存在空闲列表里。下次如果有新对象需要加载时,就看垃圾的位置空间是否足够,如果够,就存放
复制算法
核心思想
将活着的内存分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活的对象复制到另一块空的内存空间中,之后清除正在使用内存块中的所有对象,交换两个内存块的角色,最后完成垃圾回收。
优点
没有标记和清除过程,实现简单,运行高效
复制过去保证内存连续性,不会出现“碎片”问题
缺点
需要两倍的内存空间
对于G1这种拆分成大量region的GC,复制而不是移动,意味着GC需要维护region之间的引用关系,不光是内存占用吗,时间开销也不小
特别的
适合存活对象很少,需要回收的对象很多的情景
Young区的Survivor 0区和Survivor 1区
应用场景
回收新生代
标记-压缩(整理)算法
也可以称为标记-清除-压缩算法
背景
老年代大部分都是存活对象,复制算法不合适
基于老年代垃圾回收的特性,需要使用其他算法
原理
标记的存活对象被整理,按照内存依次排列,未被标记的内存将被清理掉。如此,当需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了很多开销
指针碰撞
如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维护着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要修改指针的偏移量将对象分配在第一个空闲位置上,这种分配方式叫做指针碰撞
优点
消除了标记-清除算法中,内存区域分散的缺点,需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可
消除了复制算法中内存空间减半的高额代价
缺点
效率上,低于复制算法
移动对象的同时,如果对象被引用还需要调整引用的地址
移动过程中,需要全程暂停所有程序,即STW
小结
复制算法效率最高,最快,需要移动对象,浪费空间
标记-整理算法最慢,移动对象,空间占用少,且不分散无碎片
标记-清除算法速度中等,不需要移动对象,有碎片。空间占用中等
没有最好的算法,只有合适的算法
分代收集算法
不同生命周期的对象可以采用不同的回收方式,以提高效率
例如Http请求中的session对象、线程、Socket连接,生命周期长
临时变量生命周期短,如String类型
目前几乎所有的GC都采用的分代收集算法执行的垃圾回收的
年轻代
特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁
这种情况下复制算法的回收整理,速度是最快的
复制算法的效率只和当前存活对象大小有关,因此适用于年轻代的回收
复制算法内存使用率不高的问题,通过hotspot中两个Survivor的设计得到缓解
老年代
特点:区域较大,生命周期长、存活率高,回收不及年轻代频繁
这种情况下存在大量存活率较高的对象,复制算法不合适
一般由标记-清除和标记-整理混合实现
Mark阶段的开销与存活对象数量成正比
Sweep阶段的开销与所管理区域的大小有关
Compact阶段的开销与存活对象的数据成正比
增量收集与分区收集
增量收集算法
背景
STW时,所有线程都会挂起,暂停一切正常的工作,等待垃圾回收完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验和系统稳定性。为了解决这个问题,即对实时垃圾收集算法的研究 促使增量收集算法诞生
基本思想
如果一次性将所有垃圾收集,系统停顿时间较长,可以让垃圾收集线程和应用程序线程交替执行
每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成
总的来说,增量收集的算法基础仍是传统的标记-清除算法和复制算法
增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制的工作
确定
使用这种方式,由于在垃圾回收过程中,间断性的还执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
分区算法
概述
一般来说,在相同条件下,堆空间越大,一次GC时需要的时间就越长,有关GC产生的停顿就越长。为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿
分带算法将按照对象的生命周期长短划分为两个部分,分区算法将整个堆空间划分成连续的不同小区间
每个小区间都能独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间
垃圾回收相关概念
System.gc()
默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC
System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用
一般情况下,垃圾回收应该是自动进行的,无序手动触发
内存泄漏与内存溢出
内存溢出
没有空闲内存,并且垃圾收集器也无法提供更多的内存
原因可能是
1)Java虚拟机的堆内存设置不够
2)代码中创建了大量达对象,并且长时间不能被垃圾收集器收集(存在被引用)
在OOM之前,通常垃圾收集器会被触发,尽其所能的清理空间
如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等
在java.nio.reserveMemory()方法中,System.gc()会被调用。以清理空间
内存泄漏
严格来说,只有对象不会再被程序用到了,但GC又不能回收它们的情况才叫内存泄漏
举例
1.单例模式
单例的生命周期和应用程序一样长,所以在单例程序中,如果持有外部对象的引用的话,那么这个外部对象是不可能被回收的,会导致内存泄漏的产生
2.一些提供close的资源未关闭导致内存泄漏
数据库连接,网络连接和io连接必须手动close,否则是不能被回收的
Stop The World
GC事件发生过程中,会产生应用程序的停顿。停顿产生是整个应用程序都会被暂停吗,没有任何响应,优点像卡死的感觉,这个停顿叫STW
可达性分析算法中枚举根节点会导致所有Java执行线程停顿
分析工作必须在一个能确保一致性的快照中进行
一致性指整个分析期间整个运行系统看起来像被冻结在某个时间点上
如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
被STW中断的应用程序线程会在完成GC后恢复,频繁中断会让用户感觉像网速不快造成电影卡带一样,所以应减少STW的发生
STW与使用哪个GC无关,所有GC都会发生
STW是JVM在后台自动发起和自动完成的
开发中不要用System.gc(),会导致STW的发生
垃圾回收的并行与并发
并发
在操作系统中,指一段时间内几个程序处于启动和运行完成之间,且这几个程序是在同一个处理器上运行的
并不是真证意义上的同时进行,而是CPU在一段时间内切分成多个时间区间,然后在这几个时间区间之间切换,由于CPU处理的速度很快,只要切换的合理得当,用户就会感觉这几个程序在同时运行
并行
当系统有一个CPU以上时,一个CPU执行一个进程,另一个CPU执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,成为并行
决定并行的不是CPU的数量,而是CPU的核心数量,一个CPU多个核心,也可以并行
对比
并发:多个进程在一段时间内进行
多个任务之间互相抢占资源
并行:多个线程在一个时间点进行
多个任务之间不互相抢占资源
只有在多个CPU或一个CPU多核的情况下才会发生并行
垃圾回收的并行与并发
并行:多个垃圾收集线程同时工作,此时用户线程仍处于等待状态
串行:相较于并行的概念,单线程执行;如果内存不够,则程序暂停,启动垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
并发:指垃圾收集的线程与用户线程同时执行(但不一定是并行的,也可能是交替执行),垃圾回收线程执行时,不会停顿用户线程
用户程序在继续执行,而垃圾收集程序在运行与另一个CPU上
如CMS,G1
安全点与安全区域
安全点
程序并非在所有地方都能停下来进行GC,只有在合适的地方才能停顿下来,进行GC,这些合适的地方成为安全点
安全点的选择很重要,太少可能会垃圾过多,用户等待时间较长,太多会停顿过于频繁,影响程序运行性能
通常会根据“是否具有让程序长时间执行的特征”为标准,选择一些让程序执行时间较长的指令作为安全点,比如方法调用、循环跳转和异常跳转等
程序怎么让自己都跑到安全点呢
抢断式中断:首先中断所有线程,如果有线程没在安全点,就恢复线程,让线程跑到安全点(没再用了)
主动式中断:设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果这个标志为真,则将自己中断挂起
安全区域
指在一代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。可以看做被扩展的安全点
1.当线程运行到safe region的代码时,首先标识已经进入了safe region,如果这段时间内发生GC,JVM会忽略标识为safe region状态的线程
2.当线程即将离开safe region时,会检查JVM是否已完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开safe region的信号为止
引用
强引用
大部分都是强引用
new 对象,将其赋给一个变量,这个变量就成为了指向这个对象的强引用
强引用的对象是可触及的,垃圾收集器就永远不会回收强引用的对象
强引用是造成Java内存泄漏的主要原因之一
不回收
强引用可以直接访问目标对象
强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象
强引用可能导致内存泄漏
软引用
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收
通常用来实现内存敏感的缓存
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用放进一个引用队列
弱引用
只要弱引用关联的对象,只能活到下一次垃圾收集发生
软引用和弱引用非常适合保存那些可有可无的缓存数据
弱引用对象与软引用对象最大不同在于,当GC进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用更容易、更快被GC回收
虚引用
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象呗垃圾收集器回收时收到一个系统通知
由于虚引用可以跟踪对象的回收时间,因此,可以将一些资源释放操作放在虚引用中执行和记录
终结器引用
它用以实现对象的finalize()方法
无需手动编码,其内部配合引用队列使用
在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象
垃圾回收器
GC分类与性能指标
按线程数分
串行垃圾回收器
一段时间内只允许一个CPU进行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束
在单CPU处理器或较小的应用内存等硬件条件差的平台,串行回收器的性能是优于并行回收器和并发回收器的
串行回收被默认应用在客户端的Client模式下的JVM中
在并发能力较强的CPU上,并行回收器产生的停顿时间要优于串行回收器
Serial、Serial Old
并行垃圾回收器
并行回收可以多个CPU同时进行垃圾回收,提升了吞吐量,但仍是独占式的,使用STW机制
ParNew、Parallel Scavenge、Parallel Old
按工作模式分
并发式垃圾回收器
并发式垃圾回收器与用户线程交替执行,尽可能的减少用户程序停顿时间
CMS、G1
独占式垃圾回收器
独占式垃圾收集器一旦运行,就停止所用的用户线程,知道垃圾收集工作完全结束
按碎片处理方式分
压缩式垃圾收集器
压缩式垃圾收集器会在回收工作完成后,对存活对象进行压缩整理,消除回收后的碎片
非压缩式垃圾收集器
按工作的内存空间分
年轻代收集器
Serial
ParNew
Parallel Scavenge
老年代收集器
Serial Old
Parallel Old
CMS
评估GC的性能指标
吞吐量
运行用户代码的时间占总运行时间的比例
停顿时间
执行垃圾收集时,程序的工作线程被暂停的时间
占用内存
java堆区所占的内存大小
现在的标准:在最大吞吐量优先的情况下,降低停顿时间
不同的垃圾回收器
Serial GC
ParNew
Parallel
CMS
G1
ZGC
组合
图
说明
参数指令
-XX:PrintCommonLineFlags:查看相关参数(包括使用的垃圾收集器)
使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程id
Serial回收器:串行回收
最悠久,最基础
采用复制算法,串行回收,需要Stop The World机制方式回收内存
除此之外,Serial收集器还提供了用于老年代垃圾收集的Serial Old垃圾收集器。
Serial Old收集器同样使用串行回收和“Stop The World”机制,不过内存回收算法采用的标记-压缩算法
Serial Old收集器同样使用串行回收和“Stop The World”机制,不过内存回收算法采用的标记-压缩算法
Serial Old是运行在Client模式下默认的老年代的垃圾回收器
Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用 ② 作为老年代CMS收集器的后背垃圾收集方法
这是一个单线程收集器。但它的单线程不仅仅说明它只会使用一个CPU或一条收集进程完成垃圾收集工作,更重要的是,必须暂停其它所有的工作进程,直到它收集结束(Stop The World)
优势
简单高效
对于限定的单CPU来说,Serial收集器由于没有线程切换的开销,专心做垃圾收集可以获得单线程最高效率的收集
-XX:UseSerialGC
总结:现在都不是单核了,都不用了
ParNew回收器:并行回收
并行回收、复制算法、采用“Stop The World”机制
很多JVM虚拟机新生代的默认垃圾收集器
图
目前ParNew GC只和CMS回收器配合使用
-XX:UseParNewGC
-XX:ParallelGCThreads---限制线程数量,默认开启和CPU相同的数量
Parallel回收器:吞吐量优先
并行回收、复制算法、采用“Stop The World”机制
与ParNew不同,Parallel Scavenge收集器目标是达到一个可控制的吞吐量,也被称为吞吐量优先的垃圾收集器
自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别
高吞吐量可以高效率的利用CPU时间,尽快的完成程序的运算任务
主要适合用在后台计算而不需要与用户太多交互的任务
主要适合用在后台计算而不需要与用户太多交互的任务
Parallel收集器在jdk1.6提供了用于老年代垃圾收集的Parallel Old收集器
标记-压缩算法、并行回收、“Stop The World”机制
标记-压缩算法、并行回收、“Stop The World”机制
垃圾收集图示
在吞吐量优先的应用场景,Parallel和Parallel Old收集器的配合,在Server模式下性能还是不错的
Java8中,默认是此收集器
参数配置:
-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务
-XX:+UseParallelOldGC 手动指定老年代
这两个参数,默认开启一个,另一个也会被开启
-XX:ParallelGCThreads 设置年轻代并行收集器的线程数
默认等于CPU数量
当CPU大于8个,ParallelGCThread的值等于3+[5*cpu数量]/8
-XX:MaxGCPauseMills 设置垃圾收集最大停顿时间(即STW的时间)
-XX:GCTimeRatio 垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小
-XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点
手动调优比较困难的场合,可直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标吞吐量和停顿时间,让虚拟机自己完成调优工作
CMS回收器:低延迟
Hotspot中第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程同时工作
尽可能缩短垃圾收集时用户线程停顿的时间
标记-清除算法、也会“Stop The World”
老年代收集器,搭配ParNew使用,Jdk14被废弃使用
工作流程
图
初始标记:Stop The World,仅标记GC ROOTS能直接关联的对象,速度非常快。
并发标记:从GC Roots直接关联的对象开始遍历整个对象图的过程,这个过程耗时很长但不需要停顿用户线程,可以与垃圾收集线程并发运行
重新标记:修正并发标记期间,因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长,但比并发标记时间短。STW
并发清除:清理删除掉标记阶段已经标记位死亡的对象,释放内存空间。因为不需要移动存活对象,这个过程是和可以用户程序线程并发的
须知
由于最耗时的并发标记和并发清除都不需要停止用户线程,所以整体的回收时低停顿的
由于垃圾收集阶段用户程序并没有中断,所以在CMS回收过程中,还需要保证应用程序用户线程有足够的内存可用,当内存使用率到达某一阈值时,就开始回收
如果CMS回收过程中预留内存无法满足程序需要,就会出现“Concurrent Mode Failure”,临时启用Serial Old收集器进行老年代的垃圾回收
因为用的标记-清除算法,有碎片内存,就不能使用指针碰撞,只能使用空闲列表为新对象分配内存
为什么不用标记-压缩算法?因为并发进行的,如果整理内存的话,用户线程就没法用了
优点
低延迟
并发收集
缺点
会产生内存碎片
对CPU资源敏感
并发阶段虽然不会使用户线程停顿,但是会占用一部分线程导致程序变慢,总吞吐量降低
CMS无法处理浮动垃圾
在并发阶段如果产生新的垃圾对象,CMS将无法对这些对象进行标记,最终导致这些新产生的垃圾没有被即使回收
参数
-XX:+UseConcMarkSweepGC
启用CMS收集,设置这个参数后会自动将-XX:+UseParNewGC 打开
-XX:CMSlnitiatingOccupanyFraction
设置堆内存使用率的阈值
如果内存增长缓慢,可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显的改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器
通过该选项可有效降低Full GC的执行次数
-XX:+UseCMSCompactAtFullCollection
用于指定FUll GC后对内存空间进行压缩整理
-XX:CMSFullGCsBeforeCompaction
设置在执行多少次Full GC后对内存空间进行压缩整理
-XX:ParallelCMSThreads
设置CMS线程数量
CMS默认启动的线程数是(ParallelGCThreads+3)/4
小结
最小化的使用内存和并行开销,用Serial GC
最大化应用程序的吞吐量,用Parallel GC
最小化GC的中断和停顿时间,用CMS GC
新的
JDK9中CMS被标记位废弃的
JDK14总删除CMS垃圾收集器
G1回收器:区域化分代式
概述
目标是在延迟可控的情况下尽可能的获得高吞吐量
每次根据允许的时间,优先回收价值最大的Region。垃圾优先(Garbage First)
主要针对配备多核及大容量内存的机器
jdk9以后的默认垃圾收集器,全功能收集器
优势
并行与并发
并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力,此时STW
并发性:G1拥有与用户程序交替执行的能力,部分工作可以与应用程序同时进行
分代收集
从分代上看,G1依旧属于分代型垃圾收集器
将堆空间分为若干个区域,这些区域包含了逻辑上的年轻代与老年代
同时兼顾年轻代与老年代
空间整合
G1将内存划分为一个个的Region。内存的回收以Region为单位。Region之间是复制算法,但整体上可以看做使用的标记-压缩算法,可以避免内存碎片,利于程序长时间运行
可预测的时间停顿模型
每次根据允许的停顿收集时间,优先收集 价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集率
缺点
G1为了垃圾收集而产生的内存占用和程序运行时的额外执行负载都比CMS高
与CMS的平衡点在6~8GB
参数
-XX:+UseG1GC
-XX:G1HeapRegionSize
设置每个Region的大小
-XX:MaxGCPaiuseMills
设置期望达到的最大GC停顿时间指标(不保证能打达到)。默认200ms
-XX:ParallelGCThread
设置STW工作线程数。最多为8
-XX:ConcGCThreads
设置并发标记的线程数。
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的Java堆占用阈值。超过此值,就触发GC。默认45
调优
第一步:设置使用G1收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
适用场景
面向服务端场景,针对具有大内存、多处理器的机器
最主要的应用是需要低延迟,并具有大堆的应用程序提供解决方案
用来替换jdk1.5的CMS收集器
分区Region
所有的Region大小相同,且在JVM生命周期内不会被改变
一个Region只可能属于一个角色
G1还新增了一个Humongous区,用于存放大对象
如果是个短期存在的大对象,分配在老年代不合适
如果一个H区装不下一个大对象,G1会寻找连续的H区存放这个大对象
使用指针碰撞
回收过程
年轻代GC
当年轻代的Eden区内存用尽时开始年轻代的回收。G1的年轻代收集时一个并行的独占式收集器,启动多线程执行回收,然后把存活的对象移到Survivor区或Old区
第一阶段,扫描根
根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口
第二阶段,更新RSet
处理dirty card queue中的card,更新RSet。此阶段完成后,Reset可以准确的反映老年代所在的内存分段中对象的引用
第三阶段,处理RSet
识别老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
第四阶段,复制对象
此阶段,对象树被遍历,Eden区内存中存活的对象呗复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达到阈值,年龄+1,达到阈值就被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间
第五阶段,处理引用
处理Soft,Weak,Phantom,Final,JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片
老年代并发标记过程
当堆内存使用到达一定值(默认45%)时,开始老年代并发标记过程
1.初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,且会触发一次年轻代GC
2.根区域扫描:G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成
3.并发标记:在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,在并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
4.再次标记:由于应用程序持续进行,需要修正上一次的标记结构。是STW的。G1采用比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)
5.独占清理:计算各个区域存活对象和GC回收的比例,并进行排序,识别可以混合回收的区域。是STW的。这个阶段是为下阶段做铺垫,并不会实际上做垃圾收集
6.并发清理阶段:识别并清理完全空闲的区域
混合回收
标记完成后,马上开始混合回收过程。在一个混合回收期,从老年区把存活的对象移到空闲区间,这些空闲区间就成了老年代的一部分。G1的老年代不需要整个老年代被回收,一次只需要回收一部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起回收的
并发标记后,老年代中百分百为垃圾的百回收了。部分为垃圾的内存分段被计算出来了。默认分为8次回收
混合回收的回收集包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。
混合回收的算法和年轻代完全一样
由于老年代中是分8次回收的,垃圾栈内存分段的比例越高,越会被先回收
混合回收不一定要分8次。有一个阈值-XX:G1HeapWastePercent,默认值是10%,意思是允许整个堆中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。避免花费大量时间回收道德内存却很少
可选过程:Full GC
G1的设计初衷就是避免Full GC,如果上述方式不能正常工作,G1会停止应用程序执行(STW),使用单线程的内存回收算法进行垃圾回收,性能极差,停顿时间还长
发生Full GC的原因可能是
1.Ecacuation的时候没有足够的to-space来存放晋升的对象
2.并发处理过程之前空间耗尽
堆内存太小,当G1在复制存活对象的时候没有空的内存分段可以,则会退回到Full GC,这种情况可以通过增大内存解决
示意图
Remembered Set
一个对象可能被不同的区域引用的问题
无论G1还是其它分代收集器,JVM都使用Remembered Set来避免全局扫描
每一个Region都有一个对应的Remembered Set
每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作
然后检查将要写入的引用指向的对象是否和该Reference类型在不同的Region(其它收集器检查老年代对象是否引用了新生代对象)
如果不同,通过CardTable把相关引用信息记录到引用指向对象所在Region对应的Remembered Set中
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏
优化建议
年轻代大小
避免使用-XX:Smn或-XX:NewRatio参数设置年轻代的大小
固定年轻代大小会覆盖目标暂停时间
暂停目标时间不要太苛刻
G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
评估G1 GC的吞吐量是,暂停目标不要太苛刻。目标太过苛刻,会直接影响吞吐量
垃圾收集器总结
怎么选择垃圾收集器
1.优先调整堆的大小让JVM自适应
2.如果内存小于100m使用串行处理器
3.如果是单机,单核程序,并且没有停顿时间要求,使用串行处理器
4.如果是多核CPU,需要高吞吐量,允许停顿时间超过1秒,选择并行或JVM自己选择
5.如果是多CPU,追求低停顿时间,需快速响应,使用并发收集器。官方推荐G1,性能高。现在的互联网项目,基本都是使用G1
GC日志分析
参数
-XX:+PrintGc 输出GC日志
-verbose:gc 输出gc日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式)
-XX:PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XXloggs: ../logs/gc.log 日志文件的输出路径
补充
Allocation Failure
表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了
垃圾收集器新发展
Shenondoah GC
概述
旨在针对JVM上的内存回收实现低停顿的需求
团队对外宣传:Shenondoah垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为200MB还是200GB,99.9%的目标都可以把垃圾收集的停顿时间限制在十毫秒以内
强项
低延迟
弱项
高运行下的吞吐量下降
ZGC
概述
在尽可能堆吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器
工作过程分为4个阶段:并发标记——并发预备重分配——并发重分配——并发重映射等
未来将在服务端、大内存、低延迟应用的首选垃圾收集器
使用参数:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
0 条评论
下一页