jvm
程序开发周期:编写,编译,运行
- .class中的字节码
- Class 类文件结构
- 字节码指令
- 读取- 虚拟机类加载
- 时机
- 过程
- 类加载器
- 执行- 字节码执行引擎
- 运行时栈帧结构
- 方法调用
- 动态类型语言支持
- 基于栈的字节码执行引擎
- 数据- 自动内存管理
- 内存区域分配、内存溢出
- 垃圾收集器、内存分配策略
- 性能监控、故障处理、调优
- 多线程下共享内存模型
JRE 包含 Java 运行的必须组件:Java API + Java JVM。JVM 可以由硬件实现,但软件实现有可移植性
字节码
源代码.java -编译->字节码.class -运行解释->特定的机器码
- 共同点:都是做 语言映射
- 编译:先编译再运行
- 解释:边解释边运行
字节码:字节码指令的操作码,固定为1字节;
字节码示例(Java字节码):格式为 指令(有特定含义)和操作数
0: aload_0
1: invokevirtual #16 // Method java/lang/Object.getClass:()Ljava/lang/Class;
4: pop
5: return
机器码示例(x86汇编):格式为 十六进制数字,每个数字对应着底层机器指令的操作码
8B 45 FC mov eax, DWORD PTR [ebp-4]
83 C0 01 add eax, 0x1
89 45 FC mov DWORD PTR [ebp-4], eax
C9 leave
C3 ret
类加载
当程序首次使用某个类时,类加载器会根据类的全限定名找到对应的字节码文件,并将其加载到内存中。这样可以避免在程序启动时一次性加载所有类,而是根据实际需要逐步加载,减少启动时间和内存消耗
加载时机, 启动时 main() 进件加载并初始化
- new;反射调用类,初始化类
- 子类初始化触发父类初始化;接口定义为 default,实现类初始化触发接口初始化
- 静态方法/静态字段所在类:program-pattern#单例 类的创建放在方法内,实现延迟初始化
Java类型
- 基本类型:
- 引用类型:类、接口、数组类、泛型参数
加载
查找字节流,在内存中生成Class对象。数组类直接JVM生成,其他类需要借助 类加载器 完成
- 层级(双亲委派)
- 启动类加载器 C++实现;负责 JRE-lib-jar
- 扩展类加载器:JRE-lib/ext
- 应用类加载器:应用路径下的类,通过 环境变量 CLASSPATH指定,默认是应用程序中包含的类
- JVM 中,类的唯一性由 ClassLoader+全类名唯一确定
链接:类合并到 JVM中
- 验证:类满足JVM约束
- 准备:静态字段(类变量)分配内存并设置初值(数据类型的默认值),如果同时被final 修饰,就是赋值
- 解析(顺序不一定):将符号引用(类、方法、字段的唯一地址)解析成实际引用
初始化
- 初始化内容
- 类变量static 和 static{}静态代码块 赋值,父类优先于子类,由编译器封装到
<clinit>方法中,执行,JVM通过加锁保证仅被执行一次
- 类变量static 和 static{}静态代码块 赋值,父类优先于子类,由编译器封装到
内存管理
内存布局
运行时内存空间的组织结构,关注内存空间的划分和管理
内存区域(Class 文件在内存中的分布)
- 线程共享
- 方法区:存储类结构信息、常量池
- 堆:对象实例
- 线程私有
- 栈
- 面向Java方法的方法栈:方法的局部变量和字节码操作数栈;基本数据类型、引用
- 面向本地方法(C++)的本地方法栈
- 存放线程执行位置的 程序计数器
- 栈
原生内存:NIO,引入基于通道和缓冲区的IO方式,使用 Native 函数直接分配堆外内存,通过堆内的DirectByteBuffer 对象引用;受本机总内存和CPU寻址空间的限制导致 OutOfMemoryError
OutOfMemoryError
OutOfMemoryError,只影响JVM的单个线程,不会让JVM退出。终止线程引用的对象和不被任何线程引用的对象,在GC周期中释放内存,使得存活线程能继续执行
- JVM没有可用的原生内存
- 元空间内存不足
- Java堆本身内存不足:对于给定大小的堆,应用程序已经无法创建任何额外的对象
- JVM花费了太多时间执行GC
原生内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
元空间内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
元空间默认没有最大值限制,发生这个错误是你设置了最大值
- 元空间最大值不够大
- 设置最大值可能是由于 类加载器泄露消耗掉系统内存
- 类加载器内存泄露,重新部署会创建新的类加载器,旧的类加载器没有被回收,
- 原因:
- 如果是应用服务器,需要联系厂商修复
- 如果是应用程序,要确保类类加载被正确丢弃,尤其是类加载器没被线程设置为临时的加载器
- 排查:使用 dump-直方图查找GC 根,看 ClassLoader 实例被什么引用
- 原因:
堆内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- 应用程序活跃对象太多,而堆空间太小
- 应用程序存在内存泄露,持续分配新对象,而其他对象没有被GC;比较堆转储文件,查看差异
-XX:+HeapDumpOnOutOfMemoryError =true (default=false) OutOfMemoryError时创建堆转储文件
-XX:HeapDumpPath=<path>指定写入的文件,默认位置是应用程序当前工作目录下的java_pid<pid>.hprof
-XX:+HeapDumpAfterFullGC运行Full GC之后生成堆转储。-XX:+HeapDumpBeforeFullGC运行Full GC之前生成堆转储。
获取堆转储会延长停顿时间,因为堆数据会被写入磁盘
达到GC开销限制
JVM判断它花了太多时间执行GC
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
满足下面所有条件时,错误会抛出
- 在Full GC中花费的时间超过了-XX:GCTimeLimit=N标志指定的值。它的默认值是98(如果98%的时间花在GC上,该条件就满足)
- 一次Full GC回收的内存量小于-XX:GCHeapFreeLimit=N标志指定的值。默认值是2,意味着如果Full GC期间回收的内存小于堆的2%,该条件就满足。
- 前面两个条件在5个连续的Full GC周期中都成立(这个值不可调)
- -XX:+UseGCOverheadLimit标志的值是true(这是它的默认值)
内存优化
堆内存
目标:
- 谨慎地创建对象并尽快丢弃它们,减少对象创建也提高GC效率
- 重用对象(线程局部变量、特殊对象引用和对象池),提高应用程序性能
工具:查看应用程序当前使用的对象,会消耗时间和资源,不用于测量应用程序的执行
- 堆直方图:查看对象数量,识别一两个类实例过量
- 获取需要几秒钟,会触发Full GC,因此不要在性能稳定的情况下用直方图
- heap dump:
保留内存视图 - 可以被GC的对象占有的内存,找出占用大部分内存的少数对象,减少它们的创建,保留时间,或者把它们变小
- 被多次引用的对象不被计入保留内存中,最大的保留对象通常来自于类加载器
直方图视图 - 统计同类型的对象
对象引用-GC根 - GC根是系统对象,持有的静态全局引用指向普通对象,通常来自于 类的静态变量,这些类加载于系统/启动类路径,含 Thread类和所有的活跃线程。线程 通过线程局部变量/Runnable对象的引用 保留对象
- 对象有多个引用时,它可能用多个GC根;String,HashMap这样被多次引用的很难查找,经验是,寻找路径应该从集合对象(如HashMap)而不是从条目(如HashMap$Entry)开始,并且要寻找最大的集合
减少内存使用
- 减小对象大小
- 减少它持有的实例变量的数量(效果很明显):只定义必要的实例变量
- 如果字段值是计算得出的,目标是减少GC,计算更好,虽然会消耗CPU
- 减小这些变量的大小(不那么明显),特别是在频繁实例化的类中:较小的数据类型
- 如果一个类需要跟踪8个可能的状态之一,可以使用一个byte
- OpenJDK-jol,可以计算对象大小
- 减少它持有的实例变量的数量(效果很明显):只定义必要的实例变量
- 对象延迟初始化
- 一个特定类可能只有10%的时间需要一个对象,最好只在相关操作使用得不那么频繁时使用
- 如果代码必须是线程安全的,则需要做同步
- 如果在方法的后续调用中(或者在类的其他地方)不使用该变量,那从一开始就没有理由将其作为实例变量。只需要在方法中创建一个局部变量。当方法完成时,局部变量会离开作用域,垃圾回收器会释放它
- 将值设置为null ,用于及早清理(eagerly deinitializing),适用于Java集合框架;但要确保释放对象,而不是仅设为null(仍占用内存)
- 使用不可变/标准化(不可变对象的单一表示)对象,如 基本类型包装类,Bigdecimal,String
- 当这些对象被快速地创建和丢弃时,它们对新生代回收的影响比较小
- 使用不可变对象,避免创建new重复对象
标准化类框架
public class ImmutableObject {
// 创建映射存储对象的标准化版本
private static WeakHashMap<ImmutableObject, ImmutableObject> map = new WeakHashMap();
public ImmutableObject canonicalVersion(ImmutableObject io) {
synchronized(map) {
ImmutableObject canonicalVersion = map.get(io);
if (canonicalVersion == null) {
map.put(io, new WeakReference(io));
canonicalVersion = io;
}
return canonicalVersion;
}
}
}
对象生命周期管理
实现方式
- 重用对象
- 维护指向这些对象的特殊引用
重用对象
对象重用:对象池和线程局部变量
缺点:被重用的对象会在堆中停留很长时间。如果堆中有大量对象,创建新对象的空间就更少了,因此GC操作会更频繁
堆中活跃对象对GC时间的影响:增加可能不止一个数量级(*10)
重用对象的原因:许多对象的初始化成本很高,权衡了增加的GC时间之后,还是重用对象的效率更高
JDK中满是使用线程局部变量的类,以避免重新分配某种类型的对象
- JDBC连接池:创建网络连接,可能还需要登录以及建立数据库会话,成本很高。
- 线程池化可以节省创建线程的时间;
- 随机数生成器Random类和(特别是)SecureRandom类的实例,在使用随机数种子初始化时成本很高
- 大数组:Java要求,当一个数组被分配时,其中的每个元素都必须初始化为基于0的默认值(null、0或false,视情况而定)。对于很大的数组,这是非常耗时的
- 原生NIO缓冲区:无论缓冲区有多大,分配一个直接的java.nio.Buffer(调用allocateDirect()方法返回的缓冲区)都是成本很高的操作。最好是创建一个大的缓冲区,然后通过按需切片的方式来管理缓冲区,以便在将来的操作中返回它们重新使用。
- 与安全相关的类:MessageDigest、Signature以及其他安全算法的实例,初始化成本都很高
- 字符串编码器和解码器对象:JDK中的各种类都会创建和重用这些对象。在大多数情况下,这些对象也是软引用
- StringBuilder帮助类:BigDecimal类在计算中间结果时会重用一个StringBuilder对象。
- 从DNS查询中获得的名字:网络查询的成本很高
- ZIP编码器和解码器:初始化成本并不是特别高,但是它们的释放成本很高,因为它们依赖对象终结(finalization)来确保它们使用的原生内存也被释放了
这些例子的共同点
- 初始化对象需要很长的时间
- 对象初始化的性能取决于对象本身。你应该考虑只重用初始化成本非常高的对象,并且只有当初始化这些对象是程序中最主要的操作之一时,才考虑重用
- 共享的对象数量往往很少:将GC操作的影响降到最低
- 在池中有少量对象并不会对GC效率产生太大影响,但堆中充满池化对象时会大大减慢GC的速度
重用性能
- 对象池
- 问题:大小很难设置准确,将对象管理的责任交给了开发:不能直接让对象退出作用域,开发必须将对象返还到池中
- 性能
- GC 影响:持有大量对象会降低GC的效率(有时会大幅降低)
- 同步:如果对象被频繁地删除和替换,对象池可能会存在大量竞争。结果是,对象池的访问会比初始化新的对象还慢
- 限流(throttling):向数据库发出的JDBC连接超出了其处理能力,数据库的性能就会下降,因为CPU不堪重负;限制JDBC连接数,因此线程要等待空闲资源
- 线程局部变量
- 生命周期管理:对象池使用完后要归还
- 基数性(cardinality):一个线程至多一个对象,不限制线程数量就无法对资源限流
- 同步:线程局部变量不需要同步,因为它们只能在单个线程内使用
原则:
- 普通类的大型对象池带来的性能问题肯定会比解决的问题还多。所以要把这些技术用在那些初始化成本很高并且重用对象数量较少的类上
- 使用对象池还是局部变量:如果线程和可重用对象之间有一一对应的关系,那么线程局部变量更容易使用
不确定引用
更多地用于缓存 耗时的计算或数据库查询的结果,重用这个复杂对象,否则创建成本太高
引用(reference):一个引用(或对象引用)可以是任何类型的引用,比如强引用、弱引用和软引用等。一个普通实例变量指向一个对象,这就是一个强引用
不确定引用(indefinite reference):任何特殊类型的引用(例如,软引用或弱引用)。一个不确定引用实际上是一个对象实例(例如,SoftReference类的一个实例)
所引对象(referent):不确定引用的工作方式是,将另一个引用(几乎总是强引用)嵌入到不确定引用类的实例中。被封装的这个对象被称为所引对象
优点:一定会被垃圾回收
缺点:不确定引用本身会消耗内存;至少需要两个 GC 周期,// TODO 图
-XX:+PrintReferenceGC标志(默认为false)打印 GC 引用花费时间
软引用
软引用本质上是一个大型的、最近最少使用的(LRU)对象池(对象大概率被重用),良好性能的关键是要确保它被及时清理。
软引用被清理的时机:所引对象必须没有被其他的强引用所引用。如果软引用是指向所引对象的唯一引用,那么当软引用最近没有被访问时,所引对象就会在下个GC周期被释放
如果完全耗尽内存或抖动(thrashing)过于严重,JVM会清理所有的软引用,否则会抛出OutOfMemoryError异常
当对象的数量不太多时,软引用的效果才会好(很容易填满堆),否则,还是要考虑使用更传统的、大小有界的对象池,并且以LRU缓存形式实现
弱引用
弱引用对象只有在它的所引对象仍然存在时才可以使用(在所引对象被回收后的下个GC周期,它就会被清理)
其他引用
// TODO
原生内存
整体内存的使用,监控和发现性能问题
JVM的内存占用(footprint)=原生内存+堆内存
应用程序使用原生内存:NIO
Java进程使用的原生内存与其他进程共享,为了获得最佳性能,你需要确保所有Java进程总的内存占用不超过机器的物理内存(加上你想为其他应用程序留出的内存)
测量内存占用
随着编译的代码增多,代码缓存会从初始值增长到最大值,这适用于 堆,元空间(初始值(提交的内存)增长到最大值(保留的内存))
线程栈空间在创建时就被完全分配了。每当JVM创建一个线程时,操作系统都会分配一些原生内存来存放线程栈,并向进程提交更多的内存(至少持续到线程退出)
# https://cloud.tencent.com/developer/article/1683708
sudo apt-get install smem
smem -r -p <PID>
最小化内存占用,限制内存使用量
- 堆:占总内存的50%到60%;减小堆的最大值(或者设置GC优化参数,让堆永远不会完全占满)可以限制程序的内存占用
- 线程栈:9章
- 代码缓存:代码缓存使用原生内存来保存编译后的代码
- 原生库分配:原生库可以分配自己的内存
原生内存跟踪,适用于 JVM 分配的内存
-XX:NativeMemoryTracking=off(default)|summary|detail 可以查看原生内存的信息
-XX:+PrintNMTStatistics标志(default false),JVM会在程序退出时输出原生内存的分配信息
summary-内存的使用情况
- Java Heap
- Class 类元数据
- Thread
- Code 即时编译代码缓存
- GC GC的堆外内存
- Compiler 编译器将结果代码用于自身操作
- Internal JVM内部操作
- Symbol 符号表引用
- Native Memory Tracking NMT自身操作
- JVM簿记(bookkeeping)
detial- 内存随时间的变化
共享库原生内存
从架构的角度来看,NMT是HotSpot的一部分,HotSpot是运行程序的Java字节码的C++引擎。它位于JDK的下层,所以NMT无法跟踪JDK层面上任何东西分配的内存。JDK层面的分配来自共享库(通过调用System.loadLibrary()加载的共享库)
原生内存泄漏,即应用程序的RSS或工作集随着时间的推移不断增长,通常不会被NMT检测到
如果一个进程的工作集增长到10 GB,而NMT告诉我们JVM只提交了6 GB的内存,那我们就知道另外4 GB的内存一定是由原生库分配的
Profiler,能跟踪原生代码的内存分配,不能跟踪Java代码的
原生内存被大量使用
- 使用Inflater对象和Deflater对象:当大量原生内存泄漏时,获取应用程序的堆转储并寻找这些Inflater对象和Deflater对象是很有帮助的。这些对象本身可能不会引发堆的问题(它们太小了),但是大量的这些对象就表明,它们会占用很大的原生内存。
- 使用NIO缓冲区
- 如果NIO字节缓冲区是通过ByteBuffer类的allocateDirect()方法或者FileChannel类的map()方法创建的,那么它们会分配堆外的原生内存
- 从性能的角度来看,原生字节缓冲区非常重要,因为它们允许原生代码和Java代码在不复制的情况下共享数据。用于文件系统和套接字操作的缓冲区是最常见的例子。将数据写入原生NIO缓冲区之后,不需要在JVM和传输数据的C库之间复制数据,就可以将该数据发送到通道(例如,文件或套接字)。但如果使用的是堆字节缓冲区,那么JVM必须复制缓冲区的内容
- 调用allocateDirect()方法的成本很高,应当尽可能重用直接字节缓冲区。重用使用线程局部变量,但如果线程需要大小不一的缓冲区或线程局部缓冲区不适合程序设计时,使用对象池
- 字节缓冲区也可以通过切片来管理。应用程序可以分配一个非常大的直接字节缓冲区,各个请求通过使用ByteBuffer类的slice()方法从该缓冲区中分配内存。但是当切片的大小总是不相同时,这个方案会变得很难处理。当分配和回收的对象大小不同时,原有的字节缓冲区会像堆一样变得碎片化。与堆不同的是,字节缓冲区的切片不能被压缩。所以只有当所有切片的大小一致时,这种方案才能很好地发挥作用
- 应用程序可以分配的直接字节缓冲区的内存总量受JVM的限制。为直接字节缓冲区分配的内存总量可以通过-XX:MaxDirectMemorySize=N标志设定。在当前的JVM中,这个标志的默认值是0。这个限制的含义经常变化,在Java 8的后期版本(以及Java 11的所有版本)中,这个限制的最大值等于堆的最大值。如果堆的最大值是4 GB,你可以在直接字节缓冲区和/或映射字节缓冲区中创建4 GB的堆外内存。如果需要,你还可以增加该值,使其超过堆的最大值。
- NMT报告的Internal部分包含为直接字节缓冲区分配的内存信息。如果这个数字很大,那基本上都是因为这些缓冲区。如果想知道缓冲区本身到底占用了多少内存,MBean会对此进行跟踪。检查MBean的java.nio.BufferPool.direct.Attributes或java.nio.BufferPool.mapped.Attributes可以看到每种类型分配的内存量
Linux 系统内存泄露
原生内存不进行压缩,碎片化严重,耗尽原生内存,排查:
- 应用程序会抛出OutOfMemoryError异常,表明原生内存已经耗尽。
- 如果查看进程的smaps文件,那么会发现它显示有许多小的(通常是64 KB)分配。
补救的办法是将环境变量MALLOC_ARENA_MAX设置为像2或4这样小的数。该变量的默认值是系统的核心数乘以8(这也是这个问题在大型系统上比较常见的原因)。在这种情况下,原生内存仍然会变得碎片化,但不会太严重。
针对操作系统的JVM优化
- 大页
- Linux 大页
- Linux 透明大页
- Windows 大页
页面(page)是操作系统用来管理物理内存的内存单位。它是操作系统分配的最小单元。当分配1字节时,操作系统一定会分配一整个页面。该程序之后分配的内存都在这个页面,直到它被填满,而后会分配一个新的页面。
操作系统分配的页面比物理内存能容纳的页面多很多,这就是为什么需要分页。地址空间的页面会被移入或移出交换空间(或其他存储,这跟页面中包含的内容有关)。这意味着,这些页面和它们当前存储在计算机RAM中的位置之间一定存在某种映射。这些映射有两种方式,所有的页面映射都保存在一个全局页表中(操作系统可以扫描这张表,以找到特定的映射),最常用的映射保存在转换后备缓冲区(translation lookaside buffer,TLB)中。TLB保存在快速缓存中,因此通过TLB条目访问页面比通过页表访问要快得多。
机器的TLB条目数量有限,因此最大限度地提高TLB条目的命中率就变得很重要了(它的功能是作为最近最少使用的缓存)。由于每个条目代表一页内存,因此增加应用程序使用的页面大小通常是有利的。如果每个页面能承载更多的内存,涵盖整个程序所需的TLB条目就会更少,需要时在TLB中找到一个页面的概率就更大。
大页必须在Java和操作系统两个层面上开启。在Java层面,-XX:+UseLargePages标志可以开启大页的使用,默认情况下这个标志是false。并非所有的操作系统都支持大页,开启大页的方式显然也不尽相同。
内存模型
Java Memory Model,多线程环境下的内存访问规则、操作顺序和同步机制。确保不同线程间的可见性、有序性和原子性
Java内存模型与堆内存的并发安全设计相关,但它涵盖了整个内存体系结构,包括主内存、工作内存和各种缓存等
- 主内存(Main Memory):主内存是所有线程共享的内存区域,用于存储对象的实例数据和静态变量等
- 工作内存(Working Memory):工作内存是每个线程独立拥有的内存区域,用于存储线程私有的栈帧和本地变量等。工作内存保存了从主内存中读取的副本,并对副本进行操作
- 内存屏障(Memory Barrier):内存屏障是一种同步机制,用于确保特定的内存操作顺序和可见性。在编写多线程代码时,通过插入内存屏障可以控制指令的执行顺序,以保证线程之间的协调和数据的正确更新
- 原子性操作(Atomicity):Java内存模型保证了一些特定操作的原子性,即这些操作要么完全执行成功,要么不执行。例如,对volatile变量的读写操作是具有原子性的
- 可见性(Visibility):Java内存模型确保在一个线程中对共享变量的修改对其他线程可见。使用同步机制(如锁、volatile关键字、synchronized块等)可以确保可见性
happens-before
让程序免于数据竞争的干扰,描述两个操作内存可见性:操作X happens-before Y,则 X 的结果对 Y 可见
- 线程内:字节码的顺序表达了 happens-before,单如果后者不数据依赖前者,可能指令重排
- 线程间:
- 加锁解锁、lovatile 读写
- 线程启动、第一个操作 / 线程最后一个操作、终止 / 线程中断、接收中断事件
- 构造器 最后一个操作、析构器第一个操作
内存模型底层实现:通过内存屏障 memory barrier 禁止指令重排
对于JIT 编译器,在 happens-before 向正编译的目标方法插入相应 读读、读写、写写 内存屏障
内存屏障使用具体的 CPU 指令。
内存模型关键字
- 锁:解锁时,JVM 强制刷新缓存,使得当前线程修改的内存对其他线程可见
- 条件是一个线程有的同把锁
- volatile:写操作强制刷新缓存;不保证原子性
- JIT 编译器 不会将其分配到寄存器,每次都是从内存中读写
- final:新建对象的发布。当一个对象包含final 字段,我们希望其他线程只能看到已初始化好的final 实例
- final 字段的写操作后 插入一个 写写 屏障,防止新建对象的发布先于final对象的写操作之前
JVM 实现 synchronized
字节码含 monitorenter 和 monitorexit 指令。退出方法时,正常返回或异常抛出,都需要解锁
可重复锁:同一线程每次持有锁,计数器+1
JVM锁实现
- 重量级锁:线程阻塞(阻塞加锁失败的线程)和唤醒(目标锁释放的时候唤醒/取消阻塞线程)
- 操作系统实现,涉及系统调用,从操作系统的用户态切换的内核态,开销大
- 为避免昂贵的线程操作,JVM在线程 进入阻塞前,唤醒后竞争不到锁的情况下,自旋,
- 在处理器上空跑并轮询锁是否被释放
- 自旋会消耗处理器资源,自旋时间,根据以往时间动态调整
- 自旋导致不公平锁竞争,自旋中的线程,很可能优先获得锁
- 轻量级锁:不同时间多个线程请求锁,没有锁竞争
- 对象头标记字段 mark word,最后两位标识 锁状态
- 非重量级锁,划分锁记录栈帧。先 CAS 替换 锁对象的标记字段为当前锁记录的地址,失败:
- 同一把锁,锁记录清空
- 其他线程持有该锁,膨胀成重量级锁
- 偏向锁:只有一个线程请求一把锁
- 线程加锁时,如果支持偏向锁,则请求锁只需判断锁对象的标记字段,如果满足,加锁成功
- 为保证加锁的线程没有丢锁,会遍历所有线程的Java栈,找出加锁实例,标志 epoch+1,表示之前的偏向锁已失效
GC
GC:找出不被引用的对象,并回收对象的内存
垃圾:死亡对象占据的堆空间;不回收 栈内存中的引用变量(指向堆内存中对象的地址),它们由编译器和虚拟机自动管理。
比起 跟踪指针引起的BUG,垃圾回收更简单,用时更少
GC 的性能取决于,找到不使用的对象,回收其内存,压缩堆内存(不同回收器有不同的实现方式)
查找
找出不被引用的对象,策略:
- 引用计数
- 存在问题:
List<Object>, list 中每个对象都指向后一个对象。列表头没有被引用,整个列表就不会被引用;但如果列表是环形的,所有对象都被列表中的某个对象引用,即便没有外部引用,也不会被垃圾回收
- 存在问题:
- 可达性分析:GC root 对象开始,查找所有可到达的对象
- 如果一个对象无法通过任何引用链路与根节点相连,即使它与其他对象相互引用,也会被认为是不可达的
// 可达性分析伪代码
algorithm markAndSweep():
// 标记阶段
// rootObjects 代表根对象集合
mark(rootObjects) // 从根对象开始,标记可达的对象
// 清除阶段
sweep() // 清除未被标记的对象
function mark(object):
if object is marked:
return
mark object as marked
for each reference in object:
mark(reference)
function sweep():
for each object in heap:
if object is marked:
unmark object
else:
deallocate object
// 程序入口
function main():
// 创建对象并建立引用关系
obj1 = new Object()
obj2 = new Object()
obj3 = new Object()
obj1.setReference(obj2)
obj2.setReference(obj3)
// obj1 是根对象,其他对象通过引用可达
markAndSweep()
// 假设 obj1 不再可达
obj1 = null
// 执行垃圾回收
markAndSweep()
STW
问题:多线程环境下,很多线程会更新对象引用(应用更新对象,GC跟踪对象引用,GC移动对象),导致误报(引用设置为 null ,损失垃圾回收机会)或 漏报 (引用设置为未标记的对象,访问会崩溃)
解决:Stop-the-world、安全点
- 暂停其他非GC线程,直到完成GC,通过安全点实现:JVM 收到 Stop-the-world 请求,等待所有线程到达安全点,Stop-the-world 请求线程独占工作
- 只要不离开安全点,JVM 能够在垃圾回收时,继续运行
- Native 代码
- 解释执行字节码:字节码与字节码间都可作为安全点,当有安全请求时,执行一条字节码做一次安全检测
- 执行即时编译器生成的机器码和线程阻塞(阻塞线程处于JVM 线程调度器控制中)
减少 STW:
垃圾回收方式
- 清除:标记,空闲内存记录在空闲列表中,新建对象时从空闲列表划内存
- 缺点:1. 内存碎片 2. 分配效率低,需要逐个访问列表项;而连续内存可通过 指标加法 实现分配
- 压缩:存活对象聚集在内存的起始位置
- 缺点:压缩算法性能开销
- 复制:内存区域两等分,指针 from, to 维护,from 分配新内存;垃圾回收时,存活对象都复制到 to,swap(from,to),清理 to
- 缺点:堆空间使用率低
分代回收
分代的原因是:大部分对象只存活小段时间,存活下来的小部分Java对象会存活很长一段时间
- 堆空间划分为两代,新生代存储新建的对象,对象存活时间够长时,将其移动到老年代;
- 不同代使用不同回收算法,新生代使用耗时较短的垃圾回收算法 Minor GC,新生代空间不足时,GC 停止应用线程并清空新生代(不使用的对象丢弃,使用的对象移动到老年代);
- 堆空间耗尽回收老年代 Full GC。为实现低停顿,使用 并发回收器(concurrent collector),代价是使用更多CPU,难以优化
新生代 Minor GC
- Java 虚拟机的堆划分:新生代(Eden区,两个同大小的Survival区)
- new 时,会在 Eden 区划出一份内存存储对象,划空间需要同步(堆空间线程共享)
- 每个线程都可以向Java虚拟机申请(加锁)一块连续内存
- Eden 区空间耗尽,触发 Full GC,存活下来的对象放到 Survival 区
- 如果一个对象被复制的次数为15,则对象晋升到老年代;如果单个 Survival 被占用了 50%,较高复制次数的对象也会晋升到老年代
问题:老年代引用新生代,需要做全堆扫描
解决:整个堆化为一个卡,维护卡表,存储每个卡的标识位。标识位代表卡指向新生代对象的引用(脏卡)
在卡表中寻找脏卡,而不用扫描整个老年代
常见的垃圾收集器
垃圾回收器选择
应用程序(如REST服务器)中,考量单个请求的响应时间,
- 单个请求会受停顿时间的影响,尤其是Full GC时较长时间的停顿。如果目标是尽量减少停顿对响应时间的影响,那么并发回收器可能更合适。
- 如果平均响应时间比异常值(例如第90百分位响应时间)更重要,那么非并发回收器可能会产生更好的结果。
- 并发回收器避免长时间停顿是以消耗额外的CPU周期为代价的。如果你的机器缺乏并发回收器所需的空闲CPU周期,那么非并发回收器是更好的选择
批处理应用程序 - 如果有足够的CPU可用,那么使用并发回收器来避免Full GC的停顿可以让任务执行得更快
- 如果CPU有限,那么并发回收器的额外CPU消耗会导致批处理任务花费更多的时间
GC 算法
Serial垃圾回收器:程序运行在 32位JVM 的 Windows上,或者单核CPU(这样的Docker结构)上的默认回收器
- 单线程GC,停止所有的应用线程;Full GC 时,完全压缩老年代
- -XX:+UseSerialGC 开启回收器;Serial垃圾回收器是默认垃圾回收器的系统中,禁用它需要设定另外一个垃圾回收器
Throughput (parallel):两个或者多个CPU的64位机器的默认垃圾回收器 - 多线程回收,停止所有应用线程;Full GC时,完全压缩老年代
- -XX:+UseParallelGC 开启回收器
G1:最小停顿,两个或多个CPU的64位机器上,它是JDK 11和之后版本的默认垃圾回收器 - 多线程回收,新生代时停止所有的应用线程;老年代时不停止线程,将老年代划分为多个区域,垃圾回收时将存活对象从一个区域复制到另一个区域,对小区域进行压缩,减少碎片化
触发GC
做性能监控或基准测试时需要
- 显示调用 System.gc(),禁用方法调用 -XX:+DisableExplicitGC
- jcmd <进程ID> GC.run命令,或者用jconsole连接JVM并单击内存面板上的“执行GC”按钮
GC算法选择
取决于 1. 可用的硬件 2. 应用程序是什么样的 3. 应用程序的性能目标
- JDK 11,G1 GC
- JDK 8,
- 避免 Full GC(需要足够的CPU),G1 GC
- 如果 G1 GC 不是好选择,取决于CPU数:单核 / CPU密集的批处理任务 / 堆很小(比如100 MB)的应用程序,Serial垃圾回收器;CPU密集型任务+多核 CPU/很少Full GC/没有足够的CPU/老年代总是满的,Throughput垃圾回收器
GC 优化基本配置
对于很多应用程序来说,唯一需要优化的是选择合适的GC算法,如果需要的话,增加应用程序的堆大小。自适应大小允许JVM自动优化其行为,使用给定的堆,提供更好的性能
堆大小
- 总数大小
- 分代大小
分代大小
元空间大小
控制并行
堆总数大小
- 堆小,GC执行时间长
- 堆大,GC停顿时间长;并且 Full GC 时访问整个堆,虚拟内存交换会让停顿时间加几个数量级,G1 GC 检查堆要等待数据从磁盘复制到主内存,速度拖慢导致并发失效;
设置规则 - 堆大小(多个JVM堆总和)<= 机器物理内存;也需要为 JVM 原生内存和其他应用程序留出一些空间;操作系统需要至少1G空间
- 配置:初始值-XmsN和最大值-XmxN。默认值与 系统可用资源(操作系统类型、系统RAM总量和使用的JVM、命令行上其他标志) 有关。堆大小的调整是JVM自动优化的核心之一,设置默认值并按需增长最大值
- 如果不需要堆很大,则不需要设置堆大小,只需要设定GC算法的性能目标,比如你能容忍的停顿时间和你想花在GC上的时间百分比,这取决于具体的回收算法(设置的默认值也应该适用于广泛的应用程序,尽量不进行优化)
- 对于特定的JVM容器(虚拟机)如Docker,让堆消耗尽量大的内存,而默认值只分配虚拟机的1/4,更适合运行混合程序的系统
- 堆大小确定:调整堆的大小,让其在Full GC之后,仍然被占用30%。要计算这个值,需要运行应用程序,直到它的配置达到稳定状态,此时它已经加载了需要缓存的所有对象,并且已经创建了最大数量的客户端连接。然后用jconsole连接应用程序,强制执行Full GC,并观察当Full GC完成后有多少内存被占用。(另外,对于Throughput垃圾回收器,如果有GC日志,你可以查阅对应的日志。)如果你采用了这个方法,请调整容器的内存大小(如果可以的话),确保它有额外的0.5 GB~1 GB内存来满足JVM的非堆需求
- 即使明确地设置了最大值,堆也会自动调整。启动时堆的大小为默认的初始值,JVM会增加堆的大小以满足GC算法的性能目标。设定一个比实际需要的更大的堆,并不一定会对内存不利,堆大小只会增长到足以满足GC的性能目标。
- 如果你确切地知道应用程序需要多大的堆,那么不妨将初始值和最大值都设置为该值(例如-Xms4096m-Xmx4096m)。这会让GC稍微高效一点,因为它不再需要弄清楚是否应该调整堆大小。
分代大小
- JVM通常会自动完成这项工作,并且通常会很好地确定新生代和老年代的最佳比例
- 代大小的性能含义:如果新生代相对较大,Young GC的停顿时间会增加,但是新生代的回收频率会降低,晋升到老年代的对象也会更少。但同时,老年代相对会更小,它被填满的频率会更高,会执行更多的Full GC。找到平衡点是关键。
- 优化分代大小的命令行标志,调整的都是新生代的大小,老年代自动得到剩余的所有空间。
- -XX:NewRatio=N设置新生代与老年代的比例。
- -XX:NewSize=N设置新生代的初始值。
- -XX:MaxNewSize=N设置新生代的最大值。
- -XmnN将NewSize和MaxNewSize设置为同一个值的简单写法。
新生代初始大小=堆的初始大小/(1 +NewRatio(default =2)),在默认情况下,新生代的初始大小是堆初始大小的33%。
NewSize 直接设置新生代的值,没有默认值,优先级更高
堆大小↑,新生代大小↑,直到达到最大值,默认新生代最大值=堆的最大值大小/(1 +NewRatio(default =2))
通过设定新生代的最小值和最大值来进行优化是十分困难的。如果堆的大小是固定的(通过设置-Xms等于-Xmx),通常最好也用-Xmn将新生代大小设为固定的。如果应用程序会动态调整堆大小,并且需要更大的(或更小的)新生代,那就重点设置NewRatio的值。
自适应大小
JVM基于过去的性能,即假设未来的GC周期看起来会和最近的GC周期相似,使用策略自动优化,对于许多工作负载来说,这是一个合理的假设,即使分配比例突然改变,JVM也会根据新信息重新调整其大小。
有利于
- 小型应用程序不需要担心过度设定堆的大小。自适应大小确保它们不会使用大量内存。
- 考虑到GC算法的性能目标,JVM可以自动优化堆和代的大小,以使内存的数量分配达到最佳。自适应大小就是让自动优化发挥作用的关键。
不过,调整堆大小需要时间,如果你实现了精准的调整,那就可以禁用自适应大小 -XX:-UseAdaptiveSizePolicy=false(默认值为true)。除了Survivor空间,如果堆的最小值和最大值设置为相同的值,并且新生代的初始值和最大值也设为相同的值,那么自适应大小实际上就被关闭了。
-XX:+PrintAdaptiveSizePolicy=true GC日志可以看到 各个代是如何调整大小的详细信息
元空间大小
JVM加载类时,它必须记录这些类的某些元数据。这些数据占据了一个单独的堆空间,叫作元空间(metaspace)。
元空间并不保存类的实例(例如Class对象)或者反射对象(例如Method对象),这些都保存在常规的堆中。元空间里的信息只在编译器和JVM运行时使用,它所保存的数据被称为类元数据(class metadata)。
并没有很好的方法来提前计算一个特定应用程序所需的元空间大小。元空间的大小与它所使用的类的数量成正比,所以更大的应用程序需要更大的区域。这是JDK技术提升让生活变得更轻松的另一个领域:优化永久代在过去相当普遍,而现在优化元空间相当罕见,主要原因是元空间大小的默认值非常宽裕。
元空间类似于常规堆的一个单独实例。它的大小是根据初始大小(-XX:MetaspaceSize=N)动态调整的,并会根据需要增长到最大值(-XX:MaxMetaspaceSize=N)。
- 增加元空间
- 自动调整元空间的大小会导致Full GC,这是代价高昂的操作。如果在应用程序的启动过程中(它在加载类)有大量的Full GC,往往是因为永久代或元空间正在调整大小,所以在这种情况下,增加元空间的初始值是改善启动过程的好主意
- Java类可以被回收,每次应用程序部署(或重新部署)时,服务器都会创建新的类加载器。之后,旧的类加载器不会再被引用,可以被回收,其中定义的任何类也都如此。同时,应用程序中新的类会有新的元数据,因此元空间必须有足够的空间。元空间需要增长(或丢弃旧的元数据),这通常也会导致Full GC。
- 减少元空间
- 防止类加载器泄漏。应用程序服务器(或其他像IDE一样的应用程序)不断定义新的类加载器和类,并保持对旧的类加载器的引用时,会填满元空间,并消耗机器上的大量内存。类的定义和元数据存储在元空间中,类加载器和类对象存在于Java堆中,堆可能更快填满并抛出OutOfMemoryError异常。
堆转储可以用来诊断存在哪些类加载器,从而帮助判断是否存在类加载器泄漏,并且元空间即将被填满。除此之外,jmap可以和-clstats一起使用,以输出有关类加载器的信息。
- 防止类加载器泄漏。应用程序服务器(或其他像IDE一样的应用程序)不断定义新的类加载器和类,并保持对旧的类加载器的引用时,会填满元空间,并消耗机器上的大量内存。类的定义和元数据存储在元空间中,类加载器和类对象存在于Java堆中,堆可能更快填满并抛出OutOfMemoryError异常。
控制并行
GC 多线程数量控制,-XX:ParallelGCThreads=N(不能设置 G1 GC 的线程数)
GC操作让应用程序停止运行,JVM 尽可能充分利用 CPU 资源,以减少停顿时间。
线程数量
- 默认,每个CPU上运行一个线程,最多同时8个
-
8个CPU,每1.6个CPU会再增加一个新线程,ParallelGCThreads = 8 + ((N - 8) * 5 / 8),N 表示 CPU 数量
在一个有8个CPU的机器上,使用小堆(比如1 GB)的应用程序,如果用4个或者6个线程处理这个堆,效率会稍微高一些。在128个CPU的机器上,除非堆的大小已经达到最大值,否则83个GC线程对于任何情况来说都太多了。
如果机器上运行的JVM不止一个,那么最好限制所有JVM的GC线程总数。
GC 工具
JDK8 开启 GC 日志:-XX:+PrintGCDetails=true (default=false)
GC操作之间的时间:-XX:+PrintGCTimeStamps或-XX:+PrintGCDateStamps 时间戳(time stamp)是相对于0的(基于JVM启动的时间)值,而日期戳(date stamp)是实际的日期字符串
写入文件:-Xloggc:filename,default=标准输出
日志滚动:-XX:+UseGCLogFileRotation、-XX:NumberOfGCLogFiles=N和-XX:GCLogFileSize=N。默认情况下,UseGCLogFileRotation是关闭的。这个标志开启时,默认的文件数量是0(意味着无限多),默认的日志文件大小是0(意味着无限大)。因此,必须给以上标志设定值,才能让日志滚动按预期的方式工作。注意,如果给日志文件设置的大小不足8KB,那么这个数值会向上取整。
-Xloggc:gc.log -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFile=8 -XX:GCLogFileSize=8m
Java基本类型
8个基本类型
- 支持数值运算,在执行效率和内存使用上提升软件性能
- Java 基本类型都有值域和默认值(在内存中都是0)
- 除 boolean,byte、short、int、long、float、double 前面值域包含于后面,因此,无需强制类型转换
- boolean 0/1,char [0,65535] 是 无符号类型。char 的无符号特性适合用作数组索引
基本类型大小
boolean 占 1字节
Java 方法栈上的局部变量
- 在Java 虚拟机规范中,等价于一个数组,可以用正整数来索引。即,除了 long、double 需要两个数组单元存储,其他基本类型和引用类型的值均占一个数组单元。32位Hotspot 上占4字节,32位Hotspot 上占8字节
堆上的字段/数组元素 - byte 1,char 2,short 3 字节
Java 虚拟机规范中,boolean 类型被映射成 int 类型:true-1,false-0,Java 编译器遵守这个编码规则
编译优化
硬件视角:先解释执行字节码,将热点代码(反复运行到的)以方法为单位进行即时编译
- 解释执行(无需等待编译):逐条将字节码翻译成机器码执行
- 即时编译 Just-In-Time(快):一个方法内的所有字节码编译成机器码执行
运行效率
HotSpot 内置多个 JIT,取舍 编译时间和 生成代码的执行效率。默认线程池(线程大小根据CPU数量设置),分层编译:
- C1 编译时间短。C1编译热点方法
- C2 编译时间长。C2编译热点方法中的热点
- 线程比例,C1:C2=1:2
Java 虚拟机不约束编译,由具体的虚拟机实现,现特指 Hotspot 虚拟机
JIT编译器指令重排
前提:单线程 as-if-serial,单线程下执行效果是顺序执行;如果两操作间存在数据依赖,不能调整顺序,否则改变程序语义
工具
VM
# 进程ID
[www@139-bee-dev ~]$ jps
26049 xxx.jar
# JVM 运行时间
[www@139-bee-dev ~]$ jcmd 26049 VM.uptime
26049:
3703522.021 s
# 系统属性
[www@139-bee-dev ~]$ jcmd 26049 VM.system_properties
26049:
#Thu Oct 19 14:45:39 CST 2023
java.runtime.name=Java(TM) SE Runtime Environment
java.protocol.handler.pkgs=org.springframework.boot.loader
sun.boot.library.path=/application/jdk1.8.0_221/jre/lib/amd64
# JVM 版本
[www@139-bee-dev ~]$ jcmd 26049 VM.version
26049:
Java HotSpot(TM) 64-Bit Server VM version 25.221-b11
JDK 8.0_221
# JVM 命令行
[www@139-bee-dev ~]$ jcmd 26049 VM.command_line
26049:
VM Arguments:
jvm_args: -Xms128m -Xmx780m -XX:+HeapDumpOnOutOfMemoryError -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
java_command: /application/8201-early-warning/8201-early-warning-1.0.0-SNAPSHOT-2023-09-06_09_58.jar -jar --spring.profiles.active=dev --spring.cloud.consul.host=192.168.0.78 --spring.cloud.consul.port=32000 --spring.cloud.consul.discovery.acl-token=8dc1eb67-1f5f-4e10-ad9d-5e58b047647c
java_class_path (initial): /application/8201-early-warning/8201-early-warning-1.0.0-SNAPSHOT-2023-09-06_09_58.jar
Launcher Type: SUN_STANDARD
# JVM 调优标志
[www@139-bee-dev 9311-mortgage-service]$ jcmd 26049 VM.flags -all
26049:
[Global flags]
intx ActiveProcessorCount = -1 {product}
bool HeapDumpOnOutOfMemoryError := true {manageable}
# 修改标识:明确指定,一些标志(特别是与GC相关的标志)会影响另一些标志的最终值;如果没有令人信服的理由,那么任何一个标志都不应该更改
# 非默认值标识,其值来源于 1. 命令行设置 2. 其他标识影响 3. JVM自动计算;product 表示所有平台都有这个标识;manageable 标志的值可以在运行期间动态更改;C2 diagnostic 标志可以为编译器工程师提供诊断输出,以了解编译器如何运行
# jinfo 可以修改标识,只对 manageable 修饰的标识有效
线程
# jstack:线程栈轨迹、线程持有锁;死锁检测
[www@139-bee-dev ~]$ jstack 26049
2023-10-19 15:02:49
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.221-b11 mixed mode):
"Attach Listener" #137 daemon prio=9 os_prio=0 tid=0x00007faab0003800 nid=0x88aa waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"kafka-coordinator-heartbeat-thread | operation_info" #84 daemon prio=5 os_prio=0 tid=0x00007faa6c07e800 nid=0x6631 waiting on condition [0x00007faa527c8000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d2d17048> (a java.util.concurrent.locks.ReentrantLock$FairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$FairSync.lock(ReentrantLock.java:224)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at org.apache.kafka.clients.consumer.internals.ConsumerNetworkClient.poll(ConsumerNetworkClient.java:249)
at org.apache.kafka.clients.consumer.internals.ConsumerNetworkClient.pollNoWakeup(ConsumerNetworkClient.java:306)
at org.apache.kafka.clients.consumer.internals.AbstractCoordinator$HeartbeatThread.run(AbstractCoordinator.java:1386)
- locked <0x00000000d2ce6550> (a org.apache.kafka.clients.consumer.internals.ConsumerCoordinator)
"kafka-coordinator-heartbeat-thread | risking_info" #85 daemon prio=5 os_prio=0 tid=0x00007faa580a1000 nid=0x6630 in Object.wait() [0x00007faa528c9000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at org.apache.kafka.clients.consumer.internals.AbstractCoordinator$HeartbeatThread.run(AbstractCoordinator.java:1418)
- locked <0x00000000d2c51560> (a org.apache.kafka.clients.consumer.internals.ConsumerCoordinator)
[www@139-bee-dev ~]$ jstack 26049 > thread_dump.txt
[www@139-bee-dev ~]$ grep Thread.State thread_dump.txt | awk '{print $2$3$4$5}' | sort | uniq -c
45 RUNNABLE
2 TIMED_WAITING(onobjectmonitor)
8 TIMED_WAITING(parking)
4 TIMED_WAITING(sleeping)
2 WAITING(onobjectmonitor)
17 WAITING(parking)
类
类数量和编译信息
jstat
GC 和 类加载信息
第12章,第4章
ASM
字节码分析和修改框架,应用于 代码覆盖测试工具 JaCoCo,Java8 Lambda 的适配器类
生成新的 class文件,修改已有的 class文件
// 编译
javac Foo.java
// 查看字节码 -p打印私有方法和字段;-v class文件;常量池;字段区域;方法区域
javap -p -v Foo
GC
第五章
jmap
Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
Java堆分析器,计算对象的保留大小,查看谁在阻止垃圾收集器收集对象
- Java 进程堆 中存放的 Java 对象
[vmroot@101-efq application]$ whereis java
java: /usr/local/java /usr/local/java/jdk1.8.0_151/bin/java /usr/local/java/jdk1.8.0_151/jre/bin/java
# 生成 Java 堆转储文件
1. ps - ef | grep <xxx> Java 进程,启动信息
2. jmap -dump:file=heap_dump.hprof <pid> Java自带工具
3. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof 启用时 JVM参数
# mat 打开堆转储文件
JMC
java mission control,JVM 性能监控,组件 Java Flight Recorder,收集Java 性能数据
不依赖安全点机制
- 瞬时:发生与否的异常、线程启动
- 持续:垃圾收集
- 计时:时长超出阈值的事件
- 取样:周期性取样事件
JMH
JVM、操作系统、硬件系统会影响运行效果
输出指标 - Scope:平均吞吐量
- Error:误差范围
命令
Oracle Help Center - Search-java8 vm
更近一步
JVM 异常处理
JVM 泛型支持
运行内存故障
性能分析
性能分析工具通过 Socket 或 JVM 原生接口交互,可能会产生大量数据,给工具开启并发GC
性能分析两种模式
- 采样 1. Oracle Developer Studio分析器 2. 开源项目async-profiler 火焰图
- 探查:引起性能偏差 ; Oracle Developer Studio 检查线程的执行模式,而不是方法上的等待时间
- 线程阻塞是不是性能问题的原因,需要检查它们为什么被阻塞
- 通过正在阻塞的方法或者线程时间线分析,可以分辨阻塞的线程
- 如果原生分析器显示主要占用CPU资源的是GC时间,那么优化垃圾回收器是正确的选择。如果它显示编译线程占用大量时间,那么这通常不会影响应用程序的性能。
JFR
Java飞行记录器(Java Flight Recorder)是JVM的一项功能,可以对运行中的应用程序进行轻量级性能分析,对性能影响最多1%,可通过 jcmd 来配置
JFR会记录异常事件,如线程因为等待锁而被阻塞的事件,保存到循环缓冲区(只展示最近事件)
Java Mission Control(jmc),检查 JFR 记录
- 内存:清理引用对象的数量及用时,并发回收过程中是否有对象晋升或疏散失败,GC算法的配置参数(包括代的大小和Survivor空间的配置),甚至是已经分配的特定种类对象的信息
- 代码:热点方法和被分析代码的调用树、异常和缓存
- 线程、I/O和系统事件
JIT 编译
调整热点代码阈值
调整 Code Cache 大小
JIT编译的代码存储在 Code Cache中
调整编译器线程数,或选择适当的编译器模式
client 默认一个编译线程,server default 2个;分层编译根据CPU个数配置
JVM参数
# 查找
man java | grep "heap size"
# java 默认值配置
[vmroot@101-efq application]$ java -XX:+PrintFlagsFinal |grep HeapDumpOnOutOfMemoryError
bool HeapDumpOnOutOfMemoryError = false {manageable}
The best HotSpot JVM options and switches for Java 11 through Java 17 (oracle.com)
下面是一些常用的JVM参数设置,可以用于调整Java虚拟机的行为和性能:
- -Xms: 设置JVM的初始堆大小。
例如:-Xms512m 表示初始堆大小为512MB。 - -Xmx: 设置JVM的最大堆大小。
例如:-Xmx1024m 表示最大堆大小为1GB。 - -Xss: 设置每个线程的栈大小。
例如:-Xss256k 表示每个线程的栈大小为256KB。 - -XX:NewRatio: 设置年轻代(Young Generation)与老年代(Old Generation)的比例。
例如:-XX:NewRatio=2 表示年轻代与老年代的比例为1:2。 - -XX:MetaspaceSize: 设置元空间(Metaspace)的初始大小。
例如:-XX:MetaspaceSize=128m 表示元空间的初始大小为128MB。 - -XX:MaxMetaspaceSize: 设置元空间的最大大小。
例如:-XX:MaxMetaspaceSize=256m 表示元空间的最大大小为256MB。 - -XX:+UseParallelGC: 启用并行垃圾回收器。
例如:-XX:+UseParallelGC 表示启用并行垃圾回收器。 - -XX:+UseConcMarkSweepGC: 启用并发标记清除垃圾回收器。
例如:-XX:+UseConcMarkSweepGC 表示启用并发标记清除垃圾回收器。 - -XX:MaxGCPauseMillis: 设置垃圾回收的最大停顿时间。
例如:-XX:MaxGCPauseMillis=100 表示垃圾回收的最大停顿时间为100毫秒。 - -XX:+DisableExplicitGC: 禁用显式调用System.gc()方法触发垃圾回收。
例如:-XX:+DisableExplicitGC 表示禁用显式调用System.gc()方法。
这些只是一些常用的JVM参数设置,具体的参数根据应用需求和系统环境可能会有所不同。可以根据具体情况进行调整和优化,以获得最佳的性能和资源利用。
Docker
Docker 容器只是操作系统的一个进程(可能受资源限制),后期 Java8 可以自动设置合理的内存和CPU数量
参考文档
https://dmetzler.github.io/troubleshooting-java-apps-in-k8s/
https://www.baeldung.com/jvm-parameters
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/clopts001.html
https://github.com/peachyy/jvmdump4k8s