jdk--JVM总结以及调优(超详细)
2021-07-16 18:21:03 0 举报
AI智能生成
登录查看完整内容
为你推荐
查看更多
JVM总结,JVM调优,JVM详细笔记,汇聚三门大课的笔记
作者其他创作
大纲/内容
java虚拟机的生命周期
使用JVM参数可以看到即使子类没有初始化,但是也是加载了的
加载
验证:确保被加载类的正确性
准备
静态链接
动态链接
解析
连接
必须初始化的情况
final不初始化被调用类的情况
static不初始化子类的情况
接口初始化不会要求父接口初始化
通过子类使用父类的静态变量不会导致父类的初始化
类的初始化不会要求接口的初始化(只有使用接口的静态变量的时候才会初始化接口),但是要求父类的初始化
只是ClassLoader.loadClass加载一个类,不是类的主动使用,不会导致类的初始化,只会 加载。static代码块没有执行forName有一个参数是是否初始化,默认是true,所以我们使用Class.forName就会导致类的初始化
只是声明而不是new,不会初始化
不会初始化的情况
假如有直接父类,先初始化直接父类
假如存在初始化语句,依次从上到下执行这些初始化语句
几个假如
主动使用:只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义,才可以认为是对类或者接口的主动使用
被动使用:除了7种主动使用
初始化clinit代码块有静态就会有这个代码块
虚拟机自带的类加载器(根扩展系统)加载的类是不可以卸载的,只有虚拟机关闭的时候才会卸载因为这些虚拟机会始终引用类加载器,而类加载器始终会引用自己加载的class类对象。
类可回收判断
一个类何时结束生命周期,取决于代表他的class对象何时结束生命周期
所有的置为空之后,显式调用System.gc(),而且睡一下,使用jvisualVM 看一下
用户自定义的类加载器加载的类是可以被卸载的
类的卸载
只是声明不会执行静态代码块,只有new才会
原则
1 类加载过程
Class对象
对于数组实例来说,其类型是由JVM在运行期间动态生成的,表示为[Lcom.shengsiyuan.jvm.re.MyParent4 这种形式。动态生成的类型,其父类型就是Object。
助记符 anewarray:表示创建一个引用类型的(如类/接口/数组)数组,并将其引用值压入栈顶
助记符 newarray:表示创建一个指定的原始类型(如int/float/char等)的数组,并将其引用值压入栈顶
数组的类加载器之间的区别:自定义引用类型的数组对象是应用类加载器,原生数据类型的数组对象没有类加载器,String的数组对象是各类加载器
只有数组的对象不是类加载器创建的,数组对象是虚拟机运行期自己创建的数据类型调用getClassLoader返回的和内部元素调用返回的是一样的元素是
原始类型的数组和引用类型的数组的区别
不需要等到使用的时候才加载,但是也不是全部加载,预判会使用会尝试加载
不显式调用类对象的newInstance,而只是loadClass的时候,是不会导致类的初始化的,只是会导致类的加载
预加载机制
Launcher是启动类加载器加载的所以内部的静态内部类也是启动类加载的
加载java.lang.ClassLoader,扩展类和应用加载器
加载JRE所需要的基本组件
没有继承java.lang.ClassLoader
引导类Bootstrap
sun.misc.Launcher.ExtClassLoader
继承java.lang.ClassLoader
扩展类Extension
sun.misc.Launcher.AppClassLoader
java.system.class.loader
java.lang.ClassLoader#getSystemClassLoader默认返回的AppClassLoader
应用程序类System
3类类加载器
//扩展类加载器与系统类加载器也是由启动类加载器所加载的 //系统类加载器和扩展类加载器都是Launcher的静态内部类Launcher.class.getClassLoader()
定义类加载器:实际加载的那个
初始类加载器:自己不能加载,但是委托给父类了,父子都是这个类的初始类加载器
两种类加载器
一个类加载器必须重写findClass方法,但是loadClass 里面其实里面就实现了双亲委派, //想打破双亲委派就需要重写 loadClass
实现java.lang.ClassLoader类,然后覆盖他的findClass(String name)方法即可。findClass根据 指定的类的名字,返回对应的Class对象的引用
private String path;指定这个类加载器加载类的路径
编写
想要把自定义类加载器作为系统类加载器,指定类加载器必须有constructor(ClassLoader parent)方法,此时AppClassloader为它的父类
自定义类加载器ClassLoader
为什么可以使用clazz.getClassLoader()获取到类加载器,因为类加载 器的作用是加载类而不是对象,想获取对象的话还需要类对象newInstance方法才可以获取实例的对象
获取当前类的类加载器
获取当前线程上下文的类加载器
获取系统的类加载器
获取调用者的类加载器
获取类加载器的四种方式
实现沙箱安全,核心类不会被破坏,用户自定义的类加载器不能加载应该由父类加载器加载的可靠类,从而防止恶意代码的加载java.lang.Object总是会被启动类加载器加载,不会被其他加载
避免重复加载,各司其职
为什么:
没有的话调用父类加载器的loadClass
findLoadedClass(String)
loadByteData
defineClass
findClass(String)
loadClass
加载顺序
parent是包装关系而不是树状继承关系,创建构造器的时候可以传递一个参数作为当前构造器的父类构造器
双亲委派机制:自底向上检查类是否已经被加载,自顶向下尝试加载类
每个类加载器都有自己的命名空间,是由当前的类加载器以及父类加载器加载的类构成的。
但是线程上下文可以看到,打破双亲委派
子加载器可以看到父加载器加载的类,父加载器看不到子加载器加载的类
不同命名空间可以出现类名和包名完全相同的两个类(这种情况是不可以强转的)Tomcat就是利用命名空间实现在一个JVM进程里面加载多个类的
类加载器的命名空间
构造器会初始化 扩展类 应用类 以及 线程上下文类加载器
Launcher源码
给定二进制名字,生成类定义的数据,将给定的二进制名字转换为路径,文件读取字节码内容
二进制名字:有效的类名,
defineClass将读取的字节数组转换为内存里面可以使用的对象转换字节数组到一个类Class的实例,这个实例使用Class.newInsatance创建
findClass(String)用户想要自定义的话必须自己实现
获取系统类加载器就是在初始化的时候设置的应用类加载器
getSystemClassLoader
ClassLoader源码
使用给定的类加载器 返回 与具有给定字符串名称的类或接口关联的 Class 对象。
调用的是native方法,可以指定一个参数设置是否初始化,默认是true
此方法不能用于获取任何表示原始类型或 void 的 Class 对象。
Class.forName源码
线程上下文类加载器的重要性:SPI(Service Provider Interface),也就是SPI的实现是依赖于线程上下文类加载器的,也就是依赖于Service.load的加载能力的,有了这个能力才能让接口的加载器启动类加载器间接加载自己看不到的第三方实现类
线程上下文类加载器的一般使用模式(获取 - 使用 - 还原)
通过线程上下文类加载器获取应用类加载器
forName Driver的实现类,也就是MySQL里面的,会导致初始化。MySQL实现的静态代码块会把自己注册到DriverManager
这不会使用SPI,因为我们指定了,就是用当前的类加载器去加载。而没必要应用程序上下文
初始化DriverManager执行静态代码块
这个是我们自己的主动new和注册的
java.sql.DriverManager.registerDriver(new Driver());注册驱动到DriverManager
加载MySQL的驱动Class.forName(\"com.mysql.cj.jdbc.Driver\");
isDriverAllowed 才是真正初始化驱动实现类
获取连接getConnection
注意
JDBC加载驱动机制主动使用forName注册
ServiceLoader.load(Driver.class);driversIterator.next();加载而且初始化,next会导致文件里面的指定的SPI实现类被加载初始化
loadInitialDrivers
获取连接
不使用forName注册直接使用驱动管理器获取连接
reload()新建一个延迟迭代器,只有真正迭代获取驱动使用的时候才会forName执行初始化
注意一开始load传递的service是SPI,也就是接口,不是实现类,实现类的名字是放在了第三方驱动的SPI接口命名的文件里面我们会使用加载器的getResource获取每一行的内容
新建的时候设置了类加载器是传递过来的上下文类记载器也就是应用类加载器
load(Class<S> service) 加载一个SPI的实现服务
ServiceLoader的源码
ServiceLoader 打破双亲委派
线程上下文类加载器Thread.currentThread().getContextClassLoader();
A引用B,系统类加载器都不能加载,都使用自定义类加载器加载,不会报错。
A引用B,系统类加载器都能加载,都使用系统类加载器加载,不会报错。
A引用B,系统类加载器不可以加载B,必须使用自定义加载器加载,就会导致报错,只能向上委托父类,不能向下让子类加载
A引用B,如果系统类加载器不能加载A可以加载B的话,A是自定义类加载器加载,B是应用类加载器加载。因为加载B的时候自定义类加载器还是会委托父类先加载B
A引用B,B引用A,系统类加载器不能加载A,可以加载B,报错
全盘委托机制
类加载器:返回Class对象的引用
为什么要打破
打破双亲委派方法
实现热加载
利用命名空间实现加载多个同名包下的同名类
实现
系统类的加载
两个类对象是不是同一个,不仅仅是包名和类名,还要类加载器
总结:
Tomcat打破
2 类加载细节
类加载
大小在类加载之后就可以确定
minor
老年代空间分配担保机制
full GC
大于限制的值直接进入老年代不会尝试在年轻代分配
到达年龄限制的会进入老年代
新生代gc过后存活对象过多无法放入Survivor区域
动态年龄判断
加入老年代的时机
GC触发时机(对象分配的时候,内存不够)
为什么:减轻GC压力
逃逸分析
标量替换
1. 栈上分配
2. 大对象
Eden专属于线程的部分
3. TLAB
指针碰撞(Java堆内存规整)(默认)(使用压缩算法的话)
空闲列表(Java堆内存不规整)(使用标记清除的话)
内存分配方式
对分配内存空间的动作进行同步处理
把内存分配的动作按照线程划分在不同的空间之中进行
线程安全解决
线程逃逸
分配内存
4. Eden分配
内存分配流程
CAS+自旋保证更新操作的原子性,实现内存空间分配的同步处理
TLAB本地线程分配缓冲
并发问题
JVM对象内存分配
分配到的对象的内存空间初始化为0值
为实例赋予正确的初始值
初始化
64位的8字节,32位的4字节。
Mark Word
类指针Klass Pointer
有的话
数组长度
加锁的原理
对象头
数据体
填充字段
对象的内存布局
为什么
指针压缩
设置对象头
类的初始化方法叫做clinit
执行<init>方法
类实例化对象创建过程
年轻代
老年代
为什么分代
为什么8:1:1
空间划分
Java堆中划出一块内存作为句柄池,reference中存储的就是对象的句柄地址还有一个指针指向方法区的Class元数据
1. 使用句柄
reference中存储的直接就是对象地址
对象头指向方法区的Class元数据
2. 直接指向Heap里面的对象(Hotspot使用的)(使用压缩算法的效率更高)
访问方式
对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改
使用句柄访问
访问速度快,节省一次指针定位的的时间开销
直接指针访问
优点
对象的访问定位
堆
操作数栈
原生类型
引用类型
局部变量表
动态连接
方法出口
栈帧
虚拟机栈JVM栈
本地方法栈
程序运行时候标志下一句要执行的位置,线程切换的时候保存上下文以便恢复时候继续执行
程序计数器:(多线程)执行位置,挂起线程上下文恢复
字面量:字符串常量池,在堆
符号引用
运行时常量池
字段信息
方法信息
对应class实例的引用
静态变量每个Class的结构信息
大量的反向代理会创建大量的代理类占用永久代
为什么不要了
永久代
为什么要
移除永久代的影响
元空间内存管理
元空间
方法区
JVM 通过DirectByteBuffer操作直接内存,DirectByteBuffer是在堆上面的,避免了用户和内核的大幅度拷贝
元空间直接内存:Direct Memory
类加载子系统
字节码执行引擎
其余
JVM内存模型运行时数据区按照线程共享和不共享来说
ldc :将int float或者String从常量池推送至栈顶
bipush: 将short单字节-128 - 127从常量池推送到栈顶
sipush 表示将一个短整型常量值 -32768 32767 推送到栈顶
iconst_1 表示将int类型1 推送至栈顶,1-5 是这个规律,大一点就是bipush
anewarray 创建一个引用类型 的数组 引用值加入到栈顶
newarray 创建一个原始类型 的数组,引用值加入到栈顶
1.invokeinterface:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
2.invokestatic:调用静态方法。(解析阶段就可以确定,类加载的时候直接将符号转换为直接引用)
3.invokespecial:调用自己的私有方法、构造方法(<init>)以及父类的方法。(解析阶段就可以确定,类加载的时候直接将符号转换为直接引用)
方法区里的虚方法表vtable
方法区里的接口方法表itable
上面的两个表在虚拟机启动的时候,按照一定的规则去子类找是否有对应的实现,有的话先使用子类的
4.invokevirtual:调用虚方法。运行期动态查找的过程。(多态紧密相关)
5.invokedynamic:动态调用方法。(1.7加进来的,最复杂,jdk是静态的不是动态语言
常见助记符
javap -c Main.class
jclasslib
魔数CAFE BABE:前4个字节,字节码校验,
两个字节00 00 == > 次版本号0
后两个字节 00 34 ==》16*3+4 = 52 ==》jdk8.0低版本依次减1,6就是50
major
主次版本
注意:常量池里面的元素个数是,这里的个数减1 。0 暂时不使用满足某些常量池索引值的数据在特定情况下需要表达不引用任何一个常量池的含义根本在于索引为0也是一个常量(索引常量),只不过不位于常量表中,这个常量对应null值所以常量池的索引从1 而不是0开始
常量数量:紧跟在主版本后面,占据2个字节 eg : 00 18
常量池看做Class文件的资源仓库。供后面的内容引用的
字面量:声明为final的常量值
符号引用:类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符
和netty的编解码类似,head+body
每一个元素的第一个数据都是一个u1类型,该字节是标志位,占据1个字节。JVM在解析常量池的时候,会根据u1类型获取元素是上面的具体的类型(是字符串还是常量普通类型数据,还是类型的索引还是字符串常量的索引还是方法的索引,还是字段的索引,还是接口方法的索引还是;)
常量池不是只放不变的量,变量也在这里一个java类定义的很多信息都是常量池维护的,类的方法和变量的信息。主要存储两类信息:
常量池constant pool(常量池数组)这个数组和普通数组不一样,不同类型的元素占据的空间是不一样的。
16进制表示,2个字节:实际使用是与运算,而不是枚举每一个0021 == 0020+0001是public super。可以调用父类的方法
类的访问标志Access_Flag
2个字节
当前类的名字:类索引
父类的名字:父类索引
接口索引
不包括方法的局部变量,也就是类和接口成员变量(静态、实例变量)
有 Fields 个数,访问标志+字段表:字段的访问标志(修饰符),字段的变量,字段的类型,字段的属性的个数,字段的属性(如果个数不为0的话)
字段表:类的 Fields 成员变量
方法个数 00 03
access_flags 方法 访问标记
name_index 方法名索引--》常量池里面
descriptor_index 描述符 也就是参数以及返回值的索引 --》 常量池里面
属性个数
attribute_name_index 指向常量池的索引值 eg: 00 09 ==》 Code
指向常量池的属性的长度 00 00 00 38 == > 56
Code 属性的内容
info 真正属性的内容
属性表结构
方法表
<init>()V
类的方法
字节码文件自己的属性
结构
字节码解析
字节码
为啥STW
引用计数法
谁可以是GCROOt
可达性分析
finalize()
强引用、软引用、弱引用
强
软
弱
虚
常见GCroot引用类型
对象可回收判断
为什么分代收集
标记复制
标记清除
标记整理
垃圾收集算法(理论)
适合年轻代和老年代
单线程浪费多核,但是也就没有线程切换到开销,有很高的单线程手机效率
复制+标记压缩
老年代的收集器是一个单线程,是CMS的后备方案;1.5之前和Parallel搭配使用
Serial
Serial的多线程版本,线程数等于CPU核数
8的默认年轻代和老年代收集器,小内存没有问题。STW时间有点长
适合对停顿时间不敏感,但是CPU敏感的情况,也就是小机器
CPU中用于运行用户代码的时间与CPU总消耗时间的比值
注重吞吐量(只是为了高效利用CPU)而不是用户线程的停顿时间(提高用户体验)
Parallel
只适合年轻代
默认线程数和CPU核数一致
除了Serial收集器外,只有它能与CMS收集器配合
ParNew
只适合老年代
STW:否则不断有新的是不能完成的
标记变量GC root直接引用对象
速度快
初始标记
内存大的时候,STW显著减少,最耗时的这里占用百分之80,没有STW,所以几乎没有STW
并发标记(耗时但是用户并行)
下次GC
问题:已经标记的对象变成了垃圾,多标记
这个阶段使用三色标记里面的增量更新重新标记
问题:是垃圾又变成复活的了(误删除)
STW
重新标记
清理白色的,这个阶段加进来的直接标记为黑色
并发标记清除,不使用标记压缩是因为需要并发删除
并发清理(耗时但是用户并行)
重置GC标记的数据变成全白
并发重置
流程
STW减少,适合内存大的效果显著,也就是适合对停顿时间敏感的程序,而且CPU充足
获取最短回收停顿时间,(提高用户体验)
浮动垃圾
CPU敏感
分配的空间大于需要的空间,但是留出来的空间又放不下更多的信息
空间碎片
并发失败
缺点
启用。并发GC线程数。
Full GC 之后压缩以及多少次之后压缩
老年代默认到达92就会Full GC,就是为了避免并发失败,系统中大对象多的话需要设置小,可以设置自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
初始标记和重新标记都可以设置线程数
核心参数设置
CMS
区域划分(重要特性)
失效触发full GC
最大停顿时间,可预测的停顿(重要特性)
大对象处理
youngGC触发时机
暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
初始标记STW
并发标记(耗时,要追踪所有的存活对象)
最终标记STW
关注吞吐量和关注延迟之间的最佳平衡。
筛选回收STW
过程
回收算法
young
新生代+部分老年代
mixed
单线程进行标记、清理和压缩整理
只会Serial
full
收集分类
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个 年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent(默认85%)
-XX:G1MixedGCCountTarget
-XX:G1HeapWastePercent
垃圾回收时间长
内存对象较多,可以有效减少回收时间
G1的思路就是边处理业务变收集垃圾,不会一次性清理,所以也会有STAB
场景
G1
JDK 11中新加入的具有实验性质的低延迟垃圾收集器
基于Region内存布局的, 暂时不设分代的, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整理算法的
大、 中、 小三类容量:
小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象,
区域划分
每个CPU对应有一块内存,且这块内存在主板上离这个CPU是最近的,每个CPU优先访问这块内存,那效率自然就提高了:
ZGC是能自动感知NUMA架构并充分利用NUMA架构特性的。
numa-aware
以前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在对象地址指针中。不读取对象,只需要读取内存指针,就可以知道是不是需要回收
每个对象有一个64位指针,低42位寻址,剩下的18不用,最后4位做标记
对象指针必须是64位,那么ZGC就无法支持32位操作系统,同样的也就无法支持压缩指针了(CompressedOops,压缩指针也是32位)。
1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;
1位:Remapped标识,设置此位的值后,对象未指向需要GC的Region集合
1位:Marked1标识;
1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;
一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。
颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。
颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据
颜色指针的三大优势:
颜色指针
在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个Load Barriers。
读屏障
ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
并发标记
ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集
并发预备重分配
把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
并发重分配
修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
并发重映射
三个STW阶段
阶段
浮动垃圾。ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。
难题
ZGC
垃圾收集器(实现)
jdk1.8 默认是Parallel。
选择理论:合适场景使用合适收集器
为什么young GC比full GC快十倍
ParNew + CMS的组合有哪些痛点
如何设置合适的垃圾收集器
黑色:对象所有都扫描(不会重新扫描)。灰色:对象至少有一个没有被扫描过。白色:尚未访问
一开始全是白色,最后还是白色的就是不可达的。
新增的直接是黑的
由于可以并行清除,所以在扫描的时候gcroot可能还在栈里面,扫描之后弹出,这个时候就是多标记黑色,变成浮动垃圾,等待下次GC就好
多标(并发清除)
一开始A引用B,B引用CD。扫描完A之后是黑色,不会再扫描,扫描B到C但是没有扫描D的时候,此时A是黑色,B是灰色,C是黑色,D是白色。由于是并发标记,所以此时用户线程可以将B对D的引用去掉让A指向D,由于A不会再被扫描,所以此时D其实还是白色的,不处理最后会被当垃圾回收
黑色对象并发标记的时候指向白色的时候将新的引用记录下来,并发标记之后黑色对象变为灰色重新扫描一次
并发标记之后,开始重新标记会把新增对象的源头重新标记为灰色,就可以在最后的并发清理的时候不会误删除
写操作后(新的引用放到集合)
增量更新update
老的引用放到一个集合,重新标记的时候将集合里面的全部变成黑色
不会被回收,下次再去回收
写操作前(老的引用放到集合)
原始快照SATB
漏标(并发标记)误删除
只有使用并发标记才会有的问题
不是内存屏障,这里的屏障更像是AOP切面,代码级别的屏障
写的时候是异步写到集合里面的
写屏障
读屏障是直接针对第一步:D d = a.b.d,当读取成员变量时,一律记录下来
读写屏障解决三色并发标记误删除
写屏障+增量更新:也就是在写操作后将新的引用放到一个集合,重新标记的时候重新扫描这个黑色的节点
写屏障+原始快照SATB:也就是写操作前将老的引用放到集合里面,并发标记之后直接变为黑色,下次GC再说
读屏障:在读取一个对象的时候就记录在集合里面,说白了还是类似于写屏障的写操作后
SATB相对于增量更新效率高,不需要在重新标记阶段再次深度扫描被删除引用的对象,CMS对增量引用的根对象会做深度扫描,CMS只有一个老年代扫描代价会小一些。
垃圾回收器并发漏标记解决方案
底层三色标记算法实现原理(GC可达性分析使用的)
跨代引用时候还需要扫描老年代,解决大规模扫描效率低的问题
(概念)记忆集:记录老年代对新生代的引用,这样在minorGC的时候就不需要扫描老年代
(实现)卡表:记忆集的实现形式,底层就是年轻代的一个字节数组,维护老年代的卡页地址,1代表卡页脏,0不脏,使用写屏障实现(赋值 的时候加一个写屏障记录一下)
(实现)卡页:老年代划分最小单位,脏的卡页在GCroot的时候要扫描,卡页里面可能有很多对象,有一个引用年轻代就是脏的。
卡表变脏,即发生引用字段赋值时,更新卡表对应的标识为1。 Hotspot使用写屏障维护卡表状态。
卡表的维护
G 1 里面每一个region都会
记忆集和卡表
方法返回
循环结束
入方法之前
抛出异常的时候
GC不是想做就做,原子操作代码不可以拆开。i++、要更新程序计数器。代码设置线程上一个安全点,标志从0变1,代码设置几个安全点,到达安全点的时候回自动挂起,所有的线程都到达安全点的时候就会开始GC
一段代码中引用关系不会发生变化。在这个区域任意地方开始GC都安全
安全点是针对正在执行的线程,安全区域是针对sleep或者中断的线程
没有安全区域就不能中断JVM运行到安全点。
安全区域
安全点和安全区域
垃圾
-Xms5m -Xmx5m 堆内存,最大堆内存
-XX:+HeapDumpOnOutOfMemoryError
常用参数设置
方法区的自动扩缩容机制
java -Xss512K -Xms2048M -Xmx2048M -Xmn1024M -XX:MetaspaceSize=256M -XX:MaxMetaSpaceSize=256M -jar aaa.jar
-XX:MaxMetaSpaceSize
-XX:MetaSpaceSize
-Xss512K
-XX:PermSize -XX:MaxPermSize
内存参数分配
找到JVM的进程
jps -l 有包名
jps
对象实例个数以及占用内存大小以及类的名字
jmap -histo pid > ./log.txt
堆的各种信息
jmap -heap pid
堆内存dump
也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)
可以用jvisualvm命令工具导入该dump文件分析
OQL 对象查询语言
jhat
jmap 类加载器,堆里面的对象信息,
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
评估程序内存使用及GC压力整体情况
S0C:第一个幸存区的大小,单位KB;; S1C:第二个幸存区的大小S0U:第一个幸存区的使用大小;;S1U:第二个幸存区的使用大小EC:伊甸园区的大小 EU:伊甸园区的使用大小OC:老年代大小 OU:老年代使用大小MC:方法区大小(元空间) MU:方法区使用大小CCSC:压缩类空间大小 CCSU:压缩类空间使用大小YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间,单位sFGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间,单位sGCT:垃圾回收消耗总时间,单位s
jstat -gc pid 最常用
堆内存统计
NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量 S0C:第一个幸存区大小 S1C:第二个幸存区的大小 EC:伊甸园区的大小 OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:当前老年代大小 OC:当前老年代大小 MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小 CCSMN:最小压缩类空间大小 CCSMX:最大压缩类空间大小 CCSC:当前压缩类空间大小 YGC:年轻代gc次数 FGC:老年代GC次数
jstat -gccapacity pid
其余新生代老年代元空间的大小以及使用大小以及回收的次数,回收的时间都可以统计
年轻代对象增长的速率
Young GC的触发频率和每次耗时
每次Young GC后有多少对象存活和进入老年代
Full GC的触发频率和每次耗时
情况预估
年轻代对象增长速率
young gc的触发频率和每次耗时
每次young gc后有多少对象存活和加入老年代
full gc 的频率和每次的 耗时
jvm 运行情况预估
jstat
堆栈信息
最后会有死锁信息
还可以用jvisualvm自动检测死锁
用jstack加进程id查找死锁
使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
按H,获取每个线程的内存情况
,找到内存和cpu占用最高的线程tid,比如19664
转为十六进制得到 0x4cd0,此为线程id的十六进制表示
执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调 用方法
查看对应的堆栈信息找出可能存在问题的代码
jstack找出占用cpu内存最高的线程堆栈信息
jstack pid线程相关的信息
* 1.jcmd pid VM.flags:查看JVM的启动参数;
* 2.jcmd pid help:列出当前运行的Java进程可以执行的操作;
* 3.jcmd pid help JDR.dump:查看具体命令的选项;
* 4.jcmd pid PerfCounter.print:查看JVM性能相关的参数;
* 5.jcmd pid VM.uptime:查看JVM的启动时长;
* 6.jcmd pid GC.class_histogram:查看系统中类的统计信息;
* 7.jcmd pid Thread.print:查看线程的堆栈信息;
* 8.jcmd pid GC.heap_dump filename:导出Heap dump文件,导出的文件可以通过jvisualvm查看;
* 9.jcmd pid VM.system_properties:查看JVM的属性信息;
* 10.jcmd pid VM.version:查看目标JVM进程的版本信息;
* 11.jcmd pid VM.command_line:查看JVM启动的命令行参数信息。
jcmd(1.7开始)
jmc--java mission control
查看正在运行的Java应用程序的扩展参数
查看jvm的参数
jinfo -flags pid
查看java系统参数
jinfo -sysprops pid
Jinfo
jconsole
-XX:+HeapDumpOnOutOfMemoryError 会在项目所在的目录生成hprof文件
导入之后--概要可以看到堆转储上的线程--切换到类标签查看堆里面的对象大小分布
jvisualVM
https://arthas.aliyun.com/doc/quick-start.html
dashboard
jad 全类名 可以反编译
thread -b 检查死锁
Arthas
运行过程中的gc日志全部打印出来,然后分析gc日志得到关键性指标,分析 GC原因,调优JVM参数。
‐Xloggc:./gc‐%t.log‐XX:+PrintGCDetails‐XX:+PrintGCDateStamps‐XX:+PrintGCTimeStamps‐XX:+PrintGCCause‐XX:+UseGCLogFileRotation‐XX:NumberOfGCLogFiles=10‐XX:GCLogFileSize=100M
在JVM参数里增加参数,%t 代表时间
打印的日志包括JVM的参数以及当前GC的发生时间,前后每一个空间的情况,以及耗时,
年轻代GC
老年代GC
GC日志详解与调优
先给自己的系统设置一些初始性的 JVM参数,堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
事先启动一个web应用程序,用jps查看其进程id,接着用各种jdk自带命令优化应用
一般的调优就是调节full gc
分配速率:影响ygc频率
解决
过早提升:影响fgc频率
分析思路,解决方法
多级缓存的时候会使用本地缓存,不回收一直往里面放,堆积老年代。出问题
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。
内存泄漏
堆溢出
递归调用栈溢出
元空间溢出
OOM
让短期存活的对象 尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc。尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年 代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。同时给系统充足的内存大小,避免新生代频繁的进行垃 圾回收。
高并发系统的对象基本就会朝生夕死,这种情况就是让年轻代大一点,这样就不会加入老年代,基本就不会full GC
如果一个订单系统每秒60M对象,1s之后变为垃圾对象,Eden满之后再加入,前一秒的60M没有过期,加入Survivor
初始设置:java -Xms3072M -Xmx3072M -Xss1M -XX:MetaSpaceSize=512M -XX:MaxMetaSpaceSize=512M -jar a.jar
默认的老年代是占用2/3的堆大小,目的就是为了对象尽量在年轻代被回收 不要进入老年代。由于动态年龄判断机制,所以Survivor超过百分之五十直接进入老年代。最后积累起来full GC
调整设置:java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaSpaceSize=256M -XX:MaxMetaSpaceSize=256M -jar a.jar
相当于Survivor一个有200M,过来的60M不会进入老年代,下次GC早就过期了
动态年龄机制导致的full GC
JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),如果内存较大(超过4个G,只是经验 值),系统对停顿时间比较敏感,我们可以使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)
对于8G内存,我们一般是分配4G内存给JVM
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
可以适当将年龄阈值变小,留出来更多的空间给Survivor区域。长久使用的对象直接进入老年代
更小的年龄阈值(更大的Survivor)
这些对象一般就是你系统初始 化分配的缓存对象,比如大的缓存List,Map之类的对象。
设置合理的大小让对象直接进入老年代
很少有超过1M的对象
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC‐XX:CMSInitiatingOccupancyFraction=92 ‐XX:+UseCMSCompactAtFullCollection ‐XX:CMSFullGCsBeforeCompaction=0
堆大小,年轻代大小,元空间的大小,Eden区的大小,最大年龄阈值,大对象的阈值,使用ParNew和CMS,设置CMS full GC的占用比例,使用CMS压缩
ParNew+CMS
大致算下来每天会发生70多次Full GC,平均每小时3次,每次Full GC在400毫秒左右;每天会发生1000多次Young GC,每分钟会发生1次,每次Young GC在50毫秒左右。
-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly
年轻代变大,CMS老年代可使用的百分比变大,
-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
1、元空间不够导致的多余full gc
2、显示调用System.gc()造成多余的full gc,这种一般线上尽量通过XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果
3、老年代空间分配担保机制
,full gc的次数比minor gc的次数还多了
对象动态年龄判断机制
jmap命令大概看下是什么对象
jmap -histo pid
确定对象被频繁创建的位置
大量的对象频繁的被挪动到老年
jstack或jvisualvm来定位cpu使用较高的代码
找到CPU较高的位置
系统频繁full gc 导致系统卡顿
parnew+cms的gc,如何保证只做ygc,jvm参数如何配置?
大内存机器的新生代GC过慢的问题
频繁老年代gc问题
案例实战
JVM 调优
字面量
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
三种常量
这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装 入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也 就是我们说的动态链接了。例如,compute()这个符号引用在运行时就会被转变为compute()方法具体代码在内存中的 地址,主要通过对象头里的类型指针去转换直接引用。
Class常量池与运行时常量池
常量池一般会被频繁创建和销毁,我们开辟一个类似缓存区的地方
创建字符串常量时,首先查询字符串常量池是否存在该字符串,存在就直接返回实例,不存在就实例化放到池中
String s=\"zhuge\"; // s指向常量池中的引用
创建的对象只会在常量池里面
JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象 如果有,则直接返回该对象在常量池中的引用; 如果没有,则会在常量池中创建一个新对象,再返回引用。
直接赋值字符串
会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
先去字符串常量池检查是否存在字面量,不存在的话就先在常量池创建一个,之后去内存中创建一个字符串对象。存在的话直接去堆内存 中创建一个字符串对象\"deltaqin\"
最后,将内存中的引用返回。
String s1=new String(\"deltaqin\"); // s1指向内存中的对象引用
String s1=new String(\"zhuge\");String s2=s1.intern();System.out.println(s1==s2); //false
String中的intern方法是一个 native 的方法, 当调用 intern方法时, 如果池已经包含一个等于此String对象的字符串 (用equals(oject)方法确定),则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)。
intern方法
JDK7及以上
Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里
Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里
位置
底层是hotspot的C++实现的,底层类似一个 HashTable, 保存的本质上是字符串对象的引用。
在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字 符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新 创建的实例。
在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符 串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
设计原理
当一个字 符串由多个字符串常量连接而成时,它自己肯定也是字符串常量
JVM对于字符串常量的\"+\"号连接,将在程序编译期,JVM就将常量字符串的\"+\"连接优化为连接后的值
String s2=\"zhu\"+\"ge\";
有new就无法在编译期执行,所以会在运行时创建一个新的对象
String s2=\"zhu\"+new String(\"ge\");
由于在字符串的\"+\"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,无法被编译器优化。只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为 false。
编译时候的字节码会转换为StringBuilder的append方式
String a=\"ab\";String bb=\"b\";String b = \"a\"+bb;// a==b false
对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的\"a\" + bb和\"a\" + \"b\"效果是一样的。
String a=\"ab\";final String bb=\"b\";String b = \"a\"+bb;// a==b true
JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和\"a\"来动态 连接并分配地址为b,故上面 程序的结果为false。
String a=\"ab\";final String bb=getBB();String b = \"a\"+bb;private String getB(){ return \"B\";}// a==b false
StringBuilder的toString方法会new String()
常见案例
字符串常量池
八种基本类型的包装类和对象池
常量池
堆(Heap)
方法区(Method Area)
程序计数器(Program Counter Register)
局部变量
运算
多态
栈(Stacks
JVM数据区内存模型
主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互引用的问题
引用计数算法
虚拟机栈中(栈帧中)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(native方法)引用的对象。
可作为GC Roots的对象
可达性分析算法
判断算法
标记回收算法
复制算法
标记压缩算法
分代算法
GC基本算法
客户端
Serial收集器(复制算法)
Serial Old收集器(标记-整理算法)
多核
ParNew收集器(停止-复制算法)
多核+大吞吐量
Parallel Scavenge收集器(停止-复制算法)
Parallel Old收集器(标记-整理算法)
标记老年代中所有的GC Roots能直接关联到的对象。
初始标记(STW initial mark)
标记老年代中所有GC Roots可达的对象
并发标记(Concurrent marking)
标记引用发生了变化的对象;
并发预清理(Concurrent pre-cleaning)
标记老年代中所有存活的对象;
重新标记(STW remark)
并发清理(Concurrent sweeping)
并发重置(Concurrent reset)
流程
CMS收集器对CPU资源非常敏感
CMS收集器无法处理浮动垃圾
停顿时间是不可预期
多核+低停顿=响应速度优先
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
young GC
初始标记过程,整个过程STW,标记了从GC Root可达的对象
initial mark
:并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
concurrent marking
最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
remark
垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中
clean up
执行Mixed GC的时机
Mixed GC
Full GC
核心思想:空间换时间
引入了分区的思路,弱化了分代的概念
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集
停顿时间是可控的,可避免雪崩现象
能充分利用客户给我们的资源,减少停顿时间
面向服务端,响应速度优先
G1标记整理+复制
ZGC是一种并发的、不分代的、基于Region且支持NUMA的压缩收集器
初始标记,再标记,初始转移
垃圾回收器
大对象
GC年龄满足条件(默认是15,最大也是15)
动态对象年龄判定
ycg 存活的对象不能在另一个Survivor 完全容纳,则会通过担保机制进入老年代。
进入老年代的条件
Eden内存不足
为TLAB分配内存
YGC
年老代(Tenured)被写满;
YGC晋升总内存大于剩余内存
持久代(Perm)被写满;
System.gc()被显示调用;
上一次GC之后Heap的各域分配策略动态变化;
老年代空间担保机制
GC触发场景
CMS收集器导致碎片化严重,导致老年代没有足够的空间用于担保的。进而导致CMSgc失败,系统使用了serial old收集器。然后这个收集器是使用标记整理的算法,耗时长。
CMS六个阶段中重新标记的时间长
fullGC时间变长的原因
jvm优化经验
参数:-XX:+HeapDumpOnOutOfMemoryError
Java堆溢出
栈容量由-Xss 参数设定
虚拟机栈和本地栈溢出
通过-XX:PermSize和-XX:MaxPermSize限制方法区大小
方法区和运行时常量池溢出
通过-XX:MaxDirectMemorySize指定
本机直接内存溢出
OutOfMemoryError异常
GC
查找并加载类的二进制数据;
装载
确保被加载类信息符合JVM规范、没有安全方面的问题。
验证
为类的静态变量分配内存,并将其初始化为默认值。
把虚拟机常量池中的符号引用(因为在编译的时候并不知道真正的内存地址,)转换为直接引用。
链接
为类的静态变量赋予正确的初始值
类加载过程
根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类
为什么需要
怎么实现
为什么打破
怎么打破
双亲委派
类加载器
虚拟机遇到一条new指令,首先确保类加载过程已经完成
指针碰撞(Java堆内存规整)
空闲列表(Java堆内存不规整)
初始化零值
<init>方法
对象创建
哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
自身运行时数据(Mark Word)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定是哪个类的实例
类型指针(一般是4个字节)
对象真正存储的有效信息
实例数据
并不是必然存在的,也没有特别的含义,仅仅骑着占位符的作用
对齐填充(8个字节对齐)
对象所占内存计
类加载与对象创建
文件生成和分析
魔数
常量池和常量表
指令详解
JIT编译器
jstack导出栈信息
线程
异常报错OOM
看日志
-Xms
-Xmx
指定合适的容量
-XX:+HeapDumpOnOutOfMemorryError -XX:HeapDumpPath=/data/log/
压缩之后拉取下来放到MAT分析内存泄漏
查看stack信息找到调用栈的代码
OOM的时候自动dump内存快照
jmap -heap 查询堆区域的统计信息
jmap -histo 查看大对象
jamp -dump导出内存镜像
利用jmap查询jvm的内存使用量
内存快照
指定合适的垃圾收集器
利用 -XX:+PrintGCDetails -Xloggc:/tmp/gc.txt打印GC日志
jstat -gcutil动态查看GC的情况
1000表示采样间隔(ms),S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU分别代表两个Survivor区、Eden区、老年代、元数据区的容量和使用量
jstat -gc pid 1000
频繁GC
ps -ef | grep java 或jps命令,找出服务器的所有java进程
top -H -p pid 查看线程情况
使用top命令,查看占用cpu的进程的pid
找出CPU耗用最厉害的进程pid
将获取到的线程号转换成16进制:printf \"%x\\" pid
导出线程栈:jstack pid > pid.tdump
排查java进程cpu100%的问题
java应用线上问题排查
JVM
JVM调优
0 条评论
回复 删除
下一页