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