JVM知识点总结
2021-07-14 18:24:03 0 举报
AI智能生成
java虚拟机相关知识脑图
作者其他创作
大纲/内容
类加载机制
类生命周期
类加载顺序
加载、验证、准备、初始化、使用、卸载的开始顺序(前一个阶段可能未结束后一个阶段就开始了)
是固定的而解析阶段为了动态绑定可以在某些情况下在初始化阶段后执行
是固定的而解析阶段为了动态绑定可以在某些情况下在初始化阶段后执行
类加载时机
类加载的第一个阶段加载阶段的开始时机并不固定,而初始化阶段是固定的,因此可以用初始化阶段开始的时机来替代类加载时机
类初始化时机
主动引用--触发类初始化
new、getstatic、putstatic、invokestatic
反射调用
初始化子类时触发父类初始化
指定运行时主类(带有main方法的类)
java.lang.invoke.MethodHandle实例最后解析结果
REF_getStatic、REF_putStatic、REF_invokeStatic
对应的类如果没有进行初始化则会进行初始化
REF_getStatic、REF_putStatic、REF_invokeStatic
对应的类如果没有进行初始化则会进行初始化
被动引用--不会触发类初始化
子类调用父类静态变量
数组形式引用类
调用类中常量
类加载过程
加载
通过类的全限定名来获取该类的二进制字节流
非数组类
可以由引导类加载器来获取
可以通过自定义类加载器来控制获取方式
数组类
引用数组类
递归采用类加载过程去加载数组的组件类型(数组去掉一个维度的类型),
数组会被标记与组件类型的类加载器相关联,可见性与它的组件类型的可见性一致
数组会被标记与组件类型的类加载器相关联,可见性与它的组件类型的可见性一致
非引用数组类
数组会被标记与引导类加载器相关联,可见性默认为public
将字节流所代表的静态存储结构转化为方法区中的运行时数据结构(运行时数据结构由虚拟机自行定义)
在内存中实例化一个java.lang.Class对象(HotSpot中这个Class对象存储在方法区中而不是堆中)作为方法区中该类的各种数据访问入口
连接
验证
文件格式验证
作用
验证字节流是否符合Class文件格式的规范
以及是否能被当前版本的虚拟机处理
以及是否能被当前版本的虚拟机处理
常见验证点
是否以魔数0xCAFEBABE开头
主、次版本号是否在当前虚拟机处理范围内
常量池中的常量是否有不被支持的常量类型
等等
结果
通过该阶段验证后,字节流会进入内存的方法区中进行存储,
后续的验证过程都是针对方法区的存储结构进行的
而不会再直接操作字节流
后续的验证过程都是针对方法区的存储结构进行的
而不会再直接操作字节流
元数据验证
作用
对字节码描述的信息进行语义分析用来保证其描述的信息符合Java语言规范
常见验证点
该类是否有父类
该类的父类是否继承了final修改的类
该类如果不是抽象类是否实现了父类或接口中的需要实现的方法
等等
结果
保证不存在不符合Java语言规范的元数据信息
字节码验证
作用
通过数据流和控制流分析保证程序语义合法、合逻辑
常见验证点
验证跳转指令是否会跳转到方法体之外的字节码指令
验证方法体中的类型转换是否有效
等等
结果
通过了字节码验证的方法体也不能说是一定安全的
符号引用验证
作用
对类自身以外的信息进行匹配性验证
常见验证点
符号引用中通过全限定名能否找到对应的类
类中是否存在符合方法的字段描述符和简单名称描述的方法和字段
符号引用中的类、字段、方法的访问性能否允许当前类进行访问
等等
结果
确保后续的解析阶段能正常执行
-Xverify:none--用来关闭大部分类验证措施
准备
作用
为类变量在方法区中分配内存并赋初始值(零值)
重点
类变量特指类中的静态变量,实例变量是在类实例化的时候随着对象一起分配到Java堆中
初始值通常情况下指的是对应数据类型的零值,而不是定义的值。而如果是常量,则初始值指的是指定的值。
各数据类型的零值
解析
知识点
符号引用
是以一组符号来描述所引用的目标,符号可以是任何形式的字面量。符号引用和虚拟机实现的内存布局无关,
引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以不一样但是它们能接受的符号引用必须是一致的。
引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以不一样但是它们能接受的符号引用必须是一致的。
直接引用
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
直接引用和虚拟机实现的内存布局有关,引用目标一定已经在内存中存在,同一符号引用在不同虚拟机中翻译出的直接引用一般不同。
直接引用和虚拟机实现的内存布局有关,引用目标一定已经在内存中存在,同一符号引用在不同虚拟机中翻译出的直接引用一般不同。
作用
将常量池中的符号引用替换为直接引用
作用目标
类或接口类型符号引用
假设当前符号引用N所处类为D,N解析后的类或接口的直接引用为C
解析
如果C不是数组,则虚拟机会将N对应的全限定名转递给D的类加载器来进行加载。
加载过程中由于需要元数据验证、字节码验证则可能需要去加载其他类,
这些加载过程一旦出现异常就会解析失败。
加载过程中由于需要元数据验证、字节码验证则可能需要去加载其他类,
这些加载过程一旦出现异常就会解析失败。
如果C时数组且数组元素为对象,则会按照上述不是数组的规则去加载数组元素。
验证
如果经过上述步骤没有出现异常,则C在虚拟机中就是一个有效的类或接口 。
此时在解析完成前还需要进行符号引用验证来确定D是否具有对C的访问权限,
如果不具备权限则会出现java.lang.IllegalAccessException。
此时在解析完成前还需要进行符号引用验证来确定D是否具有对C的访问权限,
如果不具备权限则会出现java.lang.IllegalAccessException。
字段类型符号引用
字段所属类或接口的解析
虚拟机首先会对字段所属的类或接口的符号进行解析,如果解析失败则会导致字段解析的失败。如果解析成功为直接引用C,则会进行后续字段搜索的步骤。
字段搜索
如果C自身包含了简单名称和字段描述符跟目标字段都一致的字段,则会返回这个字段的直接引用,字段搜索结束
否则,如果C实现了接口,则会根据继承关系从下往上递归搜索各个接口以及它的父接口,如果接口中包含了简单名称和字段描述符跟目标字段都一致的字段,则会返回这个字段的直接引用,字段搜索结束
否则,如果C不是java.lang.Object,则会根据继承关系从下往上递归搜索它的父类,如果父类中包含了简单名称和字段描述符跟目标字段都一致的字段,则会返回这个字段的直接引用,字段搜索结束
否则,则会抛出java.lang.NoSuchFieldError异常
验证
同类或接口类型符号引用一样需要进行符号引用验证来验证访问权限
类方法类型符号引用
类方法所属类或接口的解析
同字段类型符号引用的字段所属类或接口的解析步骤,解析成功后使用C来表示类
类方法搜索
因为类方法和接口方法的常量类型定义是分开的,所以如果C是接口,则会直接抛出java.lang.IncompatibleClassChangeError
在C的自身中搜索类方法
在C的所有父类中搜索类方法
在C实现的接口列表以及它们的父接口中搜索类方法,如果搜索到类方法,则证明C为抽象类,会抛出java.lang.AbstractMethodError异常
如果未搜索到类方法,会抛出java.lang.NoSuchMehtodError异常
验证
同上需要进行符号引用验证来验证访问权限
接口方法类型符号引用
接口方法所属类或接口的解析
同字段类型符号引用的字段所属类或接口的解析步骤,解析成功后使用C来表示接口
接口方法搜索
如果C是类,则会抛出java.lang.IncompatibleClassChangeError
在C的自身中搜索接口方法
在C的所有父接口中搜索接口方法
如果未搜索到接口方法,会抛出java.lang.NoSuchMehthodError异常
注意
由于接口中的所有方法默认的访问权限修饰符都是public所以此处不会再进行访问权限的相关验证
方法类型类型符号引用
方法句柄类型符号引用
调用点限定符类型符号引用
初始化
作用
执行类构造器<clinit>()方法
<clinit>()方法相关知识
<clinit>()方法是编译器搜集类中所有静态变量赋值语句和静态语句块中的语句合并而成的,编译器搜集的顺序是由语句在源文件中出现的顺序决定的。静态语句块中的变量只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,静态语句块中只能对其进行赋值而不能访问。
虚拟机自动保证父类的<clinit>()方法一定在子类<clinit>()方法执行前执行完毕,因此父类的静态块要优先于子类静态语句块中的赋值操作。
<clinit>()方法不是必须存在的,如果没有静态语句块也没有静态变量赋值语句,则不会生成<clinit>()方法。
接口中不能使用静态语句块,但是如果接口中有静态变量赋值语句,接口也是会生成<clinit>()方法。但是接口执行<clinit>()方法之前不需要先执行父接口的<clinit>()方法,只有使用到父接口中的变量时才会执行父接口的<clinit>()方法。接口的实现类初始化时也不会执行接口的<clinit>()方法。
虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时初始化一个类,则只会有一个线程执行该类的<clinit>()方法,其他线程处于阻塞状态直到活动线程执行完<clinit>()方法。
类加载器
启动类加载器
Bootstrap ClassLoader
加载类的目录
<JAVA_HOME>/lib目录下的类
-Xbootclasspath指定的目录
注意
启动类加载器无法被java程序直接引用,
需要将加载请求委派给启动类加载器的话可以直接使用null来代替
需要将加载请求委派给启动类加载器的话可以直接使用null来代替
扩展类加载器
Extension ClassLoader
加载类的目录
<JAVA_HOME>/lib/ext目录下的类
java.ext.dirs系统变量指定的目录下类
注意
对应sun.misc.Launcher$ExtClassLoader
应用程序类加载器
Application ClassLoader
加载类的目录
加载ClassPath目录的类库
注意
对应sun.misc.Launcher$AppClassLoader
是ClassLoader中的getSystemClassLoader()方法的返回值
双亲委派模型
图示
工作过程
当一个类加载器收到加载类的请求时,它并不会自己去加载类,
而是将这个加载类的请求委派给自己的父类加载器。
因此所有的类加载请求最终都会委派到最上层类加载器,当父类加载器无法完成类加载的请求时,
子类加载器才会去自己加载类。
而是将这个加载类的请求委派给自己的父类加载器。
因此所有的类加载请求最终都会委派到最上层类加载器,当父类加载器无法完成类加载的请求时,
子类加载器才会去自己加载类。
代码实现
在java.lang.ClassLoader中的loadClass()方法中
打破双亲委派模型
JVM内存模型
程序计数器
线程私有的,用来记录当前线程执行字节码指令的行号,字节码执行引擎通过操作这个计数器的值来记录执行字节码指令对应的行号。
Java堆(GC堆)
解释
所有线程共享的,几乎所有的对象实例和数组都在堆上分配
内部组成
图例
解释
整个Java堆分为年轻代和老年代,年轻代默认占堆内存的1/3,老年代默认占堆内存的2/3。
年轻代又分为Eden区和Survivor区,Eden区默认占年轻代的8/10,
Survivor默认占年轻代的2/10。Survivor中s0和s1默认各占Survivor区的1/2即各占年轻代的1/10
年轻代又分为Eden区和Survivor区,Eden区默认占年轻代的8/10,
Survivor默认占年轻代的2/10。Survivor中s0和s1默认各占Survivor区的1/2即各占年轻代的1/10
内存分配及回收策略
内存分配
栈上分配
解释
虚拟机收到new指令后会先对对象进行逃逸分析,如果该对象不会被外部访问则会通过标量替换在栈上进行分配,
该对象占用的内存就会随着栈帧出栈而销毁,减少了垃圾回收压力。栈上分配依赖逃逸分析和标量替换。
该对象占用的内存就会随着栈帧出栈而销毁,减少了垃圾回收压力。栈上分配依赖逃逸分析和标量替换。
逃逸分析
解释
分析对象的作用域,即如果一个对象在方法中定义了就分析该对象有没有被外部引用。
配置参数
启用(jdk7以后默认开启)
-xx:+DoEscapeAnalysis
禁用
-xx:-DoEscapeAnalysis
标量替换
解释
通过逃逸分析后发现对象不会被外部引用并且能够进一步分解时,jvm不会创建该对象,
而是会把该对象拆解为被方法调用的若干个成员变量来代替然后分配在栈上或寄存器上。
而是会把该对象拆解为被方法调用的若干个成员变量来代替然后分配在栈上或寄存器上。
标量和聚合量
无法在进行拆解的称为标量
java虚拟机中的原始数据类型
可以再进一步进行拆解的称为聚合量
java对象
对象在Eden区分配
大部分对象会分配到Eden区中,如果Eden区第一次存满,字节码执行引擎会发起一次minor gc。
此时非垃圾对象会移入Survivor区中的s0,并且非垃圾对象的对象头中的分代年龄会增加1,垃圾对象直接被gc回收。
后续Eden区如果存满再次触发minor gc的时候,会同时对Eden区和Survivor区中非空的那个区的对象进行gc处理,
两个区中的非垃圾对象会移入Survivor区中空闲的那个区并且分代年龄会增加1,垃圾对象会直接被gc回收。
此时非垃圾对象会移入Survivor区中的s0,并且非垃圾对象的对象头中的分代年龄会增加1,垃圾对象直接被gc回收。
后续Eden区如果存满再次触发minor gc的时候,会同时对Eden区和Survivor区中非空的那个区的对象进行gc处理,
两个区中的非垃圾对象会移入Survivor区中空闲的那个区并且分代年龄会增加1,垃圾对象会直接被gc回收。
大对象直接进入老年代
大对象会直接进入老年代,大对象指的是很长的字符串或数组。
可以通过-XX:PretenureSizeThreshold参数来设置大对象的最小达成值。
可以通过-XX:PretenureSizeThreshold参数来设置大对象的最小达成值。
作用
为了避免为大对象分配内存时的复制操作而降低效率。
长期存活的对象将进入老年代
如果对象的分代年龄达到了15,即经历了15次minior gc后还存在,则该对象会被移入老年代。
如果老年代被放满则会触发full gc,full gc跟minor gc基本逻辑相同,只不过full gc的对象为整个堆空间和方法区。
如果full gc后未清理出老年代空间,并且老年代再次存入对象的话会触发OOM内存溢出。
可以通过-XX:MaxTenuringThreshold来设置进入老年代的最小分代年龄。
如果老年代被放满则会触发full gc,full gc跟minor gc基本逻辑相同,只不过full gc的对象为整个堆空间和方法区。
如果full gc后未清理出老年代空间,并且老年代再次存入对象的话会触发OOM内存溢出。
可以通过-XX:MaxTenuringThreshold来设置进入老年代的最小分代年龄。
对象动态年龄判断
如果Survivor区中相同分代年龄的对象数量占整个Survivor区空间的一半以上时,
所有不小于该分代年龄的对象都会直接进入老年代而不需要达到15次。(jdk7)
所有不小于该分代年龄的对象都会直接进入老年代而不需要达到15次。(jdk7)
如果一批对象的总大小大于Survivor区域当前存放对象区域的大小的50%,
此时大于等于这批对象年龄最大值的对象就直接放入老年代中。(jdk8)
此时大于等于这批对象年龄最大值的对象就直接放入老年代中。(jdk8)
对象动态年龄判断机制一般是在minor gc之后触发的。
老年代空间分配担保机制
发生minor gc之前,虚拟机会检查老年代中剩余最大连续可用空间是否大于年轻代中所有对象的空间总和,
如果大于则会进行一次安全的minor gc。
如果不大于则会查看HandlePromotionFailure设置的值来判断是否允许担保失败。
-XX:+HandlePromotionFailure为开启,-XX:-HandlePromotionFailure为关闭。
如果设置为true即允许担保失败,
则会检查老年代中剩余最大连续可用空间大小是否大于之前每次进入老年代对象空间大小的平均值,
如果大于则会尝试进行一次有风险的minor gc。
如果这次minor gc失败,或者最大连续可用空间小于平均值或者HandlePromotionFailure设置为关闭,
则会先进行full gc然后再进行minor gc。
如果大于则会进行一次安全的minor gc。
如果不大于则会查看HandlePromotionFailure设置的值来判断是否允许担保失败。
-XX:+HandlePromotionFailure为开启,-XX:-HandlePromotionFailure为关闭。
如果设置为true即允许担保失败,
则会检查老年代中剩余最大连续可用空间大小是否大于之前每次进入老年代对象空间大小的平均值,
如果大于则会尝试进行一次有风险的minor gc。
如果这次minor gc失败,或者最大连续可用空间小于平均值或者HandlePromotionFailure设置为关闭,
则会先进行full gc然后再进行minor gc。
内存回收
判断对象是否是垃圾对象的方法
引用计数算法
说明
为对象添加一个计数器,当有其他地方引用该对象的时候这个计数器就加1,
当有其他地方不在引用该对象的时候这个计数器会减1,
当这个计数器为0的时候证明没有其他地方引用该对象,即该对象为垃圾对象
当有其他地方不在引用该对象的时候这个计数器会减1,
当这个计数器为0的时候证明没有其他地方引用该对象,即该对象为垃圾对象
分析
实现简单,效率高,但是主流虚拟机并没有使用这个方法,
因为该算法解决不了对象间循环引用的问题。
因为该算法解决不了对象间循环引用的问题。
循环引用
除了两个对象相互引用这对方,再无其他地方引用这两个对象。
而他们的引用计数器也不为0,导致gc无法回收这两个对象。
而他们的引用计数器也不为0,导致gc无法回收这两个对象。
可达性分析算法
说明
通过"GC Roots"对象开始向下进行搜索,搜索走过的路径被成为引用链。当一个对象到GC Roots对象之间不存在引用链,即该对象到GC Roots是不可达的时,该对象就被视为垃圾对象。
可以当做GC Roots的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中native方法引用的对象
finalize()方法最终判定对象是否存活
解释
可达性分析算法中不可达的对象不会立刻被回收,而是需要再次标记
过程
第一次标记
对象如果没有覆盖finalize()方法则会被直接回收
第二次标记
对象需要在finalize()方法中与引用链上的任意一个对象建立关联关系
即可在次二次标记时被移除出即将回收的集合
即可在次二次标记时被移除出即将回收的集合
三色标记
解释
在并发标记的过程中,因为标记期间引用线程还在执行,对象之间的引用可能发生变化,因此可能发生多标或漏标的情况。
此处引入三色标记把GC Roots可达性分析遍历对象过程中遇到的对象按照是否访问过来标记为三种颜色。
此处引入三色标记把GC Roots可达性分析遍历对象过程中遇到的对象按照是否访问过来标记为三种颜色。
多标
在并发标记的过程中,如果由于方法运行结束导致部分局部变量(GC Roots)被销毁,
但是这些局部变量引用的对象又被扫描过(即标记为非垃圾对象),那么本轮GC就不会回收这些对象,
这部分应该回收倒是没有回收的内存被称为浮动垃圾,浮动垃圾不影响垃圾回收的正确性,但是要到下一轮GC来能清除浮动垃圾。
并发标记和并发清理开始后产生的新对象,通常是标记为黑色,本轮GC不会清除,
这些对象在GC期间也可能变为垃圾对象,也是浮动垃圾的一部分。
但是这些局部变量引用的对象又被扫描过(即标记为非垃圾对象),那么本轮GC就不会回收这些对象,
这部分应该回收倒是没有回收的内存被称为浮动垃圾,浮动垃圾不影响垃圾回收的正确性,但是要到下一轮GC来能清除浮动垃圾。
并发标记和并发清理开始后产生的新对象,通常是标记为黑色,本轮GC不会清除,
这些对象在GC期间也可能变为垃圾对象,也是浮动垃圾的一部分。
漏标
解决原理
底层使用读写屏障原理来解决漏标问题
写屏障
概念
在复制操作前后加入一些处理(类似AOP的理念)
具体实例
写前屏障
在引用删除前将引用关系存储起来从而实现原始快照
写后屏障
在新增引用后将引用关系存储起来从而实现增量更新
解决方案
增量更新
Incremental Update
当黑色对象插入心的指向白色对象的引用关系时,将这个新的引用记录下来,
在重新标记的阶段再将这些记录过的引用关系中的黑色对象作为根重新扫描一次。
即黑色对象一旦新插入了指向白色对象的引用后,这个黑色对象会变回灰色对象。
在重新标记的阶段再将这些记录过的引用关系中的黑色对象作为根重新扫描一次。
即黑色对象一旦新插入了指向白色对象的引用后,这个黑色对象会变回灰色对象。
原始快照
Snapshot At The Beginning、SATB
当灰色对象删除指向白色对象的引用关系时,将这个删除的引用记录下来,
在重新标记的阶段以记录过的引用关系中的灰色对象作为根开始扫描,
然后将扫描到的白色对象标记为黑色。
目的是为了保证这种白色对象在本轮GC中存活,下一轮GC重新扫描来判断这个对象正确的状态。
该方案可能会产生浮动垃圾,但保证了对象不会被误删除。
在重新标记的阶段以记录过的引用关系中的灰色对象作为根开始扫描,
然后将扫描到的白色对象标记为黑色。
目的是为了保证这种白色对象在本轮GC中存活,下一轮GC重新扫描来判断这个对象正确的状态。
该方案可能会产生浮动垃圾,但保证了对象不会被误删除。
HotSpot虚拟机并发标记时对漏标的处理方案
CMS垃圾收集器
写屏障+增量更新
G1,Shenandoah
写屏障+SATB
ZGC
读屏障
为什么G1使用SATB而CMS使用增量更新
SATB相比增量更新效率更高(不需要在重新标记阶段再次深度扫描,但是会产生更多的浮动垃圾)。
G1对象处于不同的region中而CMS就一块老年代,所以重新深度扫描的话G1的代价比CMS大。
因此G1选择SATB不需要重新深度扫描。
G1对象处于不同的region中而CMS就一块老年代,所以重新深度扫描的话G1的代价比CMS大。
因此G1选择SATB不需要重新深度扫描。
颜色解释
黑色
表示对象已经被垃圾收集器访问过,并且这个对象的所有引用都被扫描过。
黑色的对象表示安全存活的,如果其他对象的引用指向了黑色对象则无需重新扫描。
黑色对象不能直接(即不通过灰色对象)指向白色对象。
黑色的对象表示安全存活的,如果其他对象的引用指向了黑色对象则无需重新扫描。
黑色对象不能直接(即不通过灰色对象)指向白色对象。
灰色
表示对象已经被垃圾收集器访问过,但是这个对象里面至少存在一个引用还没有被扫描过。
白色
表示对象尚未被垃圾收集器访问过。在可达性分析刚开始时所有对象都是白色的,但是在分析结束时,白色的对象表示不可达对象。
记忆集与卡表
解释
为了解决新生代GC Roots做可达性分析时如果出现跨代引用的问题,新生代中有一小块区域引入了记忆集(Remember Set),记录从非收集区到收集区的引用从而避免把老年代加入GC Roots的扫描范围。
具体实现
HotSpot虚拟机使用卡表(Cardtable)来实现记忆集
卡表是一个字节数组,卡表把老年代分成一块一块区域,每块区域叫做卡页(HotSpot虚拟机使用的卡页大小为2^9即512字节),
卡表中的每个元素就对应一块卡页。
每个卡页中可能有多个对象,当其中有一个对象有对新生代的引用时,就称这个卡页为脏的,
即卡表中对应这块卡页的数据为1,如果卡页不脏则卡表中对应数据为0。
卡表中的每个元素就对应一块卡页。
每个卡页中可能有多个对象,当其中有一个对象有对新生代的引用时,就称这个卡页为脏的,
即卡表中对应这块卡页的数据为1,如果卡页不脏则卡表中对应数据为0。
卡表数据的维护也是通过写屏障实现的,在进行字段赋值时,对卡表对应的数据进行更新即可。
引用分类
强引用
说明
常见的变量引用,只要存在强引用,垃圾回收就不会回收该对象
例子
public static A a = new A();
软引用
说明
被SoftReference包裹的引用,该引用一般不会被gc回收。
当经过一次gc后内存空间仍不足以保存新对象时,gc会回收软引用对象。
当经过一次gc后内存空间仍不足以保存新对象时,gc会回收软引用对象。
例子
public static SoftReference<A> a = new SoftReference<A>(new A());
弱引用
说明
类似于软引用但是比软引用强度低,相当于没引用会被gc直接回收
例子
public static WeakReference<A> a = new WeakReference<A>(new A());
虚引用
说明
也叫幻影引用或幽灵引用,强度最弱的引用,
一般用来保证对象被gc回收的时候收到通知
一般用来保证对象被gc回收的时候收到通知
例子
public static PhantomReference<A> a =
new PhantomReference<A>(new A());
new PhantomReference<A>(new A());
判断类是否是无用类
该类的所有实例都被回收即java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
STW
解释
即Stop The World,虚拟机进行gc的时候会停止所有用户线程。
作用
如果没有STW机制,则虚拟机收集非垃圾对象的过程中一些非垃圾对象已经变成垃圾对象,
但是这些对象已经被标记为非垃圾对象从而导致这些垃圾对象不会被gc回收。
STW机制就是为了保证在gc过程中对象是否为垃圾对象是确定不变的,提高gc的准确度。
但是这些对象已经被标记为非垃圾对象从而导致这些垃圾对象不会被gc回收。
STW机制就是为了保证在gc过程中对象是否为垃圾对象是确定不变的,提高gc的准确度。
重点
由于gc会触发STW,对于用户而言系统会停顿,对用户不友好。因此虚拟机调优的主要目的就是减少STW的发生。
minor gc的STW时间很短,但也要减少过于频繁的minor gc,
而full gc的STW时间较长,因此要尽量减少full gc发生的次数。
minor gc的STW时间很短,但也要减少过于频繁的minor gc,
而full gc的STW时间较长,因此要尽量减少full gc发生的次数。
虚拟机栈
解释
是线程私有的,生命周期和线程的生命周期一致。每个方法执行的时候会在栈中
创建一块栈帧来存储局部变量表、操作数栈、动态链接、方法出口(返回地址)等信息。
一个方法的执行过程实际上就是一个栈帧在虚拟机栈中从入栈到出栈的过程。
创建一块栈帧来存储局部变量表、操作数栈、动态链接、方法出口(返回地址)等信息。
一个方法的执行过程实际上就是一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈帧
局部变量表
存放了各种基本数据类型、对象类型的引用地址或指向字节码指令的地址
操作数栈
一个后入先出栈,在方法执行过程中各种字节码指令会向栈中写入或提取数据
动态连接
常量池中的一部分符号引用会在类加载阶段或第一次使用的时候转换为直接引用,这种转化成为静态解析。而另一部分符号引用会在每一次运行期间转化为直接引用,这种转化成为动态连接。
方法出口(返回地址)
方法退出方式
正常完成出口
执行引擎遇到方法返回的字节码指令
异常完成出口
方法中出现异常但是没有异常处理手段
作用
保存一些调用方法前程序运行的状态信息,使得方法结束后程序能正确恢复之前的执行状态。
本地方法栈
类似虚拟机栈,也是线程私有的。区别就是虚拟机栈服务的对象为java方法,而本地方法栈服务的对象为native方法。
方法区(元空间、持久代)
所有线程共享的,存储常量(运行时常量池)、静态变量以及类信息等数据
默认初始值大小为21M,并且容量会动态变化。如果21M占用满了则会触发full gc,
full gc后如果剩余占用容量很少则下次full gc触发的容量会减少,
而如果full gc后剩余的占用容量很大,几乎占满即full gc清理了很少的空间,则下次full gc触发的容量会增加。
因此推荐设置元空间大小为256M来避免不必要的full gc。
full gc后如果剩余占用容量很少则下次full gc触发的容量会减少,
而如果full gc后剩余的占用容量很大,几乎占满即full gc清理了很少的空间,则下次full gc触发的容量会增加。
因此推荐设置元空间大小为256M来避免不必要的full gc。
相关参数设置
-Xmx
JVM最大堆内存
-Xms
JVM初始堆内存
-Xmn
年轻代内存
-Xss
栈内存,默认为1M,超过设置的值会触发StackOverflow栈溢出
-XX:SurvivorRatio
- 新生代中Eden与Survivor的内存比例,默认为8
-XX:MaxMetaspaceSize
元空间最大内存,默认为21M
-XX:MetaspaceSize
元空间初始内存,默认为21M
-XX:+UseAdaptiveSizePolicy
使Eden区和Survivor中s0和s1区的默认比例8:1:1动态变化
-XX:-UseAdaptiveSizePolicy
使Eden区和Survivor中s0和s1区的默认比例8:1:1不进行动态变化
HotSpot对象
对象创建过程
图示
解释
类加载检查
虚拟机收到new指令后会检查该指令的参数能否在常量池中找到指向某个类的符号引用,
并且检查这个符号引用对应的类是否已经被加载、解析、初始化过。如果没有则进行类的加载过程
并且检查这个符号引用对应的类是否已经被加载、解析、初始化过。如果没有则进行类的加载过程
类加载
参考上文类加载机制节点中的类加载过程节点内容
分配内存
概述
对象所需的内存大小在经过类加载后就会确定
(如何确定参考HotSpot对象节点中的对象内存布局节点),
因此分配内存其实就是在java堆中划分出一块确定大小的内存。
(如何确定参考HotSpot对象节点中的对象内存布局节点),
因此分配内存其实就是在java堆中划分出一块确定大小的内存。
分配方式
指针碰撞
前提条件
java堆中的内存是规整的,
即用过的内存在一边、空闲的内存在一边,
中间存在一个指针当做分界指示
即用过的内存在一边、空闲的内存在一边,
中间存在一个指针当做分界指示
分配过程
将指针向空闲内存那边移动
对象所需内存大小的距离
对象所需内存大小的距离
空闲列表
前提条件
java堆中的内存是不规整的,
即用过的内存和空闲内存交错出现
即用过的内存和空闲内存交错出现
分配过程
虚拟机会维护一张记录空闲内存的列表,
分配时将一块足够大的空闲内存分配给对象,
然后更新列表
分配时将一块足够大的空闲内存分配给对象,
然后更新列表
决定java堆是否规整的因素
垃圾收集器是否具有压缩整理功能
划分空间时候的并发问题的解决方案
CAS
(compare and swap)
(compare and swap)
对分配空间的行为进行同步处理,虚拟机采用CAS加失败重试方式保证更新操作的原子性
TLAB(本地线程分配缓冲)
每个线程在java堆中预先分配一小块内存,叫做TLAB(Thread Local Allocation Buffer),
有线程需要分配内存就在该线程的TLAB上进行分配,
只有当TLAB内存用完并分配新TLAB时才进行同步锁定。
虚拟机是否启用TLAB可以通过-xx:+/-UseTLAB来配置。
有线程需要分配内存就在该线程的TLAB上进行分配,
只有当TLAB内存用完并分配新TLAB时才进行同步锁定。
虚拟机是否启用TLAB可以通过-xx:+/-UseTLAB来配置。
初始化
虚拟机将分配到的内存空间初始化为零值(不包括对象头),如果启用了TLAB初始化也可以提前到TLAB分配时进行。初始化保证了对象的实例变量可以不经过赋初始值就可以直接使用,程序可以访问到这些字段对应数据类型的零值。
设置对象头
将对象的一些必要设置信息存入对象的对象头中,具体详情参照HotSpt对象节点中的对象内存布局节点。
执行<init>()方法
将对象按照程序员的设置进行初始化。
对象内存布局
对象头
标记字段(Mark Word)
图示(以32bit为例)
数据长度
32位虚拟机
32bit(4字节)
64位虚拟机
64bit(8字节)
作用
用于存储对象自身运行时的数据
对象哈希码
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳
类型指针(Klass Point)
数据长度
开启压缩
4字节
关闭压缩
8字节
作用
指向对象类元数据的指针,虚拟机通过这个指针确定对象是哪个类的实例。
指针压缩
作用
开启指针压缩会减少指向成员变量的指针和类型指针的大小,减少java堆内存压力,减少gc次数
配置
开启指针压缩(jdk1.6后默认的)
-xx:+UseCompressedOops
关闭指针压缩
-xx:-UseCompressedOops
开启Klass Point压缩
-xx:+UseCompressedClassPoints
关闭Klass Point压缩
-xx:-UseCompressedClassPoints
数组长度
数据长度
4字节
作用
只有数组对象才有,用来记录数组长度,因为虚拟机可以通过普通java对象的元数据来确定java对象的大小,但是从数组的元数据无法确定数组的大小。
实例数据
存储对象中成员变量内容。
对齐填充
不是必然存在的,为了保证对象的大小是8字节的整倍数。当对象头以及实例数据部分的大小不是8字节的整倍数时才会有对齐填充部分,该部分没有其他含义仅仅起了占位符的作用。
垃圾收集
垃圾收集算法
是内存回收的方法论
理论依据
分代收集理论
将java堆分为老年代和新生代,
根据不同代的不同特点来选用不同的垃圾收集算法
根据不同代的不同特点来选用不同的垃圾收集算法
具体算法
复制算法
将内存分为相同大小的两块区域,每次使用其中的一块内存。
当使用的那块内存空间用完后,将存活的对象复制到另一块内存,然后清空当前块内存。
当使用的那块内存空间用完后,将存活的对象复制到另一块内存,然后清空当前块内存。
总结
适合新生代使用、浪费空间
标记整理算法
标记存活对象然后将存活对象移动到一端然后将边界外的对象全部回收
标记清除算法
标记存活对象然后回收未标记对象
标记需要回收的对象然后回收这些标记的对象
总结
效率低下,清楚后会导致出现内存中出现大量不连续的碎片
垃圾收集器
是内存回收的具体实现
垃圾收集器种类
新生代垃圾回收器
Serial收集器
是一个单线程收集器,不仅只会使用一条垃圾收集线程进行垃圾收集
而且进行垃圾收集的时候会STW(Stop The World)
而且进行垃圾收集的时候会STW(Stop The World)
收集算法
复制算法
配置参数
-XX:+UseSerialGC
总结
在单线程环境下比其他垃圾收集器更加简单高效,
可以跟CMS收集器配合工作
可以跟CMS收集器配合工作
ParNew收集器
相当于Serial收集器的多线程版本,
跟Serial的区别主要就是使用多线程进行垃圾收集。
跟Serial的区别主要就是使用多线程进行垃圾收集。
收集算法
复制算法
配置参数
-XX:+UseParNewGC
总结
Serial收集器的多线程版本,
可以跟CMS收集器配合工作
可以跟CMS收集器配合工作
Parallel Scavenge收集器
类似ParNew收集器,不过关注点为吞吐量(高效率使用CPU),
CMS等收集器关注点是用户线程的停顿时间,
吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
默认的垃圾收集线程数和cpu的核心数目相同,
可以通过-XX:ParallelGCThreads来自定义垃圾收集线程数(不推荐自定义)。
不可以和CMS收集器配合使用。
CMS等收集器关注点是用户线程的停顿时间,
吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
默认的垃圾收集线程数和cpu的核心数目相同,
可以通过-XX:ParallelGCThreads来自定义垃圾收集线程数(不推荐自定义)。
不可以和CMS收集器配合使用。
收集算法
复制算法
参数配置
-XX:+UseParallelGC
老年代垃圾回收器
Serial Old收集器
是Serial收集器的老年代版本
收集算法
标记整理算法
配置参数
-XX:+UseSerialOldGC
总结
在单线程环境下比其他垃圾收集器更加简单高效
Parallel Old收集器
是Parallel Scavenge收集器的老年代版本
收集算法
标记整理算法
配置参数
-XX:+UseParallelOldGC
总结
关注点主要是吞吐量,-XX:MaxGCPauseMillis来控制最大垃圾收集停顿时间,-XX:GCTimeRatio来自定义吞吐量大小,-XX:+UseAdaptiveSizePolicy启用后不需要用户来自定义新生代大小、Eden和Survivor区比例以及晋升老年代所需的分代年龄等参数,虚拟机会进行采用GC的自适应调节策略。
CMS收集器(Concurrent Mark Sweep并发标记清除)
是HotSpot虚拟机第一款并发收集器,第一次实现了垃圾收集线程和用户变成基本上同时工作。
收集算法
标记清除算法
参数配置
-XX:+UseConcMarkSweepGC
运作过程
初始标记(进行STW)
STW并记录下gc roots直接引用的对象,速度很快
并发标记(不进行STW)
不进行STW,从GC Roots的直接关联对象开始遍历整个对象图。
耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
因为用户程序继续运行所以可能导致已经标记过的对象状态发生改变。
耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
因为用户程序继续运行所以可能导致已经标记过的对象状态发生改变。
重新标记(进行STW)
为了修正并发标记期间因为用户线程继续运行而导致标记产生变动的哪一部分对象的标记记录,
停顿时间比初始标记阶段稍长但是远远比并发标记阶段时间短。
主要利用三色标记里的增量更新算法做重新标记。
停顿时间比初始标记阶段稍长但是远远比并发标记阶段时间短。
主要利用三色标记里的增量更新算法做重新标记。
并发清理(不进行STW)
开启用户线程,同时GC线程开始对未标记的区域做清理。
这个阶段如果有新增对象则会被标记为黑色然后不做任何处理。
这个阶段如果有新增对象则会被标记为黑色然后不做任何处理。
并发重置(不进行STW)
重置本次GC过程中的标记数据。
总结
优点
并发收集
停顿时间短
缺点
会占用CPU资源
无法处理浮动垃圾(在并发标记和并发清理阶段又产生的垃圾),只能在下一次GC清理
采用的标记清除算法会导致垃圾收集结束后出现大量空间碎片
(可以通过-XX:+UseCMSCompactAtFullCollection让jvm在执行完标记清除后再做整理)
(可以通过-XX:+UseCMSCompactAtFullCollection让jvm在执行完标记清除后再做整理)
执行过程存在不确定性(上一次垃圾回收还没结束,新的垃圾回收又触发,特别是并发标记和并发清理阶段)。
一边回收,系统一边继续运行,回收没结束又再次触发full gc,会出现"concurrent mode failure",
此时会进入STW,使用serial old垃圾收集器来进行垃圾收集。
一边回收,系统一边继续运行,回收没结束又再次触发full gc,会出现"concurrent mode failure",
此时会进入STW,使用serial old垃圾收集器来进行垃圾收集。
其他参数配置
并发GC的线程数
-XX:ConcGCThreads
FullGC之后做压缩整理,减少空间碎片
-XX:+UseCMSCompactAtFullCollection
进行多少次FullGC后进行压缩整理,默认为0即每次FullGC都会压缩整理一次
-XX:CMSFullGCsBeforeCompaction
老年代使用达到多少比例会触发FullGC,默认为92(百分比)
-XX:CMSInitiatingOccupancyFraction
只使用设定的回收阙值(-XX:CMSInitiatingOccupancyFraction设定的值),
不启用这个参数JVM只会在第一次GC使用设定的值,后续会自动调整
不启用这个参数JVM只会在第一次GC使用设定的值,后续会自动调整
-XX:+UseCMSInitiatingOccupancyOnly
在CMS GC前进行一次minor GC,可以减少老年代对新生代的引用,
降低标记阶段的开销(CMS GC耗时80%都在标记阶段)
降低标记阶段的开销(CMS GC耗时80%都在标记阶段)
-XX:+CMSScavengeBeforeRemark
在初始标记阶段多线程执行,缩短STW
-XX:+CMSParallelInitialMarkEnabled
在重新标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled
G1垃圾回收器
主要多处理器大内存机器,将java堆划分成若干块大小相等的独立区域(region),jvm最多可以有2048个region。每个region大小默认为堆大小除以2048,可以通过参数进行自定义region大小(推荐默认)。G1保存了新生代和老年代的概念,它们都代表了一系列region集合(可以是不连续的)。新生代默认占整个堆的5%,可以通过参数调整。系统运行过程中JVM会不停增加新生代的region,但是不会超过整个堆的60%,可以通过参数调整。新生代中的内存划分也跟之前一样,Eden和S0、S1的占比为8:1:1。Region的区域属性会动态变化,即当前region区域可能是新生代,但是经过垃圾回收后该region可能会变成老年代。G1的对象内存分配策略跟之前相同,唯一不同的为G1对大对象的分配策略。G1中有专门存放大对象的区域(Humongous),在G1中如果一个对象的大小超过了region大小的50%,就会放入Humongous中。Humongous节省了老年代的内存开销,降低了因为老年代内存不足而进行Full GC的频率。G1进行Full GC的时候也会回收Humongous区域。
参数配置
-XX:+UseG1GC
运作过程
初始标记(进行STW)
同CMS初始标记阶段
并发标记(不进行STW)
同CMS并发标记阶段
最终标记(进行STW)
同CMS重新标记阶段
筛选回收(进行STW)
会对各个Region的回收价值和回收成本进行排序(在后台维护一个优先列表Garbage-First,每次根据期望STW时间优先回收那些回收价值高的Region,回收价值为回收空间与回收时间的比值),然后根据用户设置的期望STW的时间(通过参数配置)来制定回收计划。根据预期回收计划的时间将能够回收的对象(Collection Set,要回收的集合)进行回收,剩余的对象留到下一次GC处理。CMS的回收阶段为回收线程和用户线程并发运行的,而G1由于内部设计复杂没有实现并发回收。
回收算法
无论是新生代还是老年代都用的是复制算法进行回收,即将一个region中存活的对象复制到另一个region中,因此不会产生过多的内存碎片。
垃圾收集分类
Young GC
当Eden区放满的时候,虚拟机会计算回收当前Eden区所需的时间。如果这个时间远小于用户设定的期望STW时间,那么虚拟机会使用一块空闲的region区域来存放新的对象,而不会进行Young GC。当回收当前Eden区所需要的的时间接近期望STW时间,那么会进行Young GC来回收Eden区域
Mixed GC
当老年代在堆中占比达到设定的阙值时会执行Mixed GC,会回收所有新生代、部分老年代(根据GC的停顿时间来决定老年代区域的回收顺序)以及大对象区域。Mixed GC采用复制算法,会把每个region中的存活对象复制到其他空闲region中,如果其他空闲region不够用,则会触发Full GC。
Full GC
停止系统程序,然后通过单线程来进行标记清理、压缩整理来产生空闲的region供下次Mixed GC来使用,耗时很长。
其他参数配置
手动指定每个region的大小(必须为2的N次幂,默认大小为堆总大小除以2048,推荐使用默认计算)
-XX:G1HeapRegionSize
手动指定GC工作的线程数
-XX:ParallelGCThreads
新生代占比(默认为5%)
-XX:G1NewSizePercent
新生代最多占比(默认为60%)
-XX:G1MaxNewSizePercent
期望GC的STW时间(默认200ms,自定义值时需要切合实际,
如果设置的值过短会导致每次回收的垃圾对象很有限,最终导致堆被占满后触发full gc)
如果设置的值过短会导致每次回收的垃圾对象很有限,最终导致堆被占满后触发full gc)
-XX:MaxGCPauseMillis
触发Mixed GC的老年代堆占比阙值(默认45%)
-XX:InitatingHeapOccupancyPercent
每次GC的筛选回收的次数(默认8次)
-XX:G1MixedGCCountTarget
适用场景
50%以上堆内存被存活对象占用
对象分配和晋升变化速度很快
垃圾回收时间过长,超过1s
8GB以上堆内存
停顿时间500ms以内
ZGC
暂时先不看,用得不多,不够稳定
Shenandoah
G1的升级版本,实现了G1筛选回收中的并发进行功能
如何选择垃圾回收器
优先调整堆的大小来让服务器自己选择
如果内存小于100M,优先选择串行收集器
如果是单核并且对停顿时间没有要求,选择串行或者JVM自己选择
如果允许停顿时间超过一秒,选择并行或JVM自己选择
如果响应时间最重要且不能超过一秒则选择并发收集器
堆内存4G以下可以使用Parallel收集器,4-8G可以使用ParNew+CMS,8G以上使用G1,几百G以上使用ZGC
JDK1.8默认使用Parallel两个分代收集器,JDK1.9默认使用G1收集器
安全点和安全区域
安全点
概念
代码中一些特定的位置,当线程运行到这些位置的时候状态是确定的。此时JVM才能安全的进行一些操作,比如GC。
因此GC并不是想触发就能触发的,必须要等线程运行到安全点的时候才会触发。
因此GC并不是想触发就能触发的,必须要等线程运行到安全点的时候才会触发。
主要的安全点
方法返回前
调出某个方法之后
抛出异常的位置
循环的末尾
让GC发生时所有线程都运行到最近的安全点上再停顿的方案
抢先式中断(几乎不用)
GC发生时将所有线程中断,如果有线程的中断地方不在安全点则恢复线程,让它执行到安全点上。
主动式中断
GC需要中断线程时不直接对线程进行操作而是设置一个标志。各个线程执行时主动轮询这个标志,
发现中断标志为真时就自己中断挂起,轮询标志的位置和安全点是重合的。
发现中断标志为真时就自己中断挂起,轮询标志的位置和安全点是重合的。
安全区域
概念
当线程处于sleep或blocked状态时,无法响应JVM的中断请求,无法执行到安全点然后中断,此时就需要安全区域来处理这个问题。
安全区域指的是在一段代码段中引用关系不会发生变化,在这个区域的任意地方进行GC都是安全的。安全区域可以看做是安全点的扩展。
安全区域指的是在一段代码段中引用关系不会发生变化,在这个区域的任意地方进行GC都是安全的。安全区域可以看做是安全点的扩展。
线程运行到安全区域时会标记自己已经进入安全区域,当JVM要发起GC时就不需要处理带有安全区域标识的线程。而当线程要离开安全区域时需要检查系统是否完成了GC,如果完成了线程就离开安全区域然后继续执行,否则线程会等待直到收到可以安全离开安全区域的信号为止。

收藏

收藏
0 条评论
下一页