JVM实践与应用
- 1.类加载器(加载、连接、初始化)
- 1.1 类加载要完成的功能
- 1.2 加载类的方式
- 1.3 类加载器
- 1.4 双亲委派模型
- 1.5自定义ClassLoader
- 1.6 破坏双亲委派模型
- 2.1 类连接主要验证内容
- 2.2 类连接中的解析
- 2.3 类的初始化
- 3.1 类的初始化时机
- 3.2 类的初始化机制和顺序
- 3.2 类的卸载
- 2. 内存分配
- 2.1 JVM简化架构
- 2.2 栈、堆、方法区交互关系
- 3.1 Java堆内存模型和分配
- 3.2 对象的内存布局
- 3.3 对象的访问定位
- 4.1 内存分配的参数
- 3. 字节码执行引擎
- 3.1 栈帧
- 3.2 分派
- 4. 垃圾回收
- 4.1 什么是垃圾
- 4.2 ZGC收集器
- 4.3 GC性能指标
- 4.4 JVM内存配置原则
- 4.5 垃圾收集器
- 4.6 引用分类
- 4.7 跨代引用
- 4.8 GC类型
- 4.9 Stop-The-World
- 4.10 垃圾收集器类型
- 4.11 判断类无用的条件
- 4.12 垃圾回收算法
- 4.13 垃圾收集器基础和串行收集器
- 5.高效并发
- 5.1 Java内存模型、内存间的交互操作
- 5.2 多线程的可见性、有序性和指令重排、线程安全的处理方法
- 5.3 锁优化:自旋锁、锁消除、锁粗化、轻量级锁、偏向锁等
- 5.4 JVM中获取锁的步骤
- 5.5 同步代码的基本规则
- 6.性能监控与故障处理工具
- 6.1 命令行工具
- 6.2 图形化工具
- 6.3 两种连接方式
- 6.4 JVM监控工具的作用
- 6.5 监控与故障处理实战
- 7. JVM调优
- 7.1 调什么
- 7.2 如何调优
- 7.3 JVM调优的目标
- 7.4 常见调优策略
- 7.5 JVM调优冷思考
- 7.6 JVM调优经验
- 7.7 内存泄漏
- 7.8 调优实战
- 8. 加油站
- 8.1 字节码部分
- 8.2 内存分配
- 8.3 监控工具和实战
- 9.梳理jvm知识体系
- 9.1 地毯式总结jvm知识
- 9.2 后续学习方法
1.类加载器(加载、连接、初始化)
- 加载: 加载类的二进制数据
- 连接: 类的二进制数据合并到虚拟机运行时区。
- 验证: 验证类信息正确性,保证安全。
- 准备:为类的静态变量分配内存,并初始化它们。
- 解析: 把常量池中的符号引用转为直接引用。
- 初始化:为类的静态变量赋初始值。
1.1 类加载要完成的功能
- 通过类的全限定名来获取改类的二进制流
- 把二进制的字节流转化为方法区的运行时数据结构
- 在堆上创建一个java.lang.Class对象,用来封装类在方法区的数据结构,并向外提供访问方法区的内数据结构的接口。
1.2 加载类的方式
- 最常见的方式:本地文件系统中加载、从jar等归档文件中加载
- 动态的方式:将java源文件动态编译成class
- 其它方式:网络下载、从专有数据库中加载等等
1.3 类加载器
Java虚拟机自带的加载器包括如下几种:
启动类加载器(BootstrapClassLoader))
平台类加载器(PlatformClassLoader) | 扩展类加载器(ExtensionClassLoader)
应用程序类加载器(AppClassLoader)
用户自定义的加载器,是java.lang.ClassLoader的子类
用户可以定制类的加载方式;只不过自定义类加载器其加载
的顺序是在所有系统类加载器的最后
- 启动类加载器:
用于加载启动的基础模块类,比如:java.base、java.management、.java.xml等等
Java8的启动类加载器加载 <java_home>/lib,或者-XbootClasspath参数指定的,且是虚拟机能够识别的类库加载到内存中(按名字识别,比如rt.jar,对于不能识别的文件不予加载)
- 平台类加载器:
用于加载一些平台相关的模块,比如:
java.scripting、java.compiler*、java.corba*等等
JDK8:扩展类加载器:负责加载<JRE_HOME>/lib/ext,
或者java.ext.dirs系统变量所指定路径中的所有类库
- 应用程序加载器
JDK8:应用程序类加载器:负责加载classpath.路径中的所
有类库
说明:
Java程序不能直接引用启动类加载器,直接设置
classLoader为null,默认就使用启动类加载器
类加载器并不需要等到某个类“首次主动使用”的时候才
加载它,规范允许类加载器在预料到某个类将要被使用
的时候就预先加载它
如果在加载的时候.class文件缺失,会在该类首次主动使用时
报告LinkageError错误,如果一直没有被使用,就不会报错
1.4 双亲委派模型
JVM中的ClassLoaderi通常采用双亲委派模型,要求除了启
动类加载器外,其余的类加载器都应该有自己的父级加载器
这里的父子关系是组合而不是继承,工作过程如下:
- jdk8先委派给父加载器,递归加载
- 如果没有加载到,则使用自己的加载器加载。
1.5自定义ClassLoader
如果没有指定父加载器,默认就是启动加载器。
1.6 破坏双亲委派模型
双亲委派模型有个问题:父加载器无法向下识别子加载器加载的资源
为了解决这个问题,引入了线程上下文类加载器,可以通过Thread的setContextClassLoader()进行设置。
另外一种典型情况就是实现热替换,比如OSGI的模块化热部署,它的类加载器就不再是严格遵守双亲委派模型,很多可能就在平级的类加载器中执行了。
2.1 类连接主要验证内容
- 类文件结构检查:按照jVM规范规定的类文件结构进行
- 元数据验证:对字节码描述的信息进行语义分析,保证其符合java语言规范要求
- 字节码验证:通过对数据流和控制流进行分析,确保程序语义是合法和符合逻辑的。这里主要是对方法体进行校验。
- 符号引用验证:对类自身以外的信息,也就是常量池中的各种符号引用,进行匹配校验
2.2 类连接中的解析
所谓解析就是把常量池中的符号引用转换成直接引用的过程,包括:符号引用:以一组无歧义的符号来描述所引用的目标,与虚拟机的实现无关。
直接引用:直接指向目标的指针、相对偏移量、或是能间接定位到目标的句柄,是和虚拟机实现相关的。
主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。
2.3 类的初始化
类的初始化就是为类的静态变量赋初始值,或者说是执行类构造器方法的过程
- 如果类还没有加载和连接,就先加载和连接
- 如果类存在父类,且父类没有初始化,就先初始化父类
- 如果类中存在初始化语句,就依次执行这些初始化语句.
- 如果是接口的话:
a、初始化一个类的时候,并不会先初始化它实现的接口
b、初始化一个接口时,并不会初始化它的父接口
c、只有当程序首次使用接口里面的变量或者是调用接口方法的时候,才会导致接口初始化
3.1 类的初始化时机
Jva程序对类的使用方式分成:主动使用和被动使用,JVM必须在每个类或接口“首次主动使用”时才初始化它们;被动使用类不会导致类的初始化,主动使用的情况:
- 创建类实例
- 访问某个类或接口的静态变量
- 调用类的静态方法
- 反射某个类
- 初始化某个类的子类,而类还没有初始化
- JVM启动的时候运行的主类
- 定义了default方法的接口,当接口实现类初始化时
3.2 类的初始化机制和顺序
通过子类引用父类的静态字段不会导致子类的初始化
通过数组定义引用类,不会触发类的初始化
调用常量不会引发类的初始化
public class MyParent {
public static String parentStr="now in MyParent!";
static {
System.out.println("my parent class init");
}
}
public class MyChild extends MyParent {
static {
System.out.println("MyChild class init");
}
static {
System.out.println("my child static block 222");
}
public static int a =5;
static {
System.out.println("my child static block 333==" + a);
}
public static void t2() {
System.out.println("now in mychild t2()");
}
}
public class Test{
public static void main(String[]args){
// 子类引用父类的字符串不会导致子类的初始化
System.out.println("Mychild.parentStr = " + MyChild.parentStr);
}
}
3.2 类的卸载
当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载。
JVM自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的
2. 内存分配
2.1 JVM简化架构
运行时数据区
包括:程序计数器、虚拟机栈、本地方法栈;Java堆、方法区
- 程序计数器
每个线程拥有一个寄存器,是线程私有的,用来存储指向下一条指令的地址
在创建线程的时候,创建相应的程序计数器
执行本地方法时,程序计数器的值为undefined
是一块较小的内存空间,是唯一个在jvm规范中没有规定OutOfMemoryError的内存区域
- Java栈
1.栈是由一系列帧Frame组成(因此Java栈也叫帧栈),是线程私有的
2. 帧用来保存方法的局部变量表、操作数栈、动态连接、方法出口
3. 每一次方法调用创建一个帧,并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。
4. 局部变量表存放了编译器可知的各种基本数据类型和引用类型,每个slot存放32位的数据,long、double占两个槽位。
5. 栈的优点:
存取速度比堆快,仅次于寄存器
6. 栈的缺点:
存在栈中的数据大小、生存期是在编译期决定的,缺乏灵活性
- Java堆
用来存放应用系统创建的对象和数组,所有线程共享Java堆
GC主要管理堆空间,对分代GC来说,堆也是分代的
堆的优点:运行期动态分配内存大小、自动进行垃圾回收;
堆的缺点:效率相对较慢
- 方法区
方法区是线程共享的,通常用来保存装载的类的结构信息
通常和元空间关联在一起,但具体的跟JVM实现和版本有关
JVM规范把方法区描述为堆的一个逻辑部分,但它有一个别名为Non-heap非堆,应是为了与Java堆区分开
- 运行时常量池
- 是Class文件中每个类或接口的常量池表,在运行期间的表示形式,通常包括:类的版本、字段、方法、接口等信息
- 方法区中分配
- 通常在加载类和接口到JVM后,就创建相应的运行时常量池
- 本地方法栈
在JVM中用来支持native方法执行的栈就是本地方法栈
2.2 栈、堆、方法区交互关系
3.1 Java堆内存模型和分配
Java堆用来存放应用系统创建的对象和数组,所有线程共享Java堆;
Java堆是在运行期动态分配内存大小,自动进行垃圾回收;
Java垃圾回收(GC)主要就是回收堆内存,对分代GC来说,堆也是分代的.
新生代用来放新分配的对象;新生代中经过垃圾回收,没有回收掉的对象,被复制到老年代
老年代存储对象比新生代存储对象的年龄大得多;
老年代存储一些大对象;
总结:
整个堆大小 = 新生代+老年代
新生代 = Eden+存活区从前的持久代,用来存放Class、Method等元信息的区域,从JDK8开始去掉了,取而代之的是元空间(MetaSpace), 元空间并不在虚拟机里面,而是直接使用本地内存
3.2 对象的内存布局
- 对象在内存中存储的布局(这里以HotSpot虚拟机为例来说明),分为:对象头、实例数据和对齐填充 。
- 对象头,包含两个部分:
- Mark Word: 存储对象自身的运行状态,如:hashcode,GC分代年龄,锁状态标志等
- 类型指针:对象指向它的类元数据的指针
- 实例数据
- 真正存放对象实例数据的地方
- 对其填充
- 这部分不一定存在,也没有什么特殊含义,仅仅是占位符。因为HotSpot要求对象其实 地址都是8字节的整数倍,如果不是,就对齐。
3.3 对象的访问定位
- 对象的访问定位
在VM规范中只规定了reference类型是一个指向对象的引用,但没有规定这个引用具体如何去定位、访问堆中对象的具体位置 - 因此对象的访问方式取决于JVM的实现,目前主流的有:使用句柄或使用指针两种方式。
- 使用句柄:Java堆中会划分出一块内存来做为句柄池,referencer中存储句柄的地址,句柄中存储对象的实例数据和类元数据的地址,如下图所示
- 使用指针:Java堆中会存放访问类元数据的地址reference存储的就直接是对象的地址,如下图所示.
4.1 内存分配的参数
- Trace跟踪参数
- -Xlog:gc 可以打印GC的简要信息
- -Xlog:gc* 打印GC详细信息
- -Xlog:gc:garbage-collection.log 以文件输出
- -Xlog:gc+heap=debug 每一次GC后,都打印堆信息
- GC日志格式
- GC发生的时间,也就是JVM启动以来经过的秒数
- 日志级别信息,和日志类型标记
- GC识别号
- GC类型和说明GC的原因
- 容量:GC前的容量->GC后的容量(该区域总容量)
- GC持续时间,单位秒
- Java堆的参数
- -Xms: 初始堆大小,默认物理内存的1/64,13要求是1024的倍数,1M1M的,大于1M
- -Xmx: 最大堆大小,默认物理内存的1/4
- -Xmn: 新生代大小,默认是整个堆的3/8 【推荐:新生代是整堆的25%-50%】
- -XX: NewSize 设置新生代 -XX:MaxNewSize 设置新生代最大size 或者使用-Xmn
- -XX:+HeapDumpOnOutOfMemoryError OOM时导出堆到文件
- -XX: HeapDumpPath 导出OOM的路径
- -XX:NewRatio 老年代与新生代的比值,如果xms=xmx,且设置了xmn的情况下,该参数不用设置
- -XX: SurvivorRatio: Eden区和Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个survivor占整个新生的1/10.
- -XX: OnOutOfMemoryError 在OOM时,执行一个脚本
- Java栈的参数
- -Xss 通常只有几百k,决定 了函数调用的深度
- 元空间的参数
- -XX:MetaspaceSize 初始空间大小
- -XX:MaxMetaspaceSize 最大空间,默认是没有限制的
- -XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空间容量的百分比
- -XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空间容量的百分比
-Xms1M -Xmx2M -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\GBase8s_WorkDir\GEM_mon\monitor-alarm\dd.hprof
3. 字节码执行引擎
- JVM的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果;
- 实现方式可能有通过解释器直接解释执行字节码,或者是通过即时编译器产生本地代码,也就是编译执行,当然也可能两者皆有。
3.1 栈帧
- 栈帧是用于支持JVM进行方法调用和方法执行的数据结构
- 栈帧随着方法调用而创建,随着方法结束而销毁
- 栈帧里面存储了方法的局部变量、操作数栈、动态连接、方法返回地址等信息。
- 局部变量表
- 用来存放方法参数和方法内部定义的局部变量的存储空间
- 以变量槽slot为单位,目前一个slot存放32位以内的数据类型
- 对于64位的数据占2个slot
- 对于实例方法,第0位slot存存放的是this,然后从1到n,依次分配给参数列表
- 然后根据方法体内部定义的变量顺序和作用域来分配slot
- slot是复用的,以节省栈帧的空间,这种设计可能影响到系统的垃圾收集行为
public static void main(String[]args){
{
byte[] bs = new byte[1 * 1024 * 1024]; // 1M
bs = null; // 也可以导致堆内存回收。
}
// bs的插槽被a变量复用了,那么堆里的bs对象就会被回收。
int a = 5;
System.gc();
System.out.println("totalMemory=" +
Runtime.getRuntime().totalMemory() /1024.0/1024.0);
System.out.println("freeMemory=" +
Runtime.getRuntime().freeMemory() /1024.0/1024.0);
System.out.println("totalMemory=" +
Runtime.getRuntime().maxMemory() /1024.0/1024.0);
}
- 操作数栈
- 用来存放方法运行期间,各个指令操作的数据。
- 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配。
- 虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠区域,以存放共用的数据。
- 动态链接
- 每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程的动态连接。
- 静态解析:类加载的时候,符号引用就转化成直接引用
- 动态连接:运行期间转换为直接引用。
- 方法返回地址
- 方法执行后返回的地址
- 方法调用
- 方法调用就是确定具体调用哪一个方法,并不涉及方法内部的执行过程。
- 部分方法是直接在类加载的解析阶段,就确定了直接引用关系。
- 私有方法
- 静态方法
- 实例构造器
- 父类方法
- 但是对于实例方法,也称虚方法,因为重载和多态,需要运行期动态委派。
3.2 分派
- 分派:分成静态分派和动态分派
- 静态分派:所有依赖静态类型来定位方法执行版本的分派方式,比如:重载方法
- 动态分派:根据运行期的实际类型来定位方法执行版本的分派方式,比如:覆盖方法
单分派和多分派:就是按照分派思考的维度,多于一个的就是多分派,只有一个的称为单分派。
如何执行方法中的字节码指令:JVM通过基于栈的字节码解释执行引擎来执行指令,JVM的指令集也是基于栈的。
4. 垃圾回收
4.1 什么是垃圾
- 简单说就是内存中已经不再被使用到的内存空间就是垃圾
- 引用计数法:给对象添加一个引用计数器,有访问就加1,引用失效就减1
- 优点:实现简单,效率高。
- 缺点:不能解决对象之间循环引用问题。
- 根搜索算法
- 从根(GC Roots)节点向下搜索对象节点,搜索走过的路称为引用链,当一个对象到根之间没有连通的话,则该对象不可用。
- 可作为GCRoots的对象包括:虚拟机栈(栈帧局部变量)中引用的对象、方法区类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。
- HotSpot使用了一组叫做OopMap的数据结构达到准确式GC的目的。
- 在OopMap的协助下,JVM可以很快的做完GC ROOTs枚举。但是JVM并没有为每一条指令生成一个OopMap
- 记录OopMap的这些“特殊位置”被称为安全点,即当前线程执行到安全点后才允许暂停进行GC。
- 如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方GC都是安全的,那么这个区域就称为安全区域。
4.2 ZGC收集器
- JDK11加入的具有实验性质的低延迟收集器
- ZGC的设计目标是: 支持TB级别内存容量,暂停时间低(< 10ms),对整个程序吞吐量的影响小于15%.
- ZGC里面的技术:着色指针和读屏障
- 在指针里面存放一些信息的指针叫做着色指针
- 程序读取内存的数据之前 会执行一些操作,然后将这个地址返回回去的技术就是读屏障
4.3 GC性能指标
- 吞吐量 = 应用代码执行的时间/运行的总时间(越大)
- GC负荷,与吞吐量相反,是GC时间/运行的总时间(越小)
- 暂停时间,就是发生Stop-the-world的总时间(越小)
- GC频率,就是GC一个时间段内发生的次数(越小)
- 反应速度,就是从对象称为垃圾到被回收的时间
- 交互式应用通常希望暂停时间越少越好
4.4 JVM内存配置原则
- 新生代尽可能设置大点,如果太小会导致
- YGC次数更加频繁
- 可能会导致YGC后的对象进入老年代,如果此时老年代满了,会触发FGC(3/8 – 1/2)
- 对老年代,针对响应时间优先的应用:由于老年代通常采用并发收集器,因此其大小要综合考虑并发量和并发持续时间等参数。
- 如果设置小了,可能会造成内存碎片,高回收频率会导致应用暂停
- 如果设置大了,会需要较长的回收时间
- 对老年代,针对吞吐量优先的应用:通常设置较大的新生代和较小的老年代,这样可以尽可能回收大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象。
- 依据对象的存货周期进行分类,对象优先在新生代分配,长时间存活的对象进入老年代
- 根据不同代的特点,选取合适的收集算法:少量对象存活,适合复制算法,大量对象存活,适合标记清除或标记整理。
4.5 垃圾收集器
- 串行收集器
- 并行收集器
- 理解新生代Parallel Scavenge收集器、理解CMS、理解G1
- 理解GC性能指标和JVM内存配置原则
4.6 引用分类
- 强引用:类似Object a = new A()这样的,不会被回收
- 软引用:还有用但并不是必须的对象。用SoftReference来实现软引用
- 弱引用: 只要发生gc,弱引用对象就会被回收。
- 虚引用:也称幽灵引用或幻影引用,是最弱的引用。垃圾回收会回收掉。用PhantomReference来实现虚引用【结合引用队列使用,可以替代finalizer,在DirectByteBuffer中用来回收堆外内存。】
4.7 跨代引用
-
就是一个代中的对象引用另外一个代中的对象
-
新生代-> 老年代
-
跨代引用假说:跨代引用对于同代引用来说只是极少数
-
隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的。
为了解决跨代扫描,引入了新的数据结构【记忆集RememberedSet】:一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。(空间换时间)- 字长精度:每个记录精确到一个机器字长,该字包含跨代指针
- 对象精度:每个记录精确到一个对象,该对象有字段含有跨代指针
- 卡精度:每个精度精确到一块内存区域,该区域内有对象含有跨代指针
- 卡表(card table):是记忆集的一种具体实现,定义了记忆集的记录精度和堆内存的映射关系等。
- 卡表的每个元素都对应着其标识的内存区域中的一块特定大小的内存块,这个内存块称为卡页(Card Page)。
写屏障
写屏障可以看做是JVM对“引用类型字段赋值”这个动作的AOP
通过写屏障来实现对象状态改变后,维护卡表状态。 -
判断是否垃圾的步骤
- 根搜索算法判断不可用
- 看是否有必要执行finalize方法
- 两个步骤走完后对象仍然没有人使用,那就属于垃圾
4.8 GC类型
- MinorGC/YoungGC: 发生在新生代的收集动作
- MajorGC/OldGC: 发生在老年代的GC,目前只有CMS收集器会有单独收集老年代的行为
- MixedGC:收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为。
- FullGC: 收集整个Java堆和方法区的GC
4.9 Stop-The-World
STW是Java中一种全局暂停的现象,多半由于GC引起。所谓全局暂停,就是所有Java代码停止运行,
native代码可以执行,但不能和jvm交互。
其危害是长时间服务停止,没有响应;对于HA系统,可能引起主备切换,严重危害生产环境。
4.10 垃圾收集器类型
- 串行收集:GC单线程内存回收、会暂停所有的用户线程,如:Serial
- 并行收集:多个 GC线程并发工作,此时用户线程是暂停的,如Parallel
- 并发收集:用户线程和GC线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,如:CMS
4.11 判断类无用的条件
- JVM中该类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 没有任何地方引用该类的Class对象
- 无法再任何地方通过反射访问这个类
4.12 垃圾回收算法
- 标记清除法
- 分为标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
- 优点:简单
- 缺点:
- 效率不高、标记和清除的效率都不高
- 标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发GC
- 分为标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
- 复制算法
- 把内存分成两块完全相同的区域,每次使用其中一块当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉。
- 优点:实现简单,运行高效、不用考虑内存碎片问题
- 缺点:内存有些浪费
- JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和一块Survivor,回收时,把存活的对象复制到另一块Survivor。
- HotSpot默认的 Eden和Survivor比是8:1,也就是每次能用90%的新生代空间
- 如果survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代
- 把内存分成两块完全相同的区域,每次使用其中一块当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉。
- 标记整理算法
- 由于复制算法在存活对象比较多的时候,效率较低,且有空间浪费,因此老年代一般不会选用复制算法,老年代多选用标记整理算法。
- 标记过程跟标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存。
4.13 垃圾收集器基础和串行收集器
年轻代和老年代垃圾回收期配合方式:
-
串行收集器:
- Serial串行收集器/Serial Old收集器,是一个单线程的收集器,在垃圾收集时,会stop-the-world
- 优点:是简单,对于单cpu,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器。
- 使用-XX: +UseSerialGC来开启,会使用:Serial + SerialOld的收集器组合(新生代Serial+老年代SerialOld)
- 新生代使用复制算法,老年代使用标记-整理算法
- Serial串行收集器/Serial Old收集器,是一个单线程的收集器,在垃圾收集时,会stop-the-world
-
并行收集器
- ParNew(并行)收集器:使用多线程进行垃圾回收,在垃圾收集时,会Stop-The-World.
- 在并发能力好的CPU环境里,它停顿的时间要比串行收集器短;但对于单cpu或并发能力较弱的CPU,由于多线程的交互开销,可能比串行回收器更差。
- 是Server模式下首选的新生代收集器,且能和CMS收集器配合使用。
- 不再使用-XX:+UseParNewGC来单独开启
- -XX: ParrallelGCThreads: 指定线程数,最好于CPU数量一致
- ParNew(并行)收集器:使用多线程进行垃圾回收,在垃圾收集时,会Stop-The-World.
-
新生代Parrallel Scavenge 收集器
- 新生代Parallel Scavengel收集器/Parallel Old收集器:是一个应用于新生代的、使用复制算法的、并行的收集器
- 跟ParNew很类似,但更关注吞吐量,能最高效率的利用CPU,适合运行后台应用。
- 使用-XX: +UseParallelGC来开启
- 使用-XX: UseParallelOldGC来开启老年代使用ParrallelOld收集器,使用Parrallel Scavenge + Parrallel Old的收集器组合
- -XX: MaxGCPauseMills: 设置GC的最大停顿时间
- 新生代使用复制算法,老年代使用标记整理算法
-
CMS收集器(concurrent mark and sweep 并发标记清除)
- 分为:初始标记,只标记GC Roots能直接关联到的对象;并发标记,进行GC Roots Tracing的过程。
- 重新标记:修改并发标记期间,因程序运行导致标记发生变化的那一部分现象。
- 并发清除:并发回收垃圾对象(gc线程和用户线程同时跑)
- 初始标记和重新标记没有用户线程,所以还是会发生stop-the-world的。
- 使用标记清除算法,多线程并发收集的垃圾收集器。
- 最后的重置线程,指的是清空跟收集相关的数据并重置,为下一次收集做准备
- 优点:低停顿、并发执行
- 缺点:
- 并发执行,对CPU资源压力大
- 无法处理 在处理过程中产生的垃圾,可能导致FullGC
- 采用的是标记清除算法会导致大量碎片,从而在分配大对象时可能触发FullGC
- 开启:-XX:UseConcMarkSweepGC 使用ParNew + CMS + Serial old的收集器组合,Serial Old将作为CMS出错的后备收集器。
- -XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间使用多少后触发回收,默认80%
-
G1收集器
- G1 收集器:是一款面向服务端应用的收集器,与其他收集器相比,具有以下特点:
- Gl把内存划分成多个独立的区域(Region)
- G1仍采用分代思想,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Regionl的集合,且不需要Region是连续的。
- G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
- G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片(新生代和老年代都使用复制)
- G1的停顿可预测,能明确指定在一个时间段内,消耗在垃圾收集上的时间不能超过多长时间
- Gl跟踪各个Region里面垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限时间内的高效收集。
- 跟CMS类似,也分为四个阶段:
- 初始标记:只标记GCRootsi能直接关联到的对象
- 并发标记:进行GC Roots Tracingl的过程
- 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化的回收
- 使用与配置:
- -XX: UseG1GC 开启G1,默认就是G1
- -XX:MaxGCPauseMillis=500, 最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间
- -XX:InitiatingHeapOccupancyPercent= n; 堆内存用了多少的时候就出发GC,默认为 45
- -XX: NewRatio=n,默认为2
- -XX: SurvivorRatio=n,默认为8
- -XX:MaxTenuringThreshold=n,新生代到老年代的岁数,默认是15
- -XX:ParrallelGCThreads=n,并行GC的线程数,默认值会根据平台不同而不同
- -XX:ConcGCThreads=n:并发GC使用的线程数
- -XX:G1ReservePercent:=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
- -XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的)ava堆大小划分出约2048个区域
- G1 收集器:是一款面向服务端应用的收集器,与其他收集器相比,具有以下特点:
5.高效并发
5.1 Java内存模型、内存间的交互操作
- JCP定义了一种ava内存模型,以前是在VM规范中,后来独立出来成为SR-133(Java内存模型和线程规范修订)
- 内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
- Java内存模型主要关注VM中把变量值存储到内存和从内存中取出变量值这样的底层细节
- 所有变量(共享的)都存储在主内存中,每个线程都有自己的工作内存;工作内存中保存该线程使用到的变量的主内存副本拷贝。
- 线程对变量的所有操作(读、写)都应该在工作内存中完成。
- 不同线程不能相互访问工作内存,交互数据要通过主内存。
- 内存间的交互操作:
- Java内存模型规定了一些操作来实现内存间交互,JVM会保证它们是原子的。
- lock:锁定,把变量标识为线程独占,作用于主内存变量。
- unlock:解锁,把锁定的变量释放,别的线程才能使用作用于主内存变量。
- read:读取,把变量值从主内存读取到工作内存。
- load:载入,把read读取到的值放入工作内存的变量副本中。
- use:使用,把工作内存中一个变量的值传递给执行引擎。
- assign:赋值,把从执行引擎接收到的值赋给工作内存里面的变量。
- store:存储,把工作内存中一个变量的值传递到主内存中
- write:写入,把store进来的数据存放如主内存的变量中
- 内存间交互操作的规则
- 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但不保证连续执行,也就是说read与load之间、store-与write之间是可插入其他指令的。
- 不允许一个线程丢弃它的最近的assign操作,即变量在下作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中
- 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量,也就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其执行引ock操作,但ock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行引ock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assigna操作初始化变量的值。
- 如果一个变量没有被ock操作锁定,则不允许对它执行unlock操作,也不能unlock一个被其他线程锁定的变量
- 对一个变量执行unlocki操作之前,必须先把此变量同步回主内存(执行store和write操作)。
5.2 多线程的可见性、有序性和指令重排、线程安全的处理方法
- 多线程的可见性
- 就是一个线程修改了变量,其他线程可以知道
- 保证可见性的常见方法:
- volatile
- Synchronized
- final(一旦初始化完成,其他线程就可见)
- volatile
- 基本上是jvm提供的最轻量级的同步机制,用volatile修饰的变量,对所有线程可见,即对volatile变量所做的写操作能立即反应到其它线程中。
- 用volatile修饰的变量,在多线程环境下仍然是线程不安全的。
- volatile修饰的变量,是禁止指令重排优化的。
- 适合使用volatile的场景:
- 运算结果不依赖变量的当前值。
- 或者能保证只有一个线程修改变量的值
public class A {
private volatile int a;
public void aPlus() {
a++;
}
public int getA() {
return this.a;
}
}
public class MyRunnable implements Runnable{
private A a = null;
private String name ="";
public MyRunnable(A a, String name) {
this.a = a;
this.name = name;
}
@Override
public void run(){
for(int i = 0;i< 1000; i++){
a.aPlus()
}
System.out.println("thread =" + name+ "is game over -->>");
}
}
public class Test1{
public static void main(String[]args) {
A a = new A();
Thread t1 = new Thread(new MyRunnable(a,"t1"));
Thread t2 = new Thread(new MyRunnable(a,"t2"));
t1.join();
t2.join();
//不安全,只是保证了可见性
System.out.println("finally , t1 and t2 threads A.a = " + a.getA()); // 1903 1508
}
}
-
指令重排:指的是JVM为了优化,在条件允许的情况下,对指令进行一定的重新排列,直接运行当前能够立即执行的后续指令,避开获取下一条指令所需数据造成的等待。
- 线程内串行语义,不考虑多线程间的语义。
- 不是所有的指令都能重排,比如:写后读a=l,b=a;写一个变量之后,再读这个位置
- 写后写也是不能重排的,如:a = 1, a = 3;
- 读后写也不能重排,a = b, b = 2
-
指令重排的基本规则:
- 程序顺序原则:一个线程内保证语义的串行性。
- volatile规则:volatile?变量的写,先发生于读。
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C那么A必然先于C
- 线程的start方法先于它的每一个动作。
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码。
- 对象的构造函数执行结束先于finalize()方法。
-
多线程中的有序性
- 在本线程内,操作都是有序的
- 在线程外观察,操作都是无序的,因为存在指令重排或主内存同步延时。
-
Java线程安全的处理方法
- 不可变是线程安全的
- 互斥同步(阻塞同步):synchronized、java.util.concurrent.ReentrantLock.目前这两个方法特性已经差不多了,建议优先使用Synchronized,ReentrantLock增加了如下特性:
- 等待可中断:当持有锁的线程长时间不释放锁:正在等待的线程可以选择放弃等待
- 公平锁:多个线程等待同一个锁时,须严格按照申请锁的时间顺序来获得锁
- 锁绑定多个条件:一个ReentrantLocki对象可以绑定多个condition对象,而synchronized是针对一个条件的,如果要多个,就得有多个锁。
- 非阻塞同步:是一种基于冲突检查的乐观锁定策略,通常是先操作,如果没有冲突,操作就成功了,有冲突再采取其它方式进行补偿处理。
- 无同步方案:其实就是在多线程中,方法并不涉及共享数据自然也就无需同步了。
5.3 锁优化:自旋锁、锁消除、锁粗化、轻量级锁、偏向锁等
- 自旋锁与自适应自旋
- 自旋:如果线程可以很快获得锁,那么可以不在OS层挂起线程,而是让线程做几个忙循环,这就是自旋。
- 自适应自旋:自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者状态来决定。
- 如果锁被占用时间很短,自旋成功,那么能节省线程挂起以及切换时间,从而提升系统性能。
- 如果锁被占用时间很长,自旋失败,会白白耗费处理器资源,降低系统性能。
- 锁消除
- 在编译代码的时候,检测到根本不存在共享数据竞争,自然也就无需同步加锁了;通过-XX:+EliminateLocks来开启。
- 同时要使用-XX:+DoEscapeAnalysis:开启逃逸分析,所谓逃逸分析:
- (1) 如果一个方法中定义的一个对象,可能被外部方法引用,称为方法逃逸。
- (2) 如果对象可能被其它外部线程访问,称为线程逃逸,比如赋值给类变量或者可以在其它线程中访问的实例变量。
- 锁粗化
- 通常我们都要求同步块要小,但一系列连续的操作导致对一个对象反复的加锁和解锁,这会导致不必要的性能损耗。这种情况建议把锁同步的范围加大到整个操作序列。
- 轻量级锁
- 轻量级是相对于传统锁机制而言,本意是没有多线程竞争的情况下,减少传统锁机制使用OS实现互斥所产生的性能损耗。
- 其实现原理很简单,就是类似乐观锁的方式。
- 如果轻量级锁失败,表示存在竞争,升级为重量级锁,导致性能下降。
- 偏向锁
- 偏向锁是在无竞争情况下,直接把整个同步消除了,连乐观锁都不用,从而提高性能;所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程。
- 只要没有竞争,获得偏向锁的线程,在将来进入同步块,也不需要做同步
- 当有其它线程请求相同的锁时,偏向模式结束。
- 如果程序中大多数锁总是被多个线程访问的时候,也就是竞争比较激烈,偏向锁反而会降低性能。
- 使用-X:-UseBiasedLocking:来禁用偏向锁,默认开启
5.4 JVM中获取锁的步骤
- 会尝试偏向锁;然后尝试轻量级锁
- 再然后尝试自旋锁
- 最后尝试普通锁,使用OS互斥量在操作系统层挂起
5.5 同步代码的基本规则
- 尽量减少锁持有的时间
- 尽量减少锁的粒度
6.性能监控与故障处理工具
6.1 命令行工具
jps、jinfo、jstack、jmap、jstat、jstatd、jcmd
jps(VM Process Status Tool)):主要用来输出VM中运行
的进程状态信息,语法格式如下:jps[options][hostid]
hostid字符串的语法与URI的语法基本一致:
[protocol::][/hostname]port][/servername],如果不指定nostid,默认为当前主机或服务器。
jinfo: 打印给定进程或核心文件或远程调试服务器的配置信息。语
法格式:jinfo[option]pid #指定进程号(pid)的进程
jstack:主要用来查看某个Java进程内的线程堆栈信息。语法 格式如下:jstack[option]pid
jmap用来查看堆内存使用状况,语法格式如下jmap [option]pid
jstat
JVM统计监测工具,查看各个区内存和GC的情况
jstat -gc pid
jcmd
JVM诊断命令工具,将诊断命令请求发送到正在运行的)ava
虚拟机,比如可以用来导出堆,查看java进程,导出线程信息,执行GC等
6.2 图形化工具
jconsole、
jmc、visualvm
一个用于监视Java虚拟机的符合MX的图形工具。它可以监视衣和远程VM,还可监视和管理应用程序
jmc(JDK Mission Control)Java任务控制(JMC)客户端包括用于监视和管理Java应用程序的工具,
而不会引入通常与这些类型的工具相关联的性能开销。
VisualVM
一个图形工具,它提供有关在Java虚拟机中运行的基于Java技术的应用程序的详细信息。
Java VisualVM提供内存和CPU分析,堆转储分析,内存泄漏检测,访问MBean和垃圾回收。
jmc下载地址:点一下我 ~
6.3 两种连接方式
JMX、Jstatd
// 远程连接Tomcat
CATALINA OPTS="-Xms800m -Xmx800m -Xmn350m-
XX:SurvivorRatio=8-XX:+HeapDumpOnOutOfMemoryError
-Dcom.sun.management.jmxremote=true-
Djava.rmi.server.hostname=192.168.1.105-
Dcom.sun.management.jmxremote.port=6666-
Dcom.sun.management.jmxremote.ssl=false-
Dcom.sun.managementote.ssl=false-
Dcom.sun.management.jmxremote.authenticate=false'
配置jstatd:(1)自定义一个statd.policy文件,添加:
grant codebase "jrt:/jdk.jstatd"{
permission java.security.AllPermission;
grant codebase "jrt:/jdk.internal.jvmstat"{
permission java.security.AllPermission;
}:
(2)然后在JDK HOME/bin下面运行jstatd,示例如:
./jstatd -J-Djava.rmi.server.hostname=192.168.1.102 -
J-Djava.security.policy=java.policy -p 1099 &
6.4 JVM监控工具的作用
- 对jvm运行期间的内部情况进行监控,比如:对jvm参数、CPU、内存、堆等信息的查看
- 辅助进行性能调优
- 辅助解决应用运行时的一些问题,比如:OutOfMemoryError、内存泄露、线程死锁、锁争用、Java进程消耗CPU过高等等
6.5 监控与故障处理实战
- 内存泄漏分析、线程查看、热点方法查看、垃圾回收查看
- 线程死锁
常见的JVM监控工具:重点掌握visualvm和jmc
理解并掌握两种远程连接方式:JMX、jstatd
7. JVM调优
JVM调优:调什么、如何调、调的目标是什么
JVM调优策略、调优冷思考、调优经验
分析和处理内存溢出
7.1 调什么
- 内存方面
- JVM需要的内存总大小
- 各快内存分配,新生代,老年代,存活区
- 选择合适的垃圾回收算法、控制GC停顿次数和时间
- 解决内存泄漏的问题,辅助代码优化
- 内存热点:检查哪些对象在系统中数量最大,辅助代码优化
- 线程方面
- 死锁检查,辅助代码优化
- Dump线程详细信息:查看线程内部运行情况,查找竞争线程,辅助代码优化。
- CPU热点:检查系统哪些方法占用了大量CPU时间,辅助代码优化。
7.2 如何调优
- 监控JVM的状态,主要是内存、线程、代码、IO几部分
- 分析结果,判断是否需要优化
- 调整:垃圾回收算法(选择与参数配置)和内存分配(新生代、老年代);修改并优化代码
- 不断的重复监控、分析和调整,直至找到优化的平衡点
7.3 JVM调优的目标
- GC的时间足够小
- GC的次数足够的小
- 将转移到老年代的对象数量降低到最小
- 减少FullGC的执行时间
- 发生FullGC的时间间隔足够的长
7.4 常见调优策略
- 减少创建对象的数量
- 减少使用全局变量和大对象
- 调整新生代、老年代的大小到最合适
- 选择合适的GC收集器、并设置合理的参数
7.5 JVM调优冷思考
- 多数的java应用不需要考虑在服务器上进行GC优化
- 多数导致GC问题的java应用,都不是因为参数设置错误,而是代码问题
- 在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合)
- JVM优化是到最后不得已才采用的手段。
- 在实际使用中,分析JVM情况优化代码比优化JVM本身多得多
- 不需要优化的情况:
- MinorGC执行时间不到50ms
- MinorGC执行不频繁,约10秒一次
- FullGC执行时间不到1s
- FullGC执行的频率不算频繁,不低于10分钟1次
7.6 JVM调优经验
- 要注意32位和64位的区别,通常32位的仅支持2-3g左右的内存
- 要注意client模式和Server模式的选择
- 要想GC时间小必须要有一个更小的堆;而要保证GC次数足够少,又必须保证有一个更大的堆,这两个是冲突的,只能取其平衡。
- 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值 。
- 新生代和老年代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整,也可以通过-XX:newSize-XX:MaxNewSize来设置其绝对大小,同样,为了防止新生的堆收缩,通常会把-X:newSize -XX:MaxNewSize设置为同样大小
- 合理规划新生代和老年代的大小
- 如果应用存在大量的临时对象,应该选择更大的新生代;如果存在相对较多的持久对象,老年代应该适当增大。在抉择时应该本着FullGC尽量少的原则,让老年代尽量缓存常用的对象,JVM的默认比例1:2也是这个道理。
- 通过观察应用一段时间,看其在高峰时老年代会占多少内存,在不影响FullGC的前提下,根据实际情况加大新生代,但应该给老年代至少预留1/3的增长空间。
- 线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、 调用参数、局部变量等,对大多数应用而言这个默认值太大了,一般256K就足用。在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程。
7.7 内存泄漏
内存泄露导致系统崩溃前的一些现象,比如:
- 每次垃圾回收的时间越来越长,FuGC时间也延长到好几秒
- FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
- 老年代的内存越来越大,并且每次FuGC后年老代没有内存被释放
- 老年代堆空间被占满的情况
- 这种情况的解决方式:一般就是根据垃圾回收前后情况对比,同时根据对象引用情况分析,辅助去查找泄漏点
5.1 堆栈溢出的情况
通常抛出StackOverFlowError例外
一般就是递归调用没退出,或者循环调用造成
dump thread and memory shot
7.8 调优实战
重点是调优的过程、方法和思路
内存调整、数据库连接调整、内存泄漏查找等
CATALINA OPTS="-Xms512m -Xmx512m -Xmn200m -XX SurvivorRatio=8
-XX +HeapDumpOnOutofMemoryError
-Dcom.sun.management.jmxremote=true
-Djava.rmi.server.hostname=192.168.1.113
-Dcom.sun.management.jmxremote.port=6666
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.managementote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false"
8. 加油站
8.1 字节码部分
- 知道字节码吗?字节码指令集都有哪些?Integer x = 5,int y = 5,比较x==y都经过哪些步骤?
Code:
stack=2,locals=4,args_size=1
0:iconst_5
1:invokestatic #16 //method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4:astore_1
5:iconst_2
6:istore_2
7:aload_1
8:invokevirtual #22 // method java/lang/Integer.intValue:()
11:iload_2
12:if_icmpne
15:iconst_1
16:goto
19:iconst_0
20:istore_3
21:return
LineNumberTable:
line5:0
line6:5
line8:7
- 简述Java的类加载机制,并回答一个JVM中 可以是否存在两个相同的类
* 通过类的全限定名来获取改类的二进制流
* 把二进制的字节流转化为方法区的运行时数据结构
* 在堆上创建一个java.lang.Class对象,用来封装类在方法区的数据结构,并向外提供访问方
法区的内数据结构的接口。
# 主要说双亲委派流程
- 讲讲加载机制,都有哪些类加载器,这些类加载器都加载哪些文件?
- 启动类加载器 java.base、java.management、java.xml等
- 平台类加载器 java.scripting、java.compiler*、java.corba*
- 应用程序加载器:jdk.compile、jdk.jartool、jdk.jshell等 还加载classpath路径中的所有类库
加载-> 连接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载
8.2 内存分配
- 谈谈JVM内存模型(*内存分配 | 高效并发)
- 堆、元空间(线程共享)、本地方法栈、虚拟机栈、程序计数器(线程私有)
- JVM的数据区有哪些?作用是什么?
- Java堆内存一定是线程共享的吗?
内存分配方法:为对象分配内存的基本方法:指针碰撞法、空闲列表法
内存分配并发问题的解决:CAS、TLAB(Hotspot会预先为每个线程在堆上划分一块内存,也就是线程
私有的(分配时是线程独有的,其他回收使用都是共享的),TLAB这块内存是比较小的,大概占Eden区
的1/100)。
- Java堆内存结构是咋样的?哪些情况会触发GC?会触发哪些GC?
MinorGC/YoungGC: 发生在新生代的收集动作
MajorGC/OldGC: 发生在老年代的GC,目前只有CMS收集器会单独收集老年代的行为
MixedGC:收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为
- 说说JVM的垃圾回收
什么是垃圾、如何判定是垃圾、如何回收
根搜索算法、引用分类、GC类型、垃圾收集类型
/*
简单说就是内存已经不再被使用到的内存空间就是垃圾
引用计数法:给对象添加一个引用计数器,有访问就加1,引用失效就减1
优点:简单、效率高、缺点:不能解决对象之间循环引用的问题
根搜索算法
清除算法:复制算法、标记清除、标记整理
垃圾收集器:串行收集器、并行收集器、新生代Parrallel、Scavenge收集器、CMS、G1
GC性能指标和JVM内存配置原则
*/
- JVM四种引用类型:
- 强应用:类似于Object a =new A(),不会被回收
- 软引用:还有用但并不必须的对象。用SoftReference来实现
- 弱引用:非必须对象,比软引用还要弱,垃圾回收会回收掉。用WeakReference来实现弱引用
- 虚引用:是最弱的引用,垃圾回收时会回收掉。用PhantomReference来实现虚引用。
- JVM回收算法和垃圾收集器。
8.3 监控工具和实战
- 如何把java内存的数据全部dump出来
-XX:+HeapDumpOnOutOfMemoryError: OOM时导出堆文件
Jmap来查看堆内存使用情况,jmap pid
jmc、virualvm
- Jstack是干嘛的?
- 用来查看java进程中线程堆栈信息。jstack pid
- JVM统计检测工具,查看各个区内存和GC情况
- jstat
- 如何定位问题?如何解决问题?说一下解决思路和处理方法。
jmc和virualVM结合使用
jfr定位问题,记录过程,内存分配情况,线程、垃圾回收过程
结合源代码定位问题。
监控JVM的状态,主要是内存、线程、代码、I/O几部分
分析结果,判断是否需要优化
调整:垃圾回收算法和内存分配;修改并优化代码
不断的重复监控、分析和调整,直至找到优化的平衡点
- CPU 使用率过高咋么办?
- 应用逻辑耗CPU:那么就是计算密集型[图形图像处理]
- 频繁的io,大量线程死锁,线程竞争资源
- CFR记录一段时间,分析
- 线上引用频繁full gc如何处理?
- 内存设置,整个堆太小了,新生代->进入老年代(老年代不够了,频繁触发fullgc)
- 堆够,比例不对
- 代码频繁分配大对象,能不能复用大对象或者尽快回收,设置为null
- 垃圾收集器是否合理,参数是否合理。
- 如果应用周期性地出现卡顿,你会咋么来排查这个问题?
- full-gc导致stw
- 规避full-gc,或者将full-gc的周期拉长
- 减少full-gc,时间变短
- 其他原因
- 你有没有遇到过OutOfMemory问题?你是咋么来处理这个问题的?
- 老年代堆空间被占满的情况
- 这种情况的解决办法:一般就是根据垃圾回收前后情况对比,同时根据对象的引用情况分析,辅助去查找泄漏点
- StackOverFlow异常有没有遇到过?这个异常会在什么情况下被触发?如何指定线程堆栈的大小?
- 堆栈溢出的情况
- 通常抛出java.lang.StackOverFlowError例外
- 一般就是递归调用没有退出,或者循环调用造成
- -Xss: 通常只有几百k,决定了函数调用的深度。
写在最后 :
监控工具实战的方面、内存分配、垃圾回收
类的装载、连接、初始化最重要
9.梳理jvm知识体系
9.1 地毯式总结jvm知识
/*
JVM 概述:认识jvm、java如何实现平台无关的
JVM规范:理解JVM规范的作用、了解JVM规范里规定的主要内容
Class文件格式:CLass文件格式、阅读class字节码文件、阅读虚拟机汇编语言表示的java类、asm开发
实战(认识asm、asm编程模型和核心api、asm开发)
类加载、连接和初始化:理解类从加载、连接、初始化到卸载的生命周期、理解类加载、类加载器、
双亲委派模型、类连接、类初始化(类初始化、类初始化时机)、类卸载
JVM内存分配:JVM简化架构、内存模型、理解栈、堆、方法区之间的交互关系
JVM堆内存:堆的结构、对象的内存布局、内存分配参数(Trace跟踪参数、GC日志参数、堆参数、
栈参数、元空间参数)
字节码执行引擎:栈帧、局部变量表、操作数栈、动态连接、方法返回地址
栈帧、运行期操作数栈和局部变量表之间的交互关系
方法调用(静态分派、动态分配)
垃圾回收基础:什么是垃圾、如何判定是垃圾、如何回收、根搜索算法、引用分类、跨代引用、记忆集
写屏障、GC类型、Stop-the-World、垃圾收集类型
垃圾收集算法:标记清除法、复制算法、分配担保、标记整理法
垃圾收集器:HotSpot中的收集器、串行收集器、并行收集器、新生代Parallel Scavenge收集器
CMS收集器、G1收集器、了解ZGC收集器、GC性能指标、JVM内存配置原则
JVM对高效并发的支持:Java内存模型、内存间的交互操作、多线程的可见性、有序性、原子性
指令重排、线程安全的处理方法、锁优化(自旋锁、锁消除、锁粗化、轻量级锁、偏向锁)
性能监控与故障处理工具:
命令行工具:jps、jinfo、jstack、jmap、jstat、jstatd、jcmd
图形化工具:jconsole、jmc、visualvm
两种远程连接方式:jmx、jstatd
jvm调优实战:
jvm调优(调什么、如何调、调优的目的是什么)
JVM调优策略、jvm调优冷思考、jvm调优经验
分析和处理内存泄漏、调优实战(多练习)
*/
9.2 后续学习方法
多应用,将所学的内容进行分析应用
掌握的越深刻 掌握的越牢固!