JVM
线程状态转换图
线程状态(State
枚举值代表线程状态)
- 新建状态( NEW): 线程刚创建, 尚未启动。
Thread thread = new Thread()
。 - 可运行状态(RUNNABLE): 线程对象创建后,其他线程(比如 main 线程)调用了该对象的
start
方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。 - 运行(running): 线程获得 CPU 资源正在执行任务(
run()
方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束 - 阻塞状态(Blocked): 线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。
sleep
,suspend
,wait
等方法都可以导致线程阻塞 - 等待(WAITING): 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING): 该状态不同于
WAITING
,它可以在指定的时间后自行返回。 - 终止(TERMINATED): 表示该线程已经执行完毕,如果一个线程的
run
方法执行结束或者调用stop
方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start
方法令其进入就绪。
线程在Running的过程中可能会遇到阻塞(Blocked)情况
- 调用
join()
和sleep()
方法,sleep()
时间结束或被打断,join()
中断,IO完成都会回到Runnable
状态,等待JVM的调度。 - 调用
wait()
,使该线程处于等待池(wait blocked pool),直到notify()
/notifyAll()
,线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable) - 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
JVM
Java Virtual Machine是一种虚拟机,它是Java程序运行的环境,JVM提供了Java程序的运行时环境,包括内存管理、垃圾回收、安全性、类加载等功能,是Java语言的核心,它使]ava语言具有跨平台的特性(一次编译处处运行)。
常见JVM实现
除了以下几种JVM的具体实现,还有一些其他的实现
这些JVM实现在实现上会有一些差异,但是它们的基本功能是相同的
JVM实现的差异主要体现在性能、稳定性、兼容性等方面
不同的JVM实现在不同的场景下可能会有不同的表现
虚拟机名称 | 说明 |
---|---|
HotSpot | 是SunjDK和OpenjDK中所带的虚拟机,也是目前使用范围最广的ava虚拟机,主要使用C4+实现,JNI接口部分用C实现 |
JRockit | 除 HotSpot 之外另一款比较厉害的 VM,一开始是BEA公司的,一度获取运行最快的虚拟机。Oracle 在 2008 年收购了 BEA 公司,jRockit 与 HotSpot 同属于 Oracle |
J9 VM | 基于IBM公司开发的IDK,主要应用在IBM公司开发的的软件或服务器端,如:嵌入式、服务端、桌面等,基本上IBM本司出品的产品都是用的J9 VM |
JDK与JRE的关系
JVM主要组成部分
+---------------------------+--------------------
| 类加载子系统 (Class Loader Subsystem) |
+---------------------------+--------------------
| 运行时数据区 (Runtime Data Area) |
| - 方法区 (Method Area) |
| - 堆 (Heap) |
| - Java 栈 (Java Stacks) |
| - 本地方法栈 (Native Method Stacks) |
| - 程序计数器 (Program Counter) |
+---------------------------+--------------------
| 执行引擎 (Execution Engine) |
| - 解释器 (Interpreter) |
| - 即时编译器 (Just-In-Time Compiler, JIT) |
| - 垃圾收集器 (Garbage Collector, GC) |
+---------------------------+--------------------
| 本地接口 (Native Interface) |
+---------------------------+--------------------
| 本地方法库 (Native Method Libraries) |
+---------------------------+--------------------
JVM内存组成部分和堆空间分布
JVM内存的5大组成
(基于JDK8的Hotipot虚拟机,不同虚拟机不同版本会有不一样)
名称 | 作用 | 特点 |
---|---|---|
程序计数器 | 也叫PC寄存器,用于记录当前线程执行的字节码指令位置,以便线程在恢复执行时能够从正确的位置开始 | 线程私有 |
Java虚拟机栈 | 用于存储Java方法执行过程中的局部变量、方法参数和返回值,以及方法执行时的操作数栈 | 线程私有 |
本地方法栈 | 用于存储Java程序调用本地方法的参数和返回值等信息 | 线程私有 |
堆 | 用于存储Java程序创建的对象,所有线程共享一个堆,堆中的对象可以被垃圾回收器回收,以便为新的对象分配空间 | 线程共享 |
元数据区 | 用于存储类的元数据信息,如类名、方法名、字段名等,以及动态生成的代理类、动态生成的字节码等 | 线程共享 |
堆空间分布
用于存储Java程序创建的对象,所有线程共享一个堆
堆中的对象可以被垃圾回收器回收,以便为新的对象分配空间
JVM堆空间垃圾回收流程
- 新建对象,放到Eden区,满后触发Minor GC(每次都是由Eden区满触发Minor GC,接连放对象到S0或S1)
- 如果存活的对象移动到Survivor的S0区,然后Eden区空闲,如果S0满后触发MinorGC;S0存活下来的对象移动到S1区,然后S0区空闲
- 如果存活的对象移动到Survivor的S1区,S1满后触发MinorGC,再次移动到S0区,然后S1区空闲
- 反复GC每次对象涨1岁,到达一定次数后(默认15),进入老年代
- 当老年代内存不足会触发FullGC,出现STW(Stop-The-World)
- 堆被垃圾回收,基本都是采用分代收集算法,不同区域采用不同的垃圾回收算法
- 方法结束后,堆中的对象不会马上移除,在垃圾回收的时候才会被移除(不是实时GC的,ThreadLocal里面说过)
堆空间分配比例
内存垃圾回收相关JVM参数调整
JVM参数格式分类
格式 | 解释 | 例子 |
---|---|---|
标准参数(-) | 所有JVM都实现这些参数的功能 | -verbose:gc 打印GC简要信息 |
非标准参数(-X) | 不保证所有JVM实现都满足 | -Xmx2048m 等价 -XX:MaxHeapSize JVM最大堆内存为2048M |
非稳定参数(-XX) | 不稳定未来可能取消,但很有用 | -XX:+PrintGCDetials 每次GC时打印详细信息 |
-XX:+ | 开启对应的参数 | -XX:-UseSerialGC 启用串行GC |
-XX:- | 关闭对应的参数 | -XX:-DisableExplicitGC 禁止调用System.gc() |
-XX:= | 设定数字参数 | -XX:NewRatio=2 新生代和老年代内存比例 |
JVM堆栈内存配置参数
参数 | 解释 |
---|---|
-Xms | 初始堆大小,推荐和最大堆一样 |
-Xmx | 最大堆大小,推荐和初始堆一样 |
-Xmn | 年轻代大小 |
-Xss | 每个线程的堆栈大小 |
JVM常见的命令行参数配置
参数 | 解释 |
---|---|
-XX:+PrintGCDetails | 打印GC回收信息 |
-XX:NewRatio | 新生代和老年代空间大小的比率,由 -XX:NewRatio 参数控制;-XX:NewRatio 参数的默认值是2,表示新生代和老年代的比例是1:2;如果将 -XX:NewRatio 设置为4,表示新生代和老年代的比例是1:4 |
--:MaxMetaspaceSize | 元空间所分配内存的最大值,默认没限制 |
-XX:+UseConcMarkSweepGC | 设置并发收集器 |
JVM虚拟机栈参数调整(虚拟机栈溢出)
JVM虚拟机栈
- 用来存储Java程序中的方法调用和局部变量的内存区域
- 每个线程都有自己的虚拟机栈,其生命周期与线程相同
- 当一个方法被调用时,Java虚拟机会在该线程的虚拟机栈中创建一个栈帧,用来存储该方法的局部变量、方法返回值等信息
- 默认情况下,JVM虚拟机栈的大小是固定的,JDK1.5后通常为1MB
- 如果线程在执行方法时需要更多的栈空间,在空间不足时JVM会抛出StackOverflowError异常
- 调整栈空间:JVM参数 xss,比如 -xss1m 表示1MB
查看当前栈帧信息
package com.soulboy.jvm;
public class StackFrameDemo {
public static void main(String[] args) {
StackFrameDemo stackFrameDemo = new StackFrameDemo();
stackFrameDemo.method1();
}
public void method1(){
String str = "Hello";
method2(str);
System.out.println("method1--完成");
}
private void method2(String str) {
int num = 123;
method3(str, num);
System.out.println("method2--完成");
}
private void method3(String str, int num) {
double d = 3.14;
method4(str, num, d);
System.out.println("method3--完成");
}
private void method4(String str, int num, double d) {
//查看当前栈帧信息
System.out.println("打印当前栈帧信息");
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
System.out.println(stackTraceElement.toString());
}
System.out.println("method4--完成");
}
}
输出
打印当前栈帧信息
java.base/java.lang.Thread.getStackTrace(Thread.java:1610)
com.soulboy.jvm.StackFrameDemo.method4(StackFrameDemo.java:29)
com.soulboy.jvm.StackFrameDemo.method3(StackFrameDemo.java:22)
com.soulboy.jvm.StackFrameDemo.method2(StackFrameDemo.java:16)
com.soulboy.jvm.StackFrameDemo.method1(StackFrameDemo.java:10)
com.soulboy.jvm.StackFrameDemo.main(StackFrameDemo.java:6)
method4--完成
method3--完成
method2--完成
method1--完成
虚拟机栈溢出
栈越小,递归调用的次数就越少,因为栈空间不足导致栈溢出异常
栈越大,递归调用的次数就越多,因为有足够的栈空间来存储方法调用的信息
public class StackSizeDemo {
private static int count = 0;
public static void main(String[] args) {
try {
recursiveMethod();
} catch (Throwable t) {
System.out.println("栈溢出,运行 " + count + " 次后抛出异常");
t.printStackTrace();
}
}
private static void recursiveMethod() {
count++;
recursiveMethod();
System.out.println("递归次数:" + count);
}
}
### 调整虚拟机栈大小之前输出
栈溢出,运行 27709 次后抛出异常
### 调整虚拟机栈大小之前输后 -Xss100k
The Java thread stack size specified is too small. Specify at least 180k
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
### 调整虚拟机栈大小之前输后 -Xss180k
栈溢出,运行 1696 次后抛出异常
JVM虚拟机堆参数调整(Jmeter5压测)
通过调整不同的VM堆参数,查看相关指标
调整参数一:-Xms64m -Xmx64m
调整参数二:-Xms640m -Xmx640m
压测接口URI: http://192.168.10.88:8080/api/v1/data/compute
Controller
package com.soulboy.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/data")
public class DataController {
@RequestMapping("compute")
public String compute(){
//每次申请1MB大小(在堆中)
Byte[] bytes = new Byte[1 * 1024 * 1024];
return "success";
}
}
调整参数一:-Xms64m -Xmx64m
控制台输出
java.lang.OutOfMemoryError: Java heap space
调整参数二:-Xms640m -Xmx640m
控制台输出无错误输出
GC Root(Garbage Collection Root)
GC Root(Garbage Collection Root)是指在Java虚拟机中被直接或间接引用的对象集合,它们被认为是存活对象,不能被垃圾回收器回收。GC Root包括以下几种类型:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- .Native方法中引用的对象
- 活动线程中的对象
- 当前类加载器加载的类的对象
GC Root的作用是为垃圾回收器提供一个初始的扫描位置,以便确定哪些对象是可达的,哪些对象是不可达的,垃圾回收器会从GC Root开始扫描,并标记所有可达对象,最终将不可达对象回收掉。
栈、堆、方法区的交互关系
从线程共享与否的角度来看
栈、堆、方法区的交互关系
-
Person:存放在元空间,也可以说方法区
-
person:存放在Java栈的局部变量表中
-
new Person():存放在Java堆
元空间(JDK8之后的方法区的实现)
方法区
是JVM中用来存储类的元数据信息的区域,包括类的结构、方法、字段信息等,Java堆类似各个线程共享的内存区域;方法区主要存放的是 Class
,而堆中主要存放的是实例化的对象
方法区(Method Area)
与Java堆
一样,是各个线程共享的内存区域。- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:
Java.lang.OutofMemoryError:PermGen space
或者java.lang.OutOfMemoryError:Metaspace
- 加载大量的第三方的 jar 包,Tomcat部署的工程过多(30~50个),大量动态的生成反射类
- 关闭JVM就会释放这个区域的内存。
元空间(永久代)
元空间(永久代)是方法区具体的落地实现,java8之前是称为永久代(PermGen),java8后引入的一个新概念【元空间】用于替代旧版JVM中的永久代(PermGen)
- 方法区和永久代以及元空间的关系很像Java 中接口和类的关系
- 类实现了接口,类就可以看作是永久代和元空间,接口可以看作是方法区
- 永久代是JDK1.8 之前的方法区实现,JDK1.8 及以后方法区的实现便成为元空间
元空间的大小是动态的,可以根据需要进行自动扩展
- 永久代(Permanent Generation):在 JDK 8 之前,永久代用于存储类的元数据、常量池、方法信息等。永久代的大小是固定的,容易导致OutOfMemoryError错误。
- 元空间(Metaspace):从 JDK 8 开始,永久代被元空间取代。元空间不在 JVM 堆中,而是使用本地内存。元空间的大小可以动态调整,减少了OutOfMemoryError的风险。
如果元空间不足IM会抛出
OutOfMemoryError:Metaspace`
元空间大小配置
这两个参数的单位可以是:字节(B),K、M、G等后缀来表示更大的单位
参数 | 说明 |
---|---|
-XX:MetaspaceSize | 用来设置元空间初始大小的参数,它的默认值是21 MB |
-XX:MaxMetaspaceSize | 用来设置元空间最大大小的参数,它的默认值是-1即不限制,使用的是本地内存,不像旧版的永久代是堆内存;如果不限制元空间的大小,可能会导致元空间占用过多的内存,从而引起内存溢出 |
查看元空间大小
代码
package com.soulboy.jvm;
public class HeapDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello World!");
Thread.sleep(10000000);
}
}
查看命令
### jsp 查看进程号
D:\Project\redlock\src\main\java\com\soulboy\thread>jps
20052 HeapDemo
### jinfo 查看 Metaspace分配内存空间 默认是21MB 22020096/1024/1024=21
jinfo -flag MetaspaceSize 20052
-XX:MetaspaceSize=22020096
### jinfo 查看 Metaspace最大空间
jinfo -flag MaxMetaspaceSize 20052
-XX:MaxMetaspaceSize=18446744073709551615
调整:自定义Metaspace大小
# 调整
-XX:MetaspaceSize=126m -XX:MaxMetaspaceSize=524m
# 调整后查看 132120576 ÷ 1024 ÷ 1024 = 126
jinfo -flag MetaspaceSize 10784
-XX:MetaspaceSize=132120576
# 调整后查看 49453824 ÷ 1024 ÷ 1024 = 524
jinfo -flag MaxMetaspaceSize 10784
-XX:MaxMetaspaceSize=549453824
空间不足异常信息 抛出Java.Lang.OutOfMemoryError: Metaspace
什么时候容易出现元空间不足的情况
- 应用程序使用大量的反射技术,例如使用Class.forName()等方法加载类,或者使用java Reflection API进行操作
- 使用大量的动态代理技术,例如使用JavaProxy类等技术
- 使用大量的注解,例如使用Spring框架的注解等
- 使用的第三方库过多,这些库可能会在运行时动态生成新的类,导致元空间内存占用过多。
- 应用程序的业务逻辑比较复杂,需要加载大量的类
- 应用程序使用大量的JSP页面,其中每个页面都对应一个类文件(现在很少)
!!!!!!!!!!!
JVM的类加载子系统
类加载子系统
它java虚拟机的一个重要子系统,主要负责将类的字节码加载到VM内存的方法区,并将其转换为JVM内部的数据结构
类加载子系统的三大特点
双亲委派模型
- Java虚拟机采用双亲委派模型来加载类,即先从父类加载器中查找类,如果找到了就直接返回
- 否则再由自己的加载器加载,这种模型可以避免类的重复加载,提高系统的安全性。
延迟加载
- Java虚拟机采用延迟加载的策略,即只有在需要使用某个类时才进行加载
- 这种策略可以减少系统启动时间,提高系统的性能
动态加载
- Java虚拟机支持动态加载类,即可以在程序运行时动态地加载和卸载类
- 这种特性可以使java程序更加灵活和可扩展
类加载子系统的组成(三个模块)
加载器(ClassLoader)
加载器负责将类的字节码加藏到JVM中
三种类型的加载器
类加载器用父类加载器、子类加载器这样的名字,虽然看似是继承关系,实际上是组合(Composition)关系
类加载器 | 功能描述 |
---|---|
启动类加载器(Bootstrap ClassLoader) | c/c++实现,加载核心类库使用,它不继承ClassLoader,没父加载器 |
平台类加载器(Platform ClassLoader) | JDK9之前是扩展类加载器 ExtensionClassLoader |
应用程序类加载器(Application ClassLoader) | 程序的默认加载器,我们写的代码基本都是由这个加载器负责加载 |
链接器(Linker)
负责将java类的二进制代码链接到ava虚拟机中,并生成可执行的java虚拟机代码,包括 验证、准备和解析等
- 验证操作:主要是验证类的字节码是否符合JVM规范
- 准备操作:主要是为类的静态变量分配内存并设置默认值
- 解析操作:主要是将符号引用转换为直接引用
初始化器(Initializer)
负责执行lava类的静态初始化,包括静态变量的赋值、静态代码块的执行等
初始化器是类加载子系统的最后一个阶段
双亲委派机制
为什么需要双亲委派机制?
- java.lang.Object 这些存放在rt.jar中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载
- 不同加载器加载的Object类都是同一个,如果没有使用双亲委派模型,各个类加载器自行去加载的话,就会出现问题
比如用户编写了一个称为java.lang.0bject或String的类,并放在classpath下,那系统将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证
双亲委派模型
- 是一种类加载机制,类加载器之间形成了一条类加载器链,每个类加载器都有一个父类加载器,形成了从下到上的一条继承链
- 如果所有的类加载器都无法加载该类,则会抛出 ClassNotFoundException 异常
加载流程
- 一个类的加载请求首先会被委派给其父类加载器进行处理
- 如果父类加载器无法加载该类,则会将加载请求委派给其自身进行加载
- 如果自身也无法加载该类,则会将加载请求委派给其子类加载器进行处理,直到找到能够加载该类的类加载器为止
优点
- 可以保证类的唯一性和安全性。由于每个类加载器都只能加载自己的命名空间中的类
- 由于类加载器之间形成了一条继承链,因此可以保证类的安全性,防止恶意代码的注入
JDK9模块化系统
是一种新的java平台的组织方式,将java SE分成多个模块,每个模块都有自己的API和实现
每个模块都有一个唯一的标识符和版本号,可以独立地进行开发、测试、部署和维
模块之间的依赖关系通过模块描述文件(module-info.java)来声明,这个文件包含模块的名称、版本号、导出的包、依赖的模块等信息。
在编译和运行时,模块系统会根据模块描述文件来加载和链接模块,确保模块之间的依赖关系正确
JDK8
JDK17(JDK9开始使用模块化)
C:\Tools\Java\JDK17!\java.base\module-info.class
/*
* Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
/**
* Defines the foundational APIs of the Java SE Platform.
*
* <dl class="notes">
* <dt>Providers:</dt>
* <dd> The JDK implementation of this module provides an implementation of
* the {@index jrt jrt} {@linkplain java.nio.file.spi.FileSystemProvider
* file system provider} to enumerate and read the class and resource
* files in a run-time image.
* The jrt file system can be created by calling
* {@link java.nio.file.FileSystems#newFileSystem
* FileSystems.newFileSystem(URI.create("jrt:/"))}.
* </dd>
* </dl>
*
* @toolGuide java java launcher
* @toolGuide keytool
*
* @provides java.nio.file.spi.FileSystemProvider
*
* @uses java.lang.System.LoggerFinder
* @uses java.net.ContentHandlerFactory
* @uses java.net.spi.URLStreamHandlerProvider
* @uses java.nio.channels.spi.AsynchronousChannelProvider
* @uses java.nio.channels.spi.SelectorProvider
* @uses java.nio.charset.spi.CharsetProvider
* @uses java.nio.file.spi.FileSystemProvider
* @uses java.nio.file.spi.FileTypeDetector
* @uses java.security.Provider
* @uses java.text.spi.BreakIteratorProvider
* @uses java.text.spi.CollatorProvider
* @uses java.text.spi.DateFormatProvider
* @uses java.text.spi.DateFormatSymbolsProvider
* @uses java.text.spi.DecimalFormatSymbolsProvider
* @uses java.text.spi.NumberFormatProvider
* @uses java.time.chrono.AbstractChronology
* @uses java.time.chrono.Chronology
* @uses java.time.zone.ZoneRulesProvider
* @uses java.util.spi.CalendarDataProvider
* @uses java.util.spi.CalendarNameProvider
* @uses java.util.spi.CurrencyNameProvider
* @uses java.util.spi.LocaleNameProvider
* @uses java.util.spi.ResourceBundleControlProvider
* @uses java.util.spi.ResourceBundleProvider
* @uses java.util.spi.TimeZoneNameProvider
* @uses java.util.spi.ToolProvider
* @uses javax.security.auth.spi.LoginModule
*
* @moduleGraph
* @since 9
*/
module java.base {
exports java.io;
exports java.lang;
exports java.lang.annotation;
exports java.lang.constant;
exports java.lang.invoke;
exports java.lang.module;
exports java.lang.ref;
exports java.lang.reflect;
exports java.lang.runtime;
exports java.math;
exports java.net;
exports java.net.spi;
exports java.nio;
exports java.nio.channels;
exports java.nio.channels.spi;
exports java.nio.charset;
exports java.nio.charset.spi;
exports java.nio.file;
exports java.nio.file.attribute;
exports java.nio.file.spi;
exports java.security;
exports java.security.cert;
exports java.security.interfaces;
exports java.security.spec;
exports java.text;
exports java.text.spi;
exports java.time;
exports java.time.chrono;
exports java.time.format;
exports java.time.temporal;
exports java.time.zone;
exports java.util;
exports java.util.concurrent;
exports java.util.concurrent.atomic;
exports java.util.concurrent.locks;
exports java.util.function;
exports java.util.jar;
exports java.util.random;
exports java.util.regex;
exports java.util.spi;
exports java.util.stream;
exports java.util.zip;
exports javax.crypto;
exports javax.crypto.interfaces;
exports javax.crypto.spec;
exports javax.net;
exports javax.net.ssl;
exports javax.security.auth;
exports javax.security.auth.callback;
exports javax.security.auth.login;
exports javax.security.auth.spi;
exports javax.security.auth.x500;
exports javax.security.cert;
exports com.sun.crypto.provider to jdk.crypto.cryptoki;
exports com.sun.security.ntlm to java.security.sasl;
exports jdk.internal.access to
java.desktop,
java.logging,
java.management,
java.naming,
java.rmi,
jdk.charsets,
jdk.incubator.foreign,
jdk.jartool,
jdk.jfr,
jdk.jlink,
jdk.net;
exports jdk.internal.access.foreign to jdk.incubator.foreign;
exports jdk.internal.event to jdk.jfr;
exports jdk.internal.invoke to jdk.incubator.foreign;
exports jdk.internal.javac to
java.compiler,
jdk.compiler,
jdk.jshell;
exports jdk.internal.jimage to jdk.jlink;
exports jdk.internal.jimage.decompressor to jdk.jlink;
exports jdk.internal.jmod to
jdk.compiler,
jdk.jlink;
exports jdk.internal.loader to
java.instrument,
java.logging,
java.naming,
jdk.incubator.foreign;
exports jdk.internal.logger to java.logging;
exports jdk.internal.misc to
java.desktop,
java.logging,
java.management,
java.naming,
java.net.http,
java.rmi,
java.security.jgss,
jdk.attach,
jdk.charsets,
jdk.compiler,
jdk.crypto.cryptoki,
jdk.incubator.foreign,
jdk.incubator.vector,
jdk.internal.vm.ci,
jdk.jfr,
jdk.jshell,
jdk.nio.mapmode,
jdk.unsupported;
exports jdk.internal.module to
java.instrument,
java.management.rmi,
jdk.incubator.foreign,
jdk.jartool,
jdk.jfr,
jdk.jlink,
jdk.jpackage;
exports jdk.internal.org.objectweb.asm to
jdk.jartool,
jdk.jfr,
jdk.jlink;
exports jdk.internal.org.objectweb.asm.commons to jdk.jfr;
exports jdk.internal.org.objectweb.asm.tree to
jdk.jfr,
jdk.jlink;
exports jdk.internal.org.objectweb.asm.util to jdk.jfr;
exports jdk.internal.org.xml.sax to jdk.jfr;
exports jdk.internal.org.xml.sax.helpers to jdk.jfr;
exports jdk.internal.perf to
java.management,
jdk.internal.jvmstat,
jdk.management.agent;
exports jdk.internal.platform to
jdk.jfr,
jdk.management;
exports jdk.internal.ref to
java.desktop,
jdk.incubator.foreign;
exports jdk.internal.reflect to
java.logging,
java.sql,
java.sql.rowset,
jdk.dynalink,
jdk.incubator.foreign,
jdk.internal.vm.ci,
jdk.unsupported;
exports jdk.internal.util to jdk.incubator.foreign;
exports jdk.internal.util.jar to jdk.jartool;
exports jdk.internal.util.random to jdk.random;
exports jdk.internal.util.xml to jdk.jfr;
exports jdk.internal.util.xml.impl to jdk.jfr;
exports jdk.internal.vm to
jdk.internal.jvmstat,
jdk.management.agent;
exports jdk.internal.vm.annotation to
java.instrument,
jdk.incubator.foreign,
jdk.incubator.vector,
jdk.internal.vm.ci,
jdk.jfr,
jdk.unsupported;
exports jdk.internal.vm.vector to jdk.incubator.vector;
exports sun.invoke.util to
jdk.compiler,
jdk.incubator.foreign;
exports sun.net to
java.net.http,
jdk.naming.dns;
exports sun.net.dns to
java.security.jgss,
jdk.naming.dns;
exports sun.net.ext to jdk.net;
exports sun.net.util to
java.desktop,
java.net.http,
jdk.jconsole,
jdk.sctp;
exports sun.net.www to
java.net.http,
jdk.jartool;
exports sun.net.www.protocol.http to java.security.jgss;
exports sun.nio.ch to
java.management,
jdk.crypto.cryptoki,
jdk.incubator.foreign,
jdk.net,
jdk.sctp;
exports sun.nio.cs to jdk.charsets;
exports sun.nio.fs to
jdk.net,
jdk.zipfs;
exports sun.reflect.annotation to jdk.compiler;
exports sun.reflect.generics.reflectiveObjects to java.desktop;
exports sun.reflect.misc to
java.datatransfer,
java.desktop,
java.management,
java.management.rmi,
java.rmi,
java.sql.rowset;
exports sun.security.action to
java.desktop,
java.security.jgss,
jdk.crypto.ec,
jdk.incubator.foreign;
exports sun.security.internal.interfaces to jdk.crypto.cryptoki;
exports sun.security.internal.spec to
jdk.crypto.cryptoki,
jdk.crypto.mscapi;
exports sun.security.jca to
java.smartcardio,
jdk.crypto.cryptoki,
jdk.crypto.ec,
jdk.naming.dns;
exports sun.security.pkcs to
jdk.crypto.ec,
jdk.jartool;
exports sun.security.provider to
java.rmi,
java.security.jgss,
jdk.crypto.cryptoki,
jdk.crypto.ec,
jdk.security.auth;
exports sun.security.provider.certpath to
java.naming,
jdk.jartool;
exports sun.security.rsa to
jdk.crypto.cryptoki,
jdk.crypto.mscapi;
exports sun.security.timestamp to jdk.jartool;
exports sun.security.tools to jdk.jartool;
exports sun.security.util to
java.desktop,
java.naming,
java.rmi,
java.security.jgss,
java.security.sasl,
java.smartcardio,
java.xml.crypto,
jdk.crypto.cryptoki,
jdk.crypto.ec,
jdk.crypto.mscapi,
jdk.jartool,
jdk.security.auth,
jdk.security.jgss;
exports sun.security.util.math to jdk.crypto.ec;
exports sun.security.util.math.intpoly to jdk.crypto.ec;
exports sun.security.validator to jdk.jartool;
exports sun.security.x509 to
jdk.crypto.cryptoki,
jdk.crypto.ec,
jdk.jartool;
exports sun.util.cldr to jdk.jlink;
exports sun.util.locale.provider to
java.desktop,
jdk.jlink,
jdk.localedata;
exports sun.util.logging to
java.desktop,
java.logging,
java.prefs;
exports sun.util.resources to jdk.localedata;
uses java.lang.System.LoggerFinder;
uses java.net.ContentHandlerFactory;
uses java.net.spi.URLStreamHandlerProvider;
uses java.nio.channels.spi.AsynchronousChannelProvider;
uses java.nio.channels.spi.SelectorProvider;
uses java.nio.charset.spi.CharsetProvider;
uses java.nio.file.spi.FileSystemProvider;
uses java.nio.file.spi.FileTypeDetector;
uses java.security.Provider;
uses java.text.spi.BreakIteratorProvider;
uses java.text.spi.CollatorProvider;
uses java.text.spi.DateFormatProvider;
uses java.text.spi.DateFormatSymbolsProvider;
uses java.text.spi.DecimalFormatSymbolsProvider;
uses java.text.spi.NumberFormatProvider;
uses java.time.chrono.AbstractChronology;
uses java.time.chrono.Chronology;
uses java.time.zone.ZoneRulesProvider;
uses java.util.random.RandomGenerator;
uses java.util.spi.CalendarDataProvider;
uses java.util.spi.CalendarNameProvider;
uses java.util.spi.CurrencyNameProvider;
uses java.util.spi.LocaleNameProvider;
uses java.util.spi.ResourceBundleControlProvider;
uses java.util.spi.ResourceBundleProvider;
uses java.util.spi.TimeZoneNameProvider;
uses java.util.spi.ToolProvider;
uses javax.security.auth.spi.LoginModule;
uses jdk.internal.logger.DefaultLoggerFinder;
uses sun.text.spi.JavaTimeDateTimePatternProvider;
uses sun.util.locale.provider.LocaleDataMetaInfo;
uses sun.util.resources.LocaleData.CommonResourceBundleProvider;
uses sun.util.resources.LocaleData.SupplementaryResourceBundleProvider;
uses sun.util.spi.CalendarProvider;
provides java.nio.file.spi.FileSystemProvider with jdk.internal.jrtfs.JrtFileSystemProvider;
provides java.util.random.RandomGenerator with
java.security.SecureRandom,
java.util.Random,
java.util.SplittableRandom;
}
JDK9的双亲委派机制
新版本的JDK9后的类加载器
模块化系统中的类加载器可以分为两种类型
- 平台类加载器:
用于加载JDK中的模块。
- 应用程序类加载器:
用于加载应用程序中的模块。
JDK9后的类加载流程
- 当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中
- 如果 findzoadedmodule 可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载
模块的类加载器
- 在模块化系统中,每个模块都有一个类加载器,
它根据模块的依赖关系来加载模块中的类和依赖的模块中的类
- 在模块化系统中,类加载器的原理与传统的类加载器相似,
都是采用双亲委派模型
- 当一个类被加载时,类加载器首先会检查自己是否已经加载过该类,如果没有,则会将该类的加载请求委托给其父加载器
- 直到达到顶层的 Bootstrap ClassLoader 为止,如果所有的父加载器都无法加载该类,则由当前加载器自己来加载该类
ClassLoader核心源码分析
loadClass
- 用于加载指定名称的类,双亲委派模型核心实现,一般不建议重写相关方法,直接由ClassLoader自己实现
- 遵循双亲委派模型,首先委派给父类加载器进行加载,如果父类加载器无法加载该类,则自身进行加载
findClass
- 是用于查找类的方法,它通常由子类加载器实现,用于查找自身命名空间中的类
- 由于历史JDK1.2之前版本兼容问题,自定义类加载器则推荐重写这个方法
- findClass0方法是在loadClass0方法中调用,当loadClass(0方法中加载失败后,则调用自己的findClass0)方法来完成类加载
- 但是ClassLoader的findClass没有实现,需要自己实现具体逻辑,findClass方法通常是和defineClass方法一起使用的
defindClass
- 是用于定义类的方法,它将字节数组转换为 Class 对象,并将其添加到类加载器的命名空间中
- ClassLoader方法里面已经实现,findClass方法通常是和defineClass方法一起使用的
resolveClass
- 是用于解析类的方法,它将类的引用转换为实际的类,并进行链接和初始化
findClass()方法和loadClass()方法的区别
- findClass():
用于写类加载逻辑
- loadClass():
如果父类加载器加载失败则会调用自己的findClass()方法完成加载,保证了双亲委派规则
如果不想打破双亲委派模型,那么只需要重写findClass方法即可
如果想打破双亲委派模型,多数情况下需要重写整个loadClass方法
自定义类加载器
为啥需要用到自定义类加载器
为Java应用程序提供更加灵活和可定制的类加载机制
场景案例
- 拓展加载源:
从网络,数据库等地方加载类
- 防止源码泄漏:
自定义类加载器可以加载加密的类文件,保护类的安全性
- 实现类隔离(tomcat里面大量应用):
自定义类加载器可以实现类隔离,避免类之间的冲突和干扰;
比较两个类是否相等,只有两个类是由同一个类加载器加载的前提下才有意义;否则即使两个类来自同一个class文件,但是由于加载他们的类加载器不同,那这两个类就不相等;不同类加载器加载同一个class文件得到的类型是不同的。
自定义类加载器流程
- 继承ClassLoader类
- 重写loadClass方法(会破坏双亲委派机制,不推荐)
- 重写findClass方法(推荐)
自定义类加载器
ClassLoader
package com.soulboy.jvm;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = path + name + ".class";
System.out.println(fileName);
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
int len;
byte[] data = new byte[1024];
//不等于-1就说明还有数据
while ((len = bis.read(data)) != -1) {
bos.write(data, 0, len);
}
//拿到字节数组(文件在内存中)
byte[] byteArray = bos.toByteArray();
//返回class对象
Class<?> defineClass = defineClass(null, byteArray, 0, byteArray.length);
return defineClass;
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
}
测试
ClassLoaderTest
package com.soulboy.jvm;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// 拓展加载源:从网络,数据库等地方加载类
MyClassLoader myClassLoader = new MyClassLoader("D:\\Project\\redlock\\src\\main\\java\\com\\soulboy\\jvm\\");
// 加载指定类 D:\Project\redlock\src\main\java\com\soulboy\jvm>javac HeapDemo.java
Class<?> clazz = myClassLoader.loadClass("HeapDemo");
// 拿到class类可以实例化对象
Object obj = clazz.getDeclaredConstructor().newInstance();
System.out.println(obj.getClass().getName() + "----------" + obj.getClass().getClassLoader().getClass().getName() + "类加载器加载" );
}
}
控制台输出
D:\Project\redlock\src\main\java\com\soulboy\jvm\HeapDemo.class
com.soulboy.jvm.HeapDemo----------com.soulboy.jvm.MyClassLoader类加载器加载
两个不同的类加载器加载同一个class类,JVM是否认为它们相同?
不同类加载器会加载同名的 class 类,这些类在JVM 中是不同的,即它们的 Class 对象是不同的
定义类相同的条件
- 类加载器相同
- class文件相同
编码验证
public static void test2() throws Exception{
// 自定义类加载器
// 拓展加载源:从网络,数据库等地方加载类
MyClassLoader myClassLoader = new MyClassLoader("D:\\Project\\redlock\\src\\main\\java\\com\\soulboy\\jvm\\");
// 加载指定类 D:\Project\redlock\src\main\java\com\soulboy\jvm>javac HeapDemo.java
Class<?> clazz = myClassLoader.loadClass("HeapDemo");
// 拿到class类可以实例化对象
Object obj = clazz.getDeclaredConstructor().newInstance();
// class com.soulboy.jvm.HeapDemo
System.out.println(obj.getClass());
// false obj属于自定义类加载器 HeapDemo属于JVM的类加载器
System.out.println(obj instanceof HeapDemo);
// JVM本身的类加载器 VS 自定义类加载器
// com.soulboy.jvm.HeapDemo----------com.soulboy.jvm.MyClassLoader类加载器加载
System.out.println(obj.getClass().getName() + "----------" + obj.getClass().getClassLoader().getClass().getName() + "类加载器加载" );
//com.soulboy.jvm.HeapDemo----------jdk.internal.loader.ClassLoaders$AppClassLoader类加载器加载
System.out.println(HeapDemo.class.getName() + "----------" + HeapDemo.class.getClassLoader().getClass().getName() + "类加载器加载" );
// 应用程序类加载器(Application ClassLoader)
// app
System.out.println(HeapDemo.class.getClassLoader().getName());
// 平台类加载器(Platform ClassLoader)
// platform
System.out.println(HeapDemo.class.getClassLoader().getParent().getName());
// 启动类加载器(Bootstrap ClassLoader)
// null (C++编写) .getName() 会报空指针异常
System.out.println(HeapDemo.class.getClassLoader().getParent().getParent());
}
总结
- 在JVM 中,不同类加载器加载同一个类时,可能会出现重复加载的情况
- 当不同类加载器加载同一个类时,每个类加载器都会在自己的命名空间中创建一个新的 Class 对象
- 即使这些 Class 对象的字节码是一样的,也会被认为是不同的类
- 重复加载同一个类会导致一些问题,例如类的静态变量和代码块会被多次执行,导致出现意料之外的行为
- JVM采用了类的双亲委派模型来避免重复加载同一个类
垃圾回收机制(Garbage Collection)
垃圾回收机制(Garbage Collection)
- 指自动管理动态分配的内存空间的机制,自动回收不再使用的内存,以避免内存泄漏和内存溢出的问题。
- 最早是在1960年代提出的,程序员需要手动管理内存的分配和释放
- 这往往会导致内存泄漏和内存溢出等问题,同时也增加了程序员的工作量,特别是C++/C语言开发的时候
- Java语言是最早实现垃圾回收机制的语言之一,其他编程语言,如C#、Python和Ruby等,也都提供了垃圾回收机制
- 不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。
JVM自动垃圾回收机制
优点
- 减少了程序员的工作量,不需要手动管理内存
- 动态地管理内存,根据应用程序的需要进行分配和回收,提高了内存利用率
- 避免内存泄漏和野指针等问题,增加程序的稳定性和可靠
缺点
- 垃圾回收会占用一定的系统资源,可能会影响程序的性能
- 垃圾回收过程中会停止程序的执行,可能会导致程序出现卡顿等问题
- 不一定能够完全解决内存泄漏等问题,需要在编写代码时注意内存管理和编码规范
垃圾回收机制需要解决的问题——(确定哪些对象能够被回收)
总结来说
引用计数法和可达性分析法是两种不同的内存管理和垃圾回收算法。
- 引用计数法通过维护引用计数器来跟踪对象的引用数量,具有实时性好、简单高效等优点,但存在循环引用等问题;
- 可达性分析法则通过分析对象的引用关系来判断对象是否可达,从而决定对象是否可以被回收,具有准确性高、效率好等优点,是JVM中常用的垃圾回收算法之一。
引用计数法(了解)
引用计数法(Reference Counting)是一种内存管理技术,用于跟踪对象的引用数量。每个对象都有一个引用计数器,记录着指向该对象的引用数量。
当一个对象被引用时,引用计数器加一;当一个引用被释放时,引用计数器减一。当引用计数器为零时,表示没有任何引用指向该对象,该对象可以被释放,回收其占用的内存。
优点
- 实时性好:当没有引用指向一个对象时,该对象可以立即被回收,释放内存资源。
- 简单高效:引用计数法是一种相对简单的内存管理技术,实现起来较为高效。
- 无需沿指针查找:与GC标记-清除算法不同,引用计数法无需从根节点开始沿指针查找。
缺点
- 循环引用问题:当存在循环引用的情况下,对象之间的引用计数可能永远不会为零,导致内存泄漏的发生。
- 额外开销:每个对象都需要维护一个引用计数器,这会带来一定的额外开销。
- 不支持并发:在多线程环境下,引用计数法需要进行额外的同步操作,以确保引用计数的准确性,可能导致性能损失。
- 实现复杂:虽然引用计数的算法本身很简单,但实现起来却不容易。
模拟循环引用
- 类A和类B相互引用,每个对象都持有对方的引用,形成了一个循环引用的环,当Main方法执行完毕后,a和b对象都置为null
- 由于它们相互引用,它们的引用计数器都不为0,无法被垃圾回收器回收,导致内存泄漏
- 但是上面代码却不会发生内存泄漏,因为多数jvm没有采用这个引用计数器方案,而是采用可达性分析算法
pubiic class Main{
public static void main(string[]args) {
A a = new A();
B b = new B();
a.setB(b);
b.setA(a);
a = null;
b = null;
System.gc();
}
}
class A {_
private B b;
public void setB(B b){
this.b = b;
}
}
class B {
private A a;
public void setA(A a){
this.a = a;
}
}
可达性分析法(JVM实际采用的)
可达性分析算法是JVM垃圾回收中的一种算法,它通过分析对象的引用关系,判断对象是否可达,从而决定对象是否可以被回收。基本思路是从一 些“GC Roots”对象开始,通过搜索引用链的方式,找到所有可达对象 。如果一个对象没有任何引用链与GC Roots相连,那么它就被判定为不可用的,是可以被回收的
工作原理
- GC Roots:在Java中,GC Roots通常包括虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区(静态变量)中引用的对象、本地方法栈中JNI(Native方法)引用的对象等。
- 搜索过程:可达性分析算法从GC Roots开始,递归地访问所有可达的对象,并给它们打上标记。这个过程可以使用深度优先搜索(DFS)或广度优先搜索(BFS)等图遍历算法来实现。
- 回收判定:如果一个对象到GC Roots没有任何引用链相连(即该对象从GC Roots不可达),则证明该对象是不可用的,可以判定为可回收对象。
特点
- 准确性:通过从GC Roots开始搜索引用链,可以准确地判断哪些对象是可回收的。
- 效率:结合现代JVM的优化技术,如增量标记、并发标记等,可以提高可达性分析算法的效率。
- 灵活性:可达性分析算法可以与不同的垃圾回收策略(如标记-清除、标记-整理等)结合使用,以适应不同的应用场景和硬件环境。
什么是GC Root
- 指一些被JVM认为是存活的对象,它们是垃圾回收算法的起点
- 可以理解为由堆外指向堆内的引用,本身是没有存储位置,都是字节码加载运行过程中加入JM 中的一些普通引用
- 通俗的例子可以是一个树形结构,树的根节点就是GCRoots
- 是垃圾回收器的起点,
如果一个节点没有任何子节点与根节点相连,那这个节点就被认为是不可达的,可以被回收器回收
JVM中的GC Roots对象包含
由于GC Roots采用栈方式存放变量和指针,如果一个指针它保存了堆内存里面的对象,但是自己又不能存放在堆内存里面,那么它就是一个GCRoots
GC Root对象 | 描述 |
---|---|
虚拟机栈(栈帧中的本地变量表)中引用的对象 | 栈帧中的本地变量指向堆 |
方法区中类静态属性引用的对象 | JDK1.7 开始静态变量的存储从方法区移动到堆中;比如你定义了一个static的集合对象,那里面添加的对象就是可以被GCRoot可达的 |
方法区中常量引用的对象 | 字符串常量池从JDK1.7开始由方法区移动到堆中 |
本地方法栈中JNI | (即一般说的Native方法)引用的对象 |
活动线程中的对象 | |
当前类加载器加载的类的对象 |
Gc Root 示例代码
public class GCTest {
public static void main(String[] args) throws Exception {
// product 是栈帧中的本地变量,指向了title=springboot课程 这个 Product对象
Product product = new Product("springboot课程");
// 当product=null;由于此时当product充当了 Gc Root 的作用,当product与原来指向 当product对象断开了连接
// 所以这个 new Product("springboot课程") 对象会被回收
product = null;
}
}
对象可回收,就一定会被回收吗?
不一定会回收,对象的finalize方法给了对象一次最后一次的机会。
- 当对象不可达(可回收)并发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行则会先执行 finalize 方法
- 在finalize 方法中,可以将当前对象与 GCRoots 关联,执行 finalize 方法之后,GC会再次判断对象是否可达
- 如果不可达,则会被回收,如果可达,则不回收!
- 需要注意的是 finalize 方法只会被执行一次,
如果第一次执行 finalize 方法,对象变成了可达,则不会回收
- 但如果对象再次被 GC,
则会忽略 finalize 方法,对象会被直接回收掉!
JVM采用的是可达性分析算法为什么可以解决循环引用造成的内存泄漏问题?
- 当两个或多个对象相互引用时,它们的引用链会形成一个环
- 但是由于这个环中的对象与GCRoots没有任何引用链相连,
所以IVM会将这些对象判定为不可用的,从回收它们
垃圾回收机制需要解决的问题——(如何回收这些对象)
收集死亡对象方法有三种
名称 | 说明 |
---|---|
标记-清除算法 | 基础算法 |
标记-复制算法 | 适合存活对象少,垃圾对象多的场景(新生代) |
标记-整理算法 | 适合存活对象多,垃圾对象少的场景(老年代);如果使用【标记清除】算法则会有碎片化空间、效率低下等缺点;如果使用【标记复制】算法需要复制特别多对象,效率低下等缺点 |
垃圾回收算法和垃圾收集器的关系?
- 圾回收算法是垃圾回收的方法论
- 垃圾收集器是算法的落地实现
指一些被JVM认为是存活的对象,它们是垃圾回收算法的起点
可以理解为由堆外指向堆内的引用,本身是没有存储位置,都是字节码加载运行过程中加入JM 中的一些普通引用通俗的例子可以是一个树形结构,树的根节点就是GCRoots
是垃圾回收器的起点,如果一个节点没有任何子节点与根节点相连,那这个节点就被认为是不可达的,可以被回收器回收
标记-清除算法(Mark-Sweep)
- 原理:垃圾回收器会从一些GCRoots对象开始,遍历整个对象图,标记所有可达的对象,然后清除未标记的对象。
- 优点:简单直接,不需要移动对象。
- 缺点:效率问题,
标记和清除两个步骤,都需要垃圾回收器遍历整个对象图,耗费时间较长,效率都不高
;会产生内存碎片
,当频繁进行垃圾回收时,内存碎片会越来越多导致可用内存空间不足,从而影响程序的性能和稳定性,还可能导致大对象分配失败
。 - 应用场景:在实际应用中,标记清除法一般用于不需要频繁进行垃圾回收的场景,
比如在]ava堆中大对象的分配和回收
;收集算法大多都是以标记-清除算法为基础,对其缺点进行改进。
标记-复制(Coping)算法
- 原理:将内存分为两个相等的区域(一个活动区域和一个空闲区域),每次只使用其中一个。当这个区域使用完时,将存活的对象复制到另一个区域,然后清空当前区域。
- 优点:简单高效,没有内存碎片问题。
- 缺点:
需要双倍的内存空间(将内存缩小为了原来的一半,代价有点高)
;如果出现存活对象数量比较多的时候,需要复制较多的对象,效率低;假如是在老年代区域,99%的对象都是存活的,则性能底,所以老年代不适合这个算法
- 应用场景:标记复制算法一般用于新生代的垃圾回收,因此需要对新生代的对象进行分代管理;虚拟机多数采用这个算法,对新生代进行内存管理,因为多数新生代区域的存活对象数量少;
国外有公司统计过多数业务,98%的对象撑不过一次GC;所以不用1:1比例分配新生代的空间
GC时,将Eden和Survivor中存活对象一次性复制到另外一块Survivor空间上,然后清理掉Eden和已用过的那块Survivor空间
每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%+Survivor的10%),
只有一个Survivor空间,即10%的新生代是会被浪费而已
标记复制算法的详细实现步骤
- 将lava堆分为两个区域:一个活动区域和一个空闲区域,初始时,所有对象都分配在活动区域中
- 从GC Roots对象开始,遍历整个对象图,标记所有被引用的对象
- 对所有被标记存活的对象进行遍历,将它们复制到空闲区域中,并更新所有指向它们的引用,使它们指向新的地址
- 对所有未被标记的对象进行回收,将它们所占用的内存空间释放
- 交换活动区域和空闲区域的角色,
空闲区域变为新的活动区域,原来的活动区域变为空闲区域
- 当空闲区域的内存空间不足时,进行一次垃圾回收,重复以上步骤。
标记-整理(Mark-Compact)算法
- 原理:在标记阶段标记所有可达的对象后,压缩阶段将存活的对象移动到内存的一端,整理出连续的可用内存空间。
- 优点:消除了内存碎片问题,不用浪费额外的空间。
- 缺点:对象移动需要额外的时间和资源。(效率相比于标记复制算法低一些,在整理存活对象时,因对象位置点变动,需要该调整虚拟机栈中的引用地址)
- 应用场景:标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。
分代收集算法
- 原理:根据对象的生命周期将堆内存划分为几代(通常是新生代和老年代),新生代使用复制算法,老年代使用标记-压缩或标记-清除算法。
- 优点:优化了垃圾收集性能,因为大部分对象在新生代被收集,减少了老年代的垃圾收集频率。
- 缺点:需要额外的内存管理和调优。
- 缺点:需要额外的内存管理和调优。
分区算法
- 原理:将堆内存划分为多个小的独立区域(Region),每个区域可以独立进行垃圾收集。
- 优点*:提高了内存管理的灵活性和效率,适用于大堆内存的应用。
- 缺点*:实现较复杂,需要精细的内存管理。
并行和并发收集算法
- 并行收集:垃圾收集过程由多个线程并行执行,提高了垃圾收集的效率。
- 并发收集:垃圾收集过程与应用线程并发执行,减少了应用停顿时间。
JVM垃圾回收——分代收集算法思想
什么是分代收集算法
针对不同生命周期的对象采用不同的垃圾回收策略,以达到更好的垃圾回收效果,年轻代多数对象存活时间短,高频进行回收;老年代多数对象存活时间久,进行低频回收
;分代算法是根据回收对象的特点进行选择,年轻代适合标记-复制算法,老年代适合标记清除或标记压缩算法
;通过将内存划分为不同的代,可以使得Minor Gc的频率更高,更早地回收垃圾对象,减少FulGC的发生频率,提高整体性能
- 原理:根据对象的生命周期将堆内存划分为几代(通常是新生代和老年代),新生代使用复制算法,老年代使用标记-压缩或标记-清除算法。
- 优点:优化了垃圾收集性能,因为大部分对象在新生代被收集,减少了老年代的垃圾收集频率。
- 缺点:需要额外的内存管理和调优。
GC的分类和专业术语
- 不用去关心是叫 Major GC还是 Fu GC,应该关注当前的 GC是否停止了所有应用程序的线程
- 许多Major GC 是由 Minor GC触发的,出现 Major GC通常出现至少一次的 Minor GC
- MajorGC的速度一般比 Minor GC 慢 10倍以上
FullGC触发场景
- 手工调用System.gc(),建议执行Full GC,不一定会执行,
可通过-XX:+ DisableExplicitGC参数来禁止调用System.gc()
- 老年代空间不足,
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
STW(Stop The World)
- 垃圾回收发生过程中,用户线程在运行至安全点(safe point)后,就自行挂起进入暂停状态,对外的表现就是卡顿
- 所以应尽量减少Full GC的次数,
不管是Minor GC还是Major GC都会STW,区别只在于STW的时间长短
垃圾收集器组合和常见性能指标
什么是垃圾收集器
- 垃圾回收算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现
- 目前Java规范中并没有对垃圾收集器的实现有任何规范。
- 不同的厂商、不同的版本的虚拟机提供的垃圾收集器是不同的,主要讨论的是HotSpot虚拟机。
是不是有最厉害的收集器?
不存在最厉害的垃圾收集器,只有在对应场景中最合适的垃圾收集器
为什么要有很多收集器?
- 因为Java的使用场景很多,移动端,服务器等,然后内存里面对象存活时间不一样
- 需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能
垃圾收集器分类
区域类型 | 描述 |
---|---|
新生代收集器 | Serial(串行垃圾收集器)、ParNew(年轻代的并行垃圾回收器)、Parallel(并行垃圾收集器) |
老年代收集器 | Serial Old(串行老年代垃圾器)、Parallel Old(老年代的并行垃圾回收器)、CMS(ConcMarkSweep 并发标记清除) |
整堆收集器 | G1、ZGC |
具体垃圾收集器使用的算法
垃圾收集器 | 算法 |
---|---|
Serial GC | 使用标记-压缩算法。 |
Parallel GC | 新生代使用复制算法,老年代使用标记-压缩算法。 |
CMS GC | 新生代使用复制算法,老年代使用标记-清除算法,并发标记和清除。 |
G1 GC | 分区算法,结合标记-压缩和复制算法。 |
ZGC | 分区算法,使用染色指针和读屏障技术,实现并发标记和压缩。 |
Shenandoah GC | 分区算法,使用并发标记和并发压缩技术。 |
图解分配垃圾收集器的组合
- JDK8中默认使用: ParallelScavenge GC+ ParallelOld GC
- JDK9默认是用G1为垃圾收集器
- JDK14 弃用了: Parallel Scavenge GC+ Parallel OldGC
- JDK14 移除了 CMS GC
两个垃圾收集器之间如果存在连线,则说明它们可以搭配使用
图中:Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案
垃圾收集器关注的核心指标
吞吐量、暂停时间、收集频率
吞吐量(重点)
- 运行用户代码的时间占总运行时间的比例(总运行时间=程序的运行时间+内存回收的时间)
例子:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
暂停时间(重点)
- 执行垃圾收集时,程序的工作线程被暂停的时间
- 一个时间段内应用程序线程暂停,让GC线程执行的状态
GC期间100毫秒的暂停时间,说明在这100毫秒期间内没有应用程序线程是活动的
收集频率
- 指垃圾回收器多长时间会运行一次。
一般来说,垃圾回收器的频率应该是越低越好。
查看默认垃圾收集器
- JVM参数:
-XX:+PrintCommandLineFlags
查看命令行相关参数(包含使用的垃圾收集器)
JDK8
-XX:+UseParallelGC
JDK11
-XX:+UseG1Gc
JDK17
-XX:ConcGCThreads=3 -XX:G1ConcRefinementThreads=10 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=532517632 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=8520282112 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
Serial收集器
最简单的垃圾收集器,使用单线程进行垃圾收集,暂停所有应用程序线程,在单核CPU环境来说,Serial收集器更高效
- Serial Old是Serial收集器的老年代版本,在Jdk1.5之前的版本与Parallel收集器搭配使用,或者作为CMS的备选方案
适用于小型应用程序和客户端应用程序
,一般javaweb、springboot项目不会采用这类收集器
区域 | 收集算法 |
---|---|
新生代 | 复制算法 |
老年代 | 标记整理 |
相关命令参数使用
- 同时指定年轻代和老年代都使用串行垃圾收集器
-XX:+UseSerialGC
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
### 添加虚拟机参数
-XX:+UseSerialGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
### 控制台打印输出
-XX:InitialHeapSize=33554432 -XX:MaxHeapSize=33554432 -XX:MinHeapSize=33554432 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
ParNew收集器
工作在年轻代上的,只是将串行的垃圾收集器改为了并行,其他基本和Serial一样,使用多个线程进行垃圾回收的
- 适用于大型应用程序和多核处理器,以及在服务端应用程序中使用,在单核上效率比Serial低,在多核上效率比Serial高
- 只有ParNew和Serial收集器可以兼容CMS。
ParNew和Parallel收集器类似,但Parallel收集器不兼容CMS。
区域 | 收集算法 |
---|---|
新生代 | 复制算法 |
老年代 | 标记整理 |
相关命令参数使用
- 同时指定年轻代和老年代都使用串行垃圾收集器
-XX:+UseParNewGC
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
验证参数(JDK8环境,如果用JDK11会报错,JDK8开始已经不再被推荐使用)
### 添加虚拟机参数
-XX:+UseParNewGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
### 控制台打印输出
-XX:InitialHeapsize=33554432 -XX:MaxHeapSize-33554432 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParNewGC
Java HotSpot(TM)64-Bit Server vM warning: Using the ParNew young collector with the Serial oldcollector is deprecated and will likely be removed in a future release
Parallel收集器
全称 Parallel Scavenge 是一种多线程垃圾收集器,和ParNew收集器类似,是一个新生代收集器,默认线程数和cpu核数一样,用于大型应用程序和服务器应用程序,比如大批量数据处理,后台计算任务等
Parallel Old是Parallel Scavenge收集器的老年代版本,JDK8默认使用Parallel Scavenge收集器
区域 | 收集算法 |
---|---|
新生代(Parallel Scavenge) | 复制算法 |
老年代(Parallel Old) | 标记整理 |
Parallel对比ParNew
-XX:+UseParallelGC
:仅对年轻代有效,不可以和CMS收集同时使用-XX:+UseParNewGC
:设置年轻代为多线程收集,可以和CMS收集同时使用
相关命令参数使用
- 年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器
-XX:+UseParallelGC
- 年轻代使用ParallelGC垃圾回收器,老年代使用Parallel0ldGC垃圾回收器
-XX:+UseParallelOldGC
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
验证参数(JDK11环境)
### 添加虚拟机参数
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
### 控制台打印输出
-XX:InitialHeapsize=33554432 -XX:MaxHeapsize=33554432 -XX:+PrintCommandLineFlags -XX:ReservedcodeCachesize=251658240 -XX:+Seqmentedcodecache -XX:+UseCompressedclassPointers -XX:+UseCompressedoops -XX:+UseParallelGC -XX:+UseParallel0ldGc
CMS收集器
全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器;老年代中的对象生命周期较长,垃圾回收频率较低,目标是获取最短垃圾收集停顿时间
,针对老年代垃圾的收集器。
停顿时间较短,适合对响应时间要求较高的应用程序,如Web应用程序、电子商务等高并发场景
整个过程分4步(初始标记 和 重新标记 需要stopTheWorld,并发标记与并发清除阶段不需要暂停用户线程)
原理
整个过程分4步(初始标记和重新标记需要stopTheWorld,并发标记与并发清除阶段不需要暂停用户线程)
- 初始标记:
标记GC Root直接关联对象
,会导致stopTheWorld,时间很短暂,对应用程序影响不大
。 - 并发标记:与用户线程同时运行,
从GC Root直接关联的对象开始遍历所有对象,
过程比较耗时,但是不需要暂停应用线程 - 重新标记:修正并发标记的错标或漏标,会导致stopTheWorld,
重新标记会比初始标记耗时长一些,但会比并发标记耗时短很多
- 并发清除:与用户线程同时运行,
删除已经标记死亡对象,不需要移动对象,所以可以和用户进程同时运行
相关命令参数使用
- 年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器
-XX:+UseParallelGC
- 年轻代使用ParallelGC垃圾回收器,老年代使用Parallel0ldGC垃圾回收器
-XX:+UseParallelOldGC
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
验证参数(JDK11环境测试,发现然后被弃用了,切换JDK8验证成功)
### 添加虚拟机参数
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
### 控制台打印输出
-XX:InitialHeapsize=33554432 -XX:MaxHeapsize=33554432 -XX:MaxNewSize=11190272 -XX:MaxTenuringThreshold=6 -XX:Newsize-11190272 -XX:01dsi2e-22364160 -XX:+PrintCommandLineFlags -XX:Reservedcodecachesi2e-251658240 -XX:+SeqmentedcodeCache -XX:+UseCompressedclassPointers -XX:+UseCompressedoops -XX:+UseConcMarksweepcc
### 警告信息
Option UseConcMarkSweepGc was deprecated in version 9,0 and will likely be removed in a future release.
上述垃圾收集器都是接近被弃用了,大体了解即可
,重点掌握G1和ZGC
G1垃圾收集器
Garbage First
垃圾收集器是JDK7版本之后引入的一种垃圾回收器,JDK9中将G1变成默认的垃圾收集器。- G1 GC 横跨新生代和老年代,
可以在不同的内存区域中分配垃圾回收的工作,提高了垃圾回收效率。
- 分区算法,结合标记-压缩和复制算法,
整体采用标记整理算法,局部是采用复制算法,不会产生内存碎片
。
查看默认垃圾收集器
- 查看命令行相关参数
-XX:+PrintCommandLineFlags
-XX:ConcGCThreads=3 -XX:G1ConcRefinementThreads=10 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=532517632 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=8520282112 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
原理
- 保留了分代思想,把内存划分为多个独立的区域Region,区域中包含逻辑上的年轻代、老年代区域、幸存区、巨星区。
- 取消了年轻代、老年代的物理划分,简化了JVM调优的配置,
不用单独对每个年代空间进行设置
- Region的区域类型是动态变化的,
可能之前是年轻代,经过了垃圾回收之后就变成了老年代,实现更加精细化的垃圾回收
- 整体采用标记整理算法,局部是采用复制算法,
不会产生内存碎片
- 把整个ava堆划分成约2048个独立Region块,每个Region块大小根据堆空间的大小而定,为2的N次幂,1MB~32MB 每个Region的大小可通过参数
-xx:G1HeapRegionsize
配置 - 新增加一种叫Humongous内存区域,
用于存储大对象
;如果超过1.5个region,就是巨型对象,就放到H区,默认直接会被分配在老年代,一般被视为老年代
;如果一个H区装不下一个巨型对象,G1会寻找连续的H区来存储,为了能找到连续的H区,有时需要启动Full GC
G1提供三种模式垃圾回收模式
在YoungGC和Mixed GC中,G1垃圾收集器都会对每个Region的存活对象数量进行统计;根据存活对象数量和空闲Region的数量,动态地决定垃圾收集的区域和顺序;
这种动态的垃圾收集策略,可以避免Full GC的发生,提高了应用程序的响应速度
;
Young GC
- G1与之前垃圾收集器的young GC不同,并不是当新生代的Eden区放满了就进行垃圾回收
- G1会计算当前Eden区回收大概需要多久时间,如果接近参数
-xx:MaxGcPauseMills
设定的值,会触发Young GC 回收过程也是将Eden区和Survivor区中的存活对象复制到空闲的Sunvivor区,并清空Eden区和原来的Sunvivor区。
如果Survivor区也满了,那么会将存活对象复制到Old区。在YoungGC期间,应用程序会被暂停
Mixed GC
- 多数对象晋升到老年代oId region时,为了避免堆内存被耗尽问题,会触发混合的GC
混合的GC会回收整个Young Region,还会回收一部分的Old Region区域,注意不是全部Old Region区域
- 触发条件,参数
-XX:InitiatingHeapOccupancyPercent=n
决定默认:45%,即 当老年代大小占整个堆大小百分比达到该阀值时触
Full GC
- 单个线程会对整个堆的所有代中所有分区做标记、清除以及压缩动作,非常耗时
G1的MixGC垃圾收集器执行步骤
- 初始标记(STW):记录下GC Roots能直接引用的对象,并标记所有存活的对象,会执行一次年轻代GC,
需要暂停所有线程,速度很快
- 并发标记:与应用线程一起工作,进行可达性分析,
g1收集器会对堆内存进行并发标记,找出所有存活的对象,并记录它们所在的Region
- 最终标记(STW):修正并发标记期间,部分因程序运行导致发生变化的那一部分对象,根据算法修复一些引用的状态
- 筛选回收(STW):对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间,即
-XX:MaxGCPauseMillis
制定计划### 成本排序案例 现在有Region1、Region2和Region3三个区域 1、Region1预计可以回收1.5MB内存,预计耗时2MS,投产比 RO1=1.5/2=0.75 2、Region2预计可以回收1MB内存,预计耗时1MS,投产比 ROI=1/1=1 3、Region3预计可以回收0.5MB内存,预计耗时1MS,投产比 ROI=0.5/1=0.5 那Region1、Region2和Region3各自的回收价值与成本比值分别是:0.75、1、0.5 比值越高说明同样的付出收益越高,如果此时只能回收一个Region的内存空间,G1就会选择Region2进行回收
G1垃圾收集器参数使用和调优
G1使用方式简单(JDK9中将G1变成默认的垃圾收集器)
JDK9之前的垃圾回收器和测试各个JVM相关参数调整,花了大量时间调优JVM参数,最终得到了不错的吞吐量。只是换了G1垃圾回收器,然后性能指标就更好了,而且性能要好很多。
- 开启G1垃圾收集器
- 设置堆的最大内存
- 设置最大的停顿时间
相关参数
-XX:+UseG1GC
:启用G1垃圾收集器。-XX:G1HeapRegionSize
:Java 堆大小划 分出约 2048 个区域,默认是堆内存的1/2000;配置需要为2的N次幂,1MB~32MB。使用G1垃圾回收器最小堆内存应为 1MB*2048=2GB,低于这个的建议使用其它垃圾回收器。
-XX:MaxGCPauseMillis=n
:设置最大停顿时间n,单位为毫秒,默认为200毫秒(JVM会尽力实现,但不能保证达到)-XX:ParallelGCThreads=n
:设置 STW 工作线程数的值。一般设置为逻辑处理器的数量,最多为8
-XX:ConcGCThreads=n
:并行标记的线程数,一般将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4-XX:InitiatingHeapOccupancyPercent=n
:设置G1 Mix垃圾回收的触发阈值,默认为45%-XX:+PrintCommandLineFlags
:查看命令行相关参数
### 参数使用
# JVM参数
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags
# 控制台输出
-XX:ConcGCThreads=3 -XX:G1ConcRefinementThreads=10 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=549453824 -XX:MarkStackSize=4194304 -XX:MaxGCPauseMillis=100 -XX:MaxHeapSize=549453824 -XX:MinHeapSize=549453824 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
G1应用场景
- 大型应用程序:可以将堆内存划分为多个区域,以实现更加精细化的垃圾回收。
- 高并发、低延迟:对响应时间要求较高的应用程序,如Web应用程序、电子商务等高并
- 大内存应用:可以在垃圾回收过程中释放大量的空间,提高了内存的利用率。
使用G1垃圾收集器注意事项
- 不手工设置年轻代大小。
比如使用 -Xmn 选项或 -XX:NewRatio 等设置年轻代大小
暂停时间的目标不要太小
- G1 的吞吐量目标是 90%的应用程序时间和10%的垃圾回收时间
如果把停顿时间调得非常低,如设置为10毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次回收内存只占堆内存很小的一部分,收集器收集的速度跟不上分配器分配的速度, 导致垃圾慢慢堆积,应用运行时间一长就占满堆引发Full GC反而降低性能
,通常把期望停顿时间设置为一两百毫秒是比较合理的。
避免存活时间短的大对象(Humongous)
- G1垃圾收集器对程序的代码质量要求较高,需要对程序的内存使用情况进行精细化管理,对应用程序的代码进行优化和调整
Humongous大对象属于老年代,老年代的回收速度会慢一些
ZGC垃圾收集器
什么是ZGC
- z Garbage Collector 是Oracle公司开发一种可伸缩、低停顿时间的垃圾收集器,标记-复制算法(进行了改进)
垃圾回收过程几乎全部是并发,实际STW停顿时间极短,停顿时间控制10ms内
,主要采用的染色指针和读屏障技术- 在 JDK11中是实验性的特性引入,在JDK15中ZGC可以正式投入生产使用,
使用-XX:+UseZGC
启用 ZGC的堆内存也是基于 Region 来分布,和G1类似,不区分新生代老年代的,Region 支持动态地创建和销毁,大小不是固定
三种类型的 Region
- 小型页面 Small Region:容量固定2MB,主要用于放置小于 256 KB 的小对象。
- 中型页面Medium Region:容量固定32MB,主要用于放置大于等于256KB小于4MB的对象。
- 大型页面Large Region:容量不固定 为N*2MB, Region 是可以动态变化的,但必须是 2MB 的整数倍,最小支持4 MB。
ZGC特点
- 低停顿时间:
ZGC最大的特点是在不增加延迟的情况下,能够处理非常大的内存数据
- 可伸缩性:
可以处理非常大的内存数据,适应不同规模的应用程序,从小型应用程序到大型企业级应用程序
- 不需要分代:
不需要将内存分为新生代和老年代,不需要复杂的内存回收算法
- 并发处理:
采用了并发处理的方式来进行垃圾回收可以在应用程序运行的同时进行垃圾回收
ZGC工作原理
- 初始标记(STW):
找 GC Roots 直接引用的对象,处理时间和GC Roots的数量成正比,停顿时间不随着堆的大小而增加。
- 并发标记:
扫描剩余的所有对象,处理时间比较长,业务线程与GC线程同时运行,但这个阶段会有漏标问题
- 再标记(STW):
通过算法解决漏标对象,和G1中的解决漏标的算法类似
- 并发转移准备:
分析最有回收价值GC分页,即ROI计算
- 初始转移(STW):
转移初始标记的存活对象和做对象重定位,时间和GC Roots的数量成正比,时间不随堆大小而增加。
- 并发转移:
对“并发标记”阶段存活的对象进行转移
平台支持说明
- 部分版本里面是实验性参数,需要加
-XX:+UnlockExperimentalVMOptions
才可以使用
平台 | 是否支持 | 支持版本 |
---|---|---|
Linux/x64 | √ | JDK 15(Experimental since JDK 11) |
Linux/AArch64 | √ | JDK 15(Experimental since JDK 13) |
macOS/x64 | √ | JDK 15(Experimental since JDK 14) |
Windows/x64 | √ | JDK 15(Experimental since JDK 14) |
Windows/AArch64 | √ | JDK 16 |
macOS/AArch64 | √ | JDK 17 |
Linux/PowerPC | √ | JDK 18 |
验证参数(JDK17环境)
### 虚拟机输入参数
-XX:+UseZGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
### 控制台输出
-XX:InitialHeapSize=33554432 -XX:MaxHeapSize=33554432 -XX:MinHeapSize=33554432 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:-UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseZGC
升级建议
- ZGC业界还没大规模使用,更多再实验性观望阶段,还存在变动和争议阶段,如果可能则预计26年~28年成为主流
- 当下采用的垃圾收集器是G1收集器,23~25年会是主流
- 使用的话可以升级JDK,在JDK11中首次支持ZGC,且IDK11属于长期支持(LongTermSupport,LTS)版本
Linux平台下IDK11开始支持ZGC
JDK14开始支持Mac和Windows
JVM调优中,升级JDK版本有较大收益,但是风险需要评估好
- 兼容性和功能模块:
程序依赖很多三方jar包,有些jdk升级会把旧的API直接移除,导致项目直接编译不通过;版本升级,有些潜在的功能逻辑可能会被调整,导致些不可预测的故障产生,尤其是接锅侠身上
- 性能风险:
短期测试相关数据是比较优,但是也需要能保持性能稳定;常规建议技术团队可以预发布环境进行稳定性测试,基于生产环境流量拷贝
JVM诊断工具
JVM自身带了很多命令,可以方便我们做性能分析,GC参数调整、死锁检查、生产环境问题诊断等
jps(Java Process Status Tool)
格式: jps[options][hostid]
参数解释
options | 功能描述 |
---|---|
-l | 显示进程id,显示主类全名或jar路径 |
-q | 显示进程id |
-m | 显示进程id,显示JVM启动时传递给main()的参数 |
-v | 显示进程id,显示JVM启动时显示指定的JVM参数 |
全称 java process status tool,java版的ps命令,查看java进程及其相关的信息的pid则可以用这个命令,和linux的ps类似
格式 ips[-options][hostid]
睡眠(一直运行)
package com.soulboy.jvm;
public class JvmTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("程序运行!");
Thread.sleep(100000);
}
}
# JVM参数设置
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintCommandLineFlags -Xms524m -Xmx524m
# 控制台输出
-XX:ConcGCThreads=3 -XX:G1ConcRefinementThreads=10 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=549453824 -XX:MarkStackSize=4194304 -XX:MaxGCPauseMillis=100 -XX:MaxHeapSize=549453824 -XX:MinHeapSize=549453824 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
程序运行!
jinfo(Configuration Info for Java)
全称 Configuration Info for Java,可以用来查看jvm参数和动态修改部分jvm参数的命令
启动java程序,不会指定所有的Java虚拟机参数,如果开发人员想知道某一个具体的java虚拟机参数的默认值则可以使用jinfo
。
还可以在运行时修改部分参数,且立即生效,但注意并非所有参数都支持动态修改,被标记manageable的才可以动态修改
格式:jinfo [-options] <pid>
参数解释
jinfo 18340
options | 功能描述 |
---|---|
[no options] pid | 输出所有的系统属性和参数 |
-flag<具体参数> pid | 查看具体参数的值 |
-flag[+|-] | 打开或关闭参数 |
-flag | 设置参数值 |
-flags | 打印所有参数 |
-sysprops | 打印系统配置 |
最常用示例
- 查看曾经赋值过值的参数值:
jinfo -flags 18340
- 查看具体参数的值:
jinfo -flags 18340
- jinfo 动态进行参数修改:查看哪些可以动态修改参数
java -XX:+PrintFlagsFinal -version | [grep|findstr] manageable
- 修改方式:
jinfo -flag +HeapDumpAfterFullGC 18340
jstat(Java Virtual Machine statistics monitoring tool)
Java Virtual Machine statistics monitoring tool,对java应用程序的资源进行实时监控,包括堆和垃圾回收状况的监控
格式:jstat [-options] <vmid> [间隔时间(毫秒)] [查询次数]
options | 功能描述 |
---|---|
-class | 查看类加载情况的统计 |
-compiler | 查看HotSpot中即时编译器编译情况的统计 |
-gc | 查看JVM中堆的垃圾收集情况的统计 |
-gccapacity | 查看新生代、老生代及持久代的存储容量情况 |
-gcmetacapacity | 显示metaspace的大小 |
-gcnew | 查看新生代垃圾收集的情况 |
-gcnewcapacity | 用于查看新生代存储容量的情况 |
-gcold | 查看老生代及持久代垃圾收集的情况 |
-gcoldcapacity | 用于查看老生代的容量 |
-gcutil | 显示垃圾收集信息 |
-gccause | 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次仅当前正在发生的垃圾收集的原因 |
-printcompilation | 输出JIT编译的方法信息 |
常用命令
- 查看类加载情况的统计:
jstat -class 16620
输出结果 | 功能描述 |
---|---|
Loaded | 加载类的数量 |
Bytes | 加载类的size,单位为Bvte |
Unloaded | 卸载类的数目 |
Bytes | 卸载类的size,单位为Byte |
Time | 加载与卸载类花费的时间 |
- 查看JVM中堆的垃圾收集情况的统计,输出实际值:
jstat -gc 16620
输出结果 | 功能描述 |
---|---|
S0C | 年轻代中第一个survivor(幸存区)的容量(字节) |
S1C | 年轻代中第二个survivor(幸存区)的容量(字节) |
S0U | 年轻代中第一个survivor(幸存区)目前已使用空间(字节) |
S1U | 年轻代中第二个survivor(幸存区)目前已使用空间(字节) |
EC | 年轻代中Eden(伊甸园)的容量(字节) |
EU | 年轻代中Eden(伊甸园)目前已使用空间(字节) |
OC | Old代的容量(字节) |
OU | Old代目前已使用空间(字节) |
MC | metaspace(元空间)的容量(字节) |
MU | metaspace(元空间)目前已使用空间(字节) |
CCSC | 当前压缩类空间的容量(字节) |
CCSU | 当前压缩类空间目前已使用空间(字节) |
YGC | 从应用程序启动到采样时年轻代中gc次数 |
YGCT | 从应用程序启动到采样时年轻代中gc所用时间(s) |
FGC | 从应用程序启动到采样时Ful GC的次数 |
FGCT | 从应用程序启动到采样时FuGC所用时间(s) |
GCT | 从应用程序启动到采样时垃圾回收消耗总时(S) |
- 显示垃圾收集信息,和-gc类似,不过是百分比展示,每隔2000毫秒打印一次,打印3次:
jstat -gcutil 16620 2000 3
输出结果 | 功能描述 |
---|---|
S0 | 年轻代中第一个survivor(幸存区)已使用的占当前容量百分比 |
S1 | 年轻代中第二个survivor(幸存区)已使用的占当前容量百分比 |
E | 年轻代中Eden(伊甸园)已使用的占当前容量百分比 |
O | 老年代已使用的占当前容量百分比 |
M | 元数据区已使用的占当前容量百分比 |
CCS | 压缩使用百分比 |
YGC | 年轻代垃圾回收次数 |
YGCT | 年轻代垃圾回收消耗时间 |
FGCFuI GC | 垃圾回收次数 |
CGC | 并发GC次数 |
CGCT | 并发GC总耗时 |
GCT | 垃圾回收消耗总时间 |
* 显示垃圾回收的相关信息,最后一次或当前正在发生的垃圾回收的诱因:jstat -gccause 16620
输出结果 | 功能描述 |
---|---|
S0 | 年轻代中第一个survivor(幸存区)已使用的占当前容量百分比 |
S1 | 年轻代中第二个survivor(幸存区)已使用的占当前容量百分比 |
E | 年轻代中Eden(伊甸园)已使用的占当前容量百分比 |
O | 老年代已使用的占当前容量百分比 |
M | 元数据区已使用的占当前容量百分比 |
CCS | 压缩使用百分比 |
YGC | 年轻代垃圾回收次数 |
YGCT | 年轻代垃圾回收消耗时间 |
FGCFuI GC | 垃圾回收次数 |
CGC | 并发GC次数 |
CGCT | 并发GC总耗时 |
GCT | 垃圾回收消耗总时间 |
LGCC | 最后一次GC原因,常见是 A11ocation Failure 申请内存失败 |
GCC | 当前GC原因(NO GC为当前没有执行GC) |
jstack
Java堆栈跟踪工具,可以打印出|ava应用程序中所有线程的堆栈信息,包括线程状态、调用栈信息、锁信息等……
,用于诊断线程死锁、死循环、内存泄漏等问题
格式:jstack [-options] pid
打印关于锁的附加信息,如持有锁的线程、等待锁的线程等:jstack -l 15072
信息解读 | 描述 |
---|---|
main | 开头是线程名称,后面的为线程信息 |
#1 | 表示当前线程ID,从 main线程开始,JVM根据线程创建的顺序为线程编号 |
prio=5 | prio 是 priority优先级的缩写,代表当前线程的优先级,范围为[1-10]默认为 5,数值越低越优先获取到计算资源 |
cpu=62.50ms | cpu=60.91ms 表示进程在CPU上的运行时间为62.50毫秒,指的是进程实际占用CPU的时间 |
elapsed=24.68s | 进程从开始运行到当前时刻已经运行24.68s秒,包括进程等待时间和实际运行时间 |
os_prio=0 | 为线程对应系统的优先级 |
tid=0x0000027b54a2d6d0 | 表示java内的线程ID,同样在Thread类中(可以不管) |
nid=0x4334 | 本地线程编号 NativeIp的缩写,表示操作系统级别的线程ID,对应JVM 虚拟机中线程映射在操作系统中的线程编号,是十六进制 |
java.lang.Thread.State: TIMED_WAITING (sleeping) | NEW、RUNNABLE、BLOCKED(进入synchronized之前)、WAITING(已经进入synchronized,调用了wait())、TIMED_WAITING(已经进去synchronized,调用了sleep())、TERMINATED(线程结束) |
使用jstack诊断线程死锁
D:\Project\redlock>jstack -l 15072
2024-08-13 09:49:38
Full thread dump Java HotSpot(TM) 64-Bit Server VM (17.0.8+9-LTS-211 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x0000027b74547a30, length=13, elements={
0x0000027b54a2d6d0, 0x0000027b6f838b80, 0x0000027b6f83a150, 0x0000027b6f8526b0,
0x0000027b6f855070, 0x0000027b6f857a30, 0x0000027b6f8e9f50, 0x0000027b6f8583e0,
0x0000027b6f85c1e0, 0x0000027b6f8eb370, 0x0000027b6fb1d3d0, 0x0000027b6fb1cf00,
0x0000027b6fb1d8a0
}
"main" #1 prio=5 os_prio=0 cpu=62.50ms elapsed=24.68s tid=0x0000027b54a2d6d0 nid=0x4334 waiting on condition [0x0000007e278ff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(java.base@17.0.8/Native Method)
at com.soulboy.jvm.JvmTest.main(JvmTest.java:6)
Locked ownable synchronizers:
- None
"Reference Handler" #2 daemon prio=10 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f838b80 nid=0x2b1c waiting on condition [0x0000007e27fff000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@17.0.8/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@17.0.8/Reference.java:253)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@17.0.8/Reference.java:215)
Locked ownable synchronizers:
- None
"Finalizer" #3 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f83a150 nid=0x948 in Object.wait() [0x0000007e280ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@17.0.8/Native Method)
- waiting on <0x00000000fff0d5d0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@17.0.8/ReferenceQueue.java:155)
- locked <0x00000000fff0d5d0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@17.0.8/ReferenceQueue.java:176)
at java.lang.ref.Finalizer$FinalizerThread.run(java.base@17.0.8/Finalizer.java:172)
Locked ownable synchronizers:
- None
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8526b0 nid=0x15e4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Attach Listener" #5 daemon prio=5 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f855070 nid=0x3048 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Service Thread" #6 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f857a30 nid=0x38f8 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Monitor Deflation Thread" #7 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8e9f50 nid=0x3654 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread0" #8 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8583e0 nid=0x2a0c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task
Locked ownable synchronizers:
- None
"C1 CompilerThread0" #11 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f85c1e0 nid=0x29b4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task
Locked ownable synchronizers:
- None
"Sweeper thread" #12 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8eb370 nid=0x2c64 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Common-Cleaner" #13 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=24.61s tid=0x0000027b6fb1d3d0 nid=0x1930 in Object.wait() [0x0000007e288ff000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(java.base@17.0.8/Native Method)
- waiting on <0x00000000ffe59058> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@17.0.8/ReferenceQueue.java:155)
- locked <0x00000000ffe59058> (a java.lang.ref.ReferenceQueue$Lock)
at jdk.internal.ref.CleanerImpl.run(java.base@17.0.8/CleanerImpl.java:140)
at java.lang.Thread.run(java.base@17.0.8/Thread.java:833)
at jdk.internal.misc.InnocuousThread.run(java.base@17.0.8/InnocuousThread.java:162)
Locked ownable synchronizers:
- None
"Monitor Ctrl-Break" #14 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=24.50s tid=0x0000027b6fb1cf00 nid=0x1acc runnable [0x0000007e28afe000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.SocketDispatcher.read0(java.base@17.0.8/Native Method)
at sun.nio.ch.SocketDispatcher.read(java.base@17.0.8/SocketDispatcher.java:46)
at sun.nio.ch.NioSocketImpl.tryRead(java.base@17.0.8/NioSocketImpl.java:261)
at sun.nio.ch.NioSocketImpl.implRead(java.base@17.0.8/NioSocketImpl.java:312)
at sun.nio.ch.NioSocketImpl.read(java.base@17.0.8/NioSocketImpl.java:350)
at sun.nio.ch.NioSocketImpl$1.read(java.base@17.0.8/NioSocketImpl.java:803)
at java.net.Socket$SocketInputStream.read(java.base@17.0.8/Socket.java:966)
at sun.nio.cs.StreamDecoder.readBytes(java.base@17.0.8/StreamDecoder.java:270)
at sun.nio.cs.StreamDecoder.implRead(java.base@17.0.8/StreamDecoder.java:313)
at sun.nio.cs.StreamDecoder.read(java.base@17.0.8/StreamDecoder.java:188)
- locked <0x00000000ff599a60> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(java.base@17.0.8/InputStreamReader.java:177)
at java.io.BufferedReader.fill(java.base@17.0.8/BufferedReader.java:162)
at java.io.BufferedReader.readLine(java.base@17.0.8/BufferedReader.java:329)
- locked <0x00000000ff599a60> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(java.base@17.0.8/BufferedReader.java:396)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:53)
Locked ownable synchronizers:
- <0x00000000ff590360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"Notification Thread" #15 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=24.50s tid=0x0000027b6fb1d8a0 nid=0x1144 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"VM Thread" os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8334d0 nid=0x2edc runnable
"GC Thread#0" os_prio=2 cpu=0.00ms elapsed=24.68s tid=0x0000027b54a5cc40 nid=0x37e8 runnable
"G1 Main Marker" os_prio=2 cpu=0.00ms elapsed=24.68s tid=0x0000027b54a63520 nid=0x38e4 runnable
"G1 Conc#0" os_prio=2 cpu=0.00ms elapsed=24.68s tid=0x0000027b54a63f30 nid=0x2c74 runnable
"G1 Refine#0" os_prio=2 cpu=0.00ms elapsed=24.67s tid=0x0000027b6f778a30 nid=0x42e0 runnable
"G1 Service" os_prio=2 cpu=0.00ms elapsed=24.67s tid=0x0000027b6f779450 nid=0x21d8 runnable
"VM Periodic Task Thread" os_prio=2 cpu=0.00ms elapsed=24.50s tid=0x0000027b744ddb90 nid=0x31b8 waiting on condition
JNI global refs: 15, weak refs: 0
jstack生产案例:分析CPU占用过高的java线程案例
生产环境JVM中,会出现由于代码问题导致CPU占用过高
,需要诊断出来具体是哪个java代码导致
CPU占用过高代码:CpuTest
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CpuTest {
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
public static Object lock = new Object();
public static void main(String[] args) {
Task task1 = new Task();
Task task2 = new Task();
executorService.execute(task1);
executorService.execute(task2);
}
static class Task implements Runnable {
@Override
public void run() {
synchronized (lock) {
long sum = 0L;
while (true) {
sum += 1;
}
}
}
}
}
运行诊断
# 后台运行 nohup java CpuTest.java &
[root@localhost tmp]# java CpuTest.java
诊断出具体哪个类哪行代码
-
从宏观到细节
宏观:CPU、内存、网络/O、磁盘I/O
细节:哪个进程导致的
-
确认问题:是否是 CPU 过高导致的应用程序性能问题
-
确认进程 ID,使用top命令查找该【进程】下CPU使用最高的【线程】,并记录线程ID:
top -Hp 进程id
### 发现线程9479占据了大量CPU资源 [root@localhost ~]# top -Hp 9457
-
把十进制的线程id转为16进制:
printf "%x\n" 线程id
[root@localhost ~]# printf "%x\n" 9479 2507
-
分析线程堆栈信息:使用 jstack 命令查看Java 应用程序中所有线程的堆栈信息。定位问题线程堆栈信息,一般会生成快照到文本文件里面进行分析:
jstack -l [pid] > /tmp/log.txt
[root@localhost ~]# jstack -l 9457 > /tmp/log.txt [root@localhost tmp]# cat log.txt | grep -A 10 2507
-
code review:
在22行附近进行code review
jmap
Memory Map for java 用于生成java堆转储快照(heapdump),分析java应用程序的内存使用情况,包括堆的使用情况、对象的数量和类型、每个对象的大小、对象的地址、对象的引用关系等
格式:jmap [-option] pid
参数 | 描述 |
---|---|
-heap | 打印java heap 摘要 |
-histo[:live] | 打印堆中的java对象统计信息 |
-clstats | 打印类加载器统计信息 |
-finalizerinfo | 打印在f-queue中等待执行finalizer方法的对象 |
-dump | 生成java堆的dump文件,dump-options参数有:live(只转储存活的对象,如果没有指定则转储所有对象)、format=b(二进制格式)、file=[Path](将文件转储到指定文件中) |
- jmap -heap 进程id 查看堆信息,这个命令会让JVM 是暂停服务的,所以对线上的运行会产生影响,不推荐该方式。JDK9 及以上版本使用jmap -heap pid命令查看当前heap使用情况时,发现报错,提示需要使用 jhsdb jmap 来替代:
jhsdb jmap --pid 进程id --heap
[root@localhost tmp]# jhsdb jmap --pid 9479 --heap
Attaching to process ID 9479, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 17.0.11+7-LTS-207
using thread-local object allocation.
Garbage-First (G1) GC with 2 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40 #最小堆空闲比例
MaxHeapFreeRatio = 70 #最大堆空闲比例
MaxHeapSize = 1035993088 (988.0MB) #最大堆大小
NewSize = 1363144 (1.2999954223632812MB) #新生代大小
MaxNewSize = 620756992 (592.0MB) #最大新生代大小
OldSize = 5452592 (5.1999969482421875MB) #老年代大小
NewRatio = 2 #新生代和老年代的比例
SurvivorRatio = 8 #新生代中eden区和survivor区的比例
MetaspaceSize = 22020096 (21.0MB) #元空间大小
CompressedClassSpaceSize = 1073741824 (1024.0MB) #压缩类空间大小
MaxMetaspaceSize = 17592186044415 MB #最大元空间大小
G1HeapRegionSize = 1048576 (1.0MB) #G1垃圾收集器每个Region大小
Heap Usage:
G1 Heap:
regions = 988 #堆中区域数量
capacity = 1035993088 (988.0MB) #堆的总容量
used = 13600416 (12.970367431640625MB) #堆已使用的容量
free = 1022392672 (975.0296325683594MB) #堆未使用的容量
1.3127902258745572% used #堆的使用率
G1 Young Generation: #G1垃圾收集器中的年轻代
Eden Space: #年轻代中的Eden区域
regions = 9
capacity = 12582912 (12.0MB)
used = 9437184 (9.0MB)
free = 3145728 (3.0MB)
75.0% used
Survivor Space: #年轻代中的survivor区域
regions = 0
capacity = 1048576 (1.0MB)
used = 906912 (0.864898681640625MB)
free = 141664 (0.135101318359375MB)
86.4898681640625% used
G1 Old Generation: #G1垃圾收集器中的老年代
regions = 5
capacity = 53477376 (51.0MB)
used = 3256320 (3.10546875MB)
free = 50221056 (47.89453125MB)
6.089154411764706% used #老年代的使用率
- 将java堆中存活的对象信息转储到/tmp/dump.bin文件:
jmap -dump:live,format=b,file=/tmp/dump.bin 进程pid
### 生成进程的dump.bin文件 [root@localhost tmp]# jmap -dump:live,format=b,file=/tmp/dump.bin 9457 ### jhat 用于分析jmap生成的heap dump堆转储快照,内置HTTP服务器,对生成的dump文件分析后,在浏览器中查看分析结果 # 注意:jhat命令在IDK9、IDK10中已经被删除,官方建议用VisualVM代替,简单了解即可 # 有很多可视化工具可以帮助查看和分析,例如:JConsole [root@localhost tmp]# jhat /tmp/dump.bin
JConsole
Java Minitoring and Management Console,虚拟机自带的一种监控和管理工具。可以通过图形化界面展示Java应用程序的运行状态和性能指标,包括内存使用情况、线程状态、类加载情况、GC情况等
JConsole的主要用途
用途 | 描述 |
---|---|
监控Java应用程序的运行状态 | 实时展示lava应用程序的运行状态和性能指标,包括CPU使用率、内存使用情况、线程状态、类加载情况、GC情况 |
诊断Java应用程序的问题 | 提供详细的诊断信息,帮助开发人员分析和解决java应用程序的问题,如内存泄漏、死锁等 |
监控远程Java应用程序 | 可以通过JMX(Java Management Extensions)协议监控远程]ava应用程序,远程管理和监控Java应用程序 |
执行JMX操作 | JConsole可以执行JMX操作,如调用Java应用程序中的方法、修改Java应用程序的配置等 |
测试代码
import java.util.ArrayList;
import java.util.List;
public class JConsoleTest {
public static void main(String[] args) throws InterruptedException {
List<Object> list = new ArrayList<Object>();
for (int i = 0; i < 5000; i++) {
Thread.sleep(200);
list.add(new byte[1024 * 1024]); // 1MB
}
}
}
运行
# idea运行程序
# 找到进程ID
D:\Project\redlock>jps
12736 JConsoleTest
16832 RemoteMavenServer36
12244 Launcher
16004 Jps
9464
# 启动JConsole
D:\Project\redlock>jconsole
GC日志
Java虚拟机中垃圾收集器在运行过程中输出的日志信息。主要用于分析垃圾收集器的运行状态、优化垃圾收集器的工作效率以及定位垃圾收集相关的问题。
GC日志包含的内容
- 垃圾收集器的
名称和版本信息
。 - 垃圾收集器的
运行时间、开始时间和结束时间
。 - 垃圾收集器的
运行模式、垃圾收集算法和垃圾收集器的参数设置
。 - 垃圾收集器的运行情况,包括
垃圾收集的次数、垃圾收集的时间、垃圾回收的内存空间等
常见参数
参数配置 | 说明 |
---|---|
-XX:+PrintGC | 简单GC日志,JDK8后过期,后续会被移除,新版采用 -Xlog:gc |
-XX:+PrintGCDetails | GC详细日志,JDK8后过期,后续会被移除,新版采用 -Xlog:gc* |
-Xloggc:gc.log | 输出GC日志到文件,可以指定绝对的路径,JDK8后过期,后续会被移除,新版采用 -Xlog:gc:file=[filepath] |
-verbose:gc | 标准的选项,输出GC日志 |
配置案例实战
### JDK11
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xloggc:gc.log
### JDK17
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc:gc.log
Unified Logging日志格式
新版JDK的GC日志采用 采用 Unified Logging日志格式,JVM内部一直缺少类似的机制,从DK9开始引入Unified Logging格式,是一种新的日志格式
Unified Logging特点
- 统一的日志格式:
统一了GC、JIT、类加载等日志格式,使得日志更加易于分析。
- 可定制化的日志输出:
提供了丰富的日志输出选项,根据需要灵活配置,包括日志级别、标签、输出方式、输出格式等
- 低开销的日志记录:
采用异步日志记录机制,将日志记录与应用程序运行分离,降低日志记录对应用程序性能的影响。
GC日志输出的组成部分
组成部分 | 描述 |
---|---|
时间戳 | 记录GC发生的时间戳,精确到毫秒 |
日志级别 | 日志的级别,包括debug、trace、info、warning、error等 |
日志标签 | 日志的标签,用于区分不同类型的日志 |
日志内容 | 记录GC相关的信息,包括GC算法、GC的时间、GC前后的内存使用情况、回收的对象数量等。 |
新版GC日志配置格式匹配
-Xlog:[selectors]:[output]
-
JVM 采用的是
[tag-set]=[level]
的形式来表示 selectors -
selector 可以进行组合的,不同的selector 之间用逗号分隔
同时输出 gc和 gc+metaspace这两类tag的日志-Xlog:gc=debug,gc+metaspace:gc.log
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc=debug,gc+metaspace:gc.log
-
JVM 提供了通配符 * 来解决精确匹配的问题,比如想要所有tag为gc的debug级别日志 -x1og:gc*=debug
### gc*=debug:指定输出GC相关日志,级别为debug,* 表示所有包含GC标签都会输出日志,debug是最高的日志级别,通配符,日志文件比较详细 -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc*=debug:gc.log
日志内容解析
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc*=info:gc.log
### 日志内容1
[24.824s][info][gc] GC(8) Pause Young (Concurrent Start) (G1 Humongous Allocation) 251M->251M(524M) 0.437ms
这条GC日志记录了程序运行了24.824秒时发生的8次Young GC,回收了0M的内存空间,耗时0.437毫秒
# 字段拆解
[24.824s]:GC发生的时间戳,表示程序运行的时间
[info]:日志级别,表示这是一条信息级别的日志
[gc]:日志标签,表示这是一条Gc相关的日志
GC(8):GC的编号,表示这是第8次GC
# Pause Young (Concurrent Start) (G1 Humongous Allocation)
GC的类型,表示这是一次YoungGC,同时也是一次Humongous Allocation的GC,其中concurrent start表示并发启动的GC
# 251M->251M(524M)
GC前后堆内存的使用情况,其中251M表示GC前的已使用内存,251M表示GC后的已使用内存,524M表示堆内存的总大小
# 0.437ms
GC的耗时,表示这次GC的执行时间
### 日志内容2
[24.824s][info][gc] GC(9) Concurrent Mark Cycle
[24.825s][info][gc] GC(9) Pause Remark 253M->253M(524M) 0.197ms
[24.825s][info][gc] GC(9) Pause Cleanup 253M->253M(524M) 0.035ms
[24.826s][info][gc] GC(9) Concurrent Mark Cycle 1.947ms
这段GC日志记录了程序运行了24.824秒一次Mixed GC,mixed Gc回收了0M的内存空间,耗时1.947毫秒
# [24.824s][info][gc] GC(9) Concurrent Mark Cycle
Mixed Gc的相关信息,表示这是一次Med Gc的开始
# [24.825s][info][gc] GC(9) Pause Remark 253M->253M(524M) 0.197ms
Mixed GC的相关信息,表示这是一次Remark阶段的GC,回收了0M的内存空间,耗时0.197毫秒。
# [24.825s][info][gc] GC(9) Pause Cleanup 253M->253M(524M) 0.035ms
Mixed GC的相关信息,表示这是一次cleanup阶段的GC,回收了0M的内存空间,耗时0.035毫秒。
# [24.826s][info][gc] GC(9) Concurrent Mark Cycle 1.947ms
Mixed GC的相关信息,表示这是一次Mixed GC的结束,耗时1.947毫秒。
GC日志输出到文件中配置
option | 描述 |
---|---|
:file=[/path/file.log] | GC日志输出到文件中 |
filesize=104857600 | 指定单个日志文件大小为100MB,超过这个大小会自动切换到新的日志文件 |
filecount=n | 指定日志文件数量不超过n个,超过这个数量会删除最早的日志文件 |
### 参数配置
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=5,filesize=1M
该配置使用G1垃圾回收器,设置最大垃圾回收暂停时间为100毫秒,JVM堆的初始和最大大小均为524MB,并打印JVM启动参数和输出GC日志到文件portal_gc.log中,文件数量为5个一每个文件大小为1MB-日志格式为info级别 句令时间戳、级别和标签
-XX:+UseG1GC 使用G1垃圾回收器
-XX:MaxGCPauseMillis=100 设置最大垃圾回收暂停时间为100毫秒
-Xms524m 设置JVM堆的初始大小为524MB
-Xmx524m 设置JVM堆的最大大小为524MB
-XX:+PrintCommandLineFlags 打印JVM启动参数
-Xlog:gc*=info:file=portal_gc.log:utctime:,level,tags:filecount=5,filesize=1M
Xlog 指定日志输出方式为日志文件
gc* 指定日志输出类型为Gc相关的日志
info 指定输出日志的级别为info级别
file=portal_gc.log 指定日志输出的文件名为portal_gc.log
utctime 指定日志输出的时间戳使用UTC时间
level,tags 指定日志输出的格式包含级别和标签信息
filecount=5 指定最多保存5个日志文件
filesize=1M 指定每个日志文件的大小为1MB
JVM内存溢出OOM堆栈快照配置
背景
- 服务器配置是8核16g内存,需要部署一个springboot写的电商项目,日访问量100万左右的UV
- 给一份生产环境配置的Jvm参数的值,要求基于jdk11+配置oom时的堆栈快照信息
注意:使用G1收集器的时候,不用指定-Xmn
G1垃圾收集器不需要显式地指定-Xmn参数,通过自适应的方式来优化内存的使用和垃圾收集的效率
在G1中,堆内存被划分为多个区域,每个区域都可以作为年轻代或老年代的一部分
G1的年轻代采用了不同于传统的基于分代的HotSpot垃圾收集器的方式,因此不需要指定-Xmn参数来设置年轻代的大小
G1利用自适应的内存分配策略来动态地调整年轻代的大小
根据堆的使用情况来确定哪些区域应该作为年轻代,以及年轻代的大小
# 参考配置
-server
-XX:+UseG1GC
-Xms524m
-Xmx524m
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=32M
-XX:ActiveProcessorCount=8
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
-XX:+PrintCommandLineFlags
# 参数说明
-server 指定JVM使用服务器模式运行,优化性能
-XX:+UseG1GC 指定使用G1垃圾收集器
-Xms524m 指定JVM堆内存最小值为8G
-Xmx524m 指定JVM堆内存最大值为8G
-XX:MaxGCPauseMillis=200 定最大垃圾回收暂停时间为200毫秒
-XX:G1HeapRegionSize=32M 指定G1垃圾收集器的堆区域大小为32MB
-XX:ActiveProcessorCount=8 指定并行垃圾回收器的线程数为8,在JDK9及之后的版本中,Paral1elGcrhreads参数已被替代为-XX:ActiveprocessorCount参数,用于自动计算并行垃圾回收线程数
-XX:+HeapDumpOnOutOfMemoryError 指定在发生内存溢出时生成堆转储文件
-XX:HeapDumpPath=/tmp/heapdump.hprof 指定堆转储文件的路径
-XX:+PrintCommandLineFlags 打印JVM参数
-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M 定GC日志的输出格式和位置,记录GC相关信息。
配置OOM时的堆栈快照信息
option | 描述 |
---|---|
-XX:+HeapDumpOnOutOfMemoryError | 当发生OOM时,自动生成堆栈快照文件 |
-XX:HeapDumpPath=[path] | 指定堆栈快照文件的输出路径 |
-XX:OnOutOfMemoryError="[cmd];[cmd]" | 当发生OOM时,执行指定的命令 |
# 将在发生OOM时生成一个名为heapdump.hprof的堆栈快照文件,并将其保存到/var/log目录下
-XX:HeapDumpOnOutofMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
# linux
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
# windows
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump.hprof
# 融合其他配置在一起
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=5,filesize=1M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump.hprof
heapdump.hprof 文件可以使用多种工具进行分析包括
堆分析工具可能需要大量的内存和计算资源来加载和分析heapdump.hprof文件建议在高配置的机器上运行堆分析工具,并为其分配足够的内存和计算资源
- MAT(MemoryAnalyzerTool)、JProfiler、VisualVM等”
- 在线分析工具:https://heaphero.io
可视化GC日志分析工具GCEasy
一个在线GC日志分析工具,可以帮助用户快速分析|ava应用程序的GC日志,诊断内存泄漏和性能问题
GCEasy特点
- 支持多种GC日志格式:
包括HotSpot、JRockit、IBM、AzuI等
- 自动分析GC日志,并生成易于阅读和理解的报告:`包括GC统计信息、GC时长、GC频率、堆内存使用情况、内存泄漏
- 等提供多种分析工具和图表:
例如:存使用情况图、GC时长图、GC频率图、内存泄漏图等。
- 提供建议和最佳实践:
帮助用户优化Iava应用程序的性能和内存使用
使用GCEasy的步骤
注意:GCEasy是一个在线工具,需要上传GC日志文件到GCEasy网站进行分析;在上传GC日志文件时,需要确保文件大小不超过GCEasy的限制,并注意文件的隐私和安全性
- 收集Java应用程序的GC日志
- 将GC日志文件上传到GCEasy网站
- 点击“开始分析“按钮,等待分析结果
- 查看分析结果和建议,根据需要进行优化
JVM性能优化方法论
任何java业务做性能优化,都需要掌握JVM内部的工作机制和应用程序的特性。某个节点性能优化接近极致的时候,需要从局部跳到宏观层面进行分析,考虑自己和团队的ROI(缺少业务场景的性能优化都是垃圾
)。
优化方式 | 说明 |
---|---|
监控JVM性能 | 对JVM的运行情况进行监控,以了解应用程序的瓶颈和性能瓶颈;可以使用JVM自带的工具,如jstat、jmap、jstack等,或者第三方工具,如VisualVM、JProfiler等 |
压测基准指标 | 对程序进行压测,得出接口对应的吞吐量、响应时间等;外部现象(对用户体验来说,就是响应速度):可以用压测工具jmeter进行压测得出相关性能指标 ;内部现象:分析GC情况,是JVM性能调优的重要因素,需要掌握GC的工作机制和GC日志的含义,可以使用JVM自带的GC日志或者第三方工具,如GCEasy等来分析GC情况,了解GC的频率、时间、内存占用、吞吐量等情况 |
调整JVM参数 | 通过调整堆大小、GC算法、线程池大小等参数来提高应用程序的性能。注意:不同的应用程序和环境可能需要不同的M参数配置,比如I/O密集型和CPU密集型应用 |
二次压测 | 通过调整jvm参数后,二次压测看性能指标提升还是下降。单一参数调整后便于观察。外部:接口对应的吞吐量、响应时间是否更优 ;内部:GC日志,看吞吐量,GC次数,停顿时间变化 |
其他优化方式 | 其他优化方式 |
优化代码 | 通过避免不必要的对象创建、减少同步操作、使用缓存等方式来优化代码。注意:代码优化应该遵循“先正确,再优化”的原则,不应该牺牲代码的可读性和可维护性 |
使用并发编程 | 使用多线程、线程池等方式来提高并发性能,比如调整线程池的队列长度,存活线程数量等 。注意:并发编程需要考虑线程安全和锁竞争等问题,需要进行正确的设计和实现 |
使用缓存 | 可以使用本地缓存、分布式缓存等方式来提高数据访问性能。注意:缓存需要考虑缓存一致性和缓存失效等问题,需要进行正确的设计和实现 |
避免IO阻塞 |
分析结论
不同堆空间大小堆系统影响比较大,高内存则可以减少GC次数,得到比较高的吞吐量。测试的时候可以每2G的内存增长进行测试,增加到一定堆大小后,ROI会逐步下降,找到一定的峰值即可,找到最佳ROI的JVM参数
压测环境准备
测试接口:http://192.168.10.88:8080/api/product2jvm/query
Product
package com.soulboy.controller;
public class Product {
private int price;
private String title;
public Product() {
}
public Product(int price, String title) {
this.price = price;
this.title = title;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public String toString() {
return "Product{" +
"price=" + price +
", title='" + title + '\'' +
'}';
}
}
Product2JVMController
package com.soulboy.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/product2jvm")
public class Product2JVMController {
/**
* 随机产生大小不同的对象
* @return
* @throws InterruptedException
*/
@RequestMapping("/query")
public Map<String,Object> query() throws InterruptedException {
//随机数
int num = (int) (Math.random() * 100) + 1;
//5MB大小字节数组
Byte[] bytes = new Byte[5 * 1024 * 1024];
ArrayList<Product> productList = new ArrayList<>();
for (int i = 0; i < num; i++) {
Product product = new Product();
product.setPrice((int) Math.random() * 100);
product.setTitle("小象NO." + i);
productList.add(product);
}
Thread.sleep(5);
HashMap<String, Object> map = new HashMap<>(16);
map.put("data", productList);
return map;
}
}
Jmeter压测工具准备,测试计划 200并发,循环500次
堆大小配置,FullGC次数的性能影响
性能优化初始值(堆大小1G)
# 性能优化初始值 堆大小1G
-Xms1g
-Xmx1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=32M
-XX:ActiveProcessorCount=8
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
-XX:+PrintCommandLineFlags
-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
-Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=32M -XX:ActiveProcessorCount=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump1.hprof -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
外部指标
关键指标 | 描述 |
---|---|
总请求次数 | 10W |
吞吐量 | 716 |
总耗时 | 2分19秒 |
内部指标(GCEasy)
关键指标 | 描述 |
---|---|
吞吐量 | 64% |
平均暂停GC时间 | 4.95ms |
Full GC总次数 | 3576次 |
Full GC总耗时 | 1 min 28 sec 928 ms |
Young GC总次数 | 22819 |
Young GC总耗时 | 21 sec 853 ms |
压缩GC日志portal_gc.zip,上传至gceasy
性能优化调整后堆大小8G
# 性能优化调整后堆大小8G
-Xms8g
-Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=32M
-XX:ActiveProcessorCount=8
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
-XX:+PrintCommandLineFlags
-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
-Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=32M -XX:ActiveProcessorCount=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump1.hprof -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
外部指标
关键指标 | 描述 |
---|---|
总请求次数 | 10W |
吞吐量 | 2184 |
总耗时 | 45秒 |
内部指标(GCEasy)
关键指标 | 描述 |
---|---|
吞吐量 | 93.213% |
平均暂停GC时间 | 5.16 ms |
Full GC总次数 | 401次 |
Full GC总耗时 | 11 sec 860 ms |
Young GC总次数 | 3164 |
Young GC总耗时 | 8 sec 160 ms |
压缩GC日志portal_gc.zip,上传至gceasy
不同垃圾收集器对性能的影响
分析结论
- 不同垃圾回收器对程序的吞吐量影响:
同等条件下G1收集器会比Parallel收集器强吞吐量更高,响应时间更低,完成同等数量的请求耗时更少
- G1和ZGC等更适合大内存的情况业务:
尤其是16G内存以上的业务
- 内存太小可以使用:
ParallelGC
ParallelGC垃圾收集器
JDK8默认的收集器ParallelGC
性能优化初始值(堆大小1G),垃圾收集器使用ParallelGC,其他参数保持不变
# 性能优化初始值 堆大小1G,垃圾收集器使用ParallelGC,其他参数保持不变
-Xms1g
-Xmx1g
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=32M
-XX:ActiveProcessorCount=8
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
-XX:+PrintCommandLineFlags
-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
-Xms1g -Xmx1g -XX:+UseParallelGC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=32M -XX:ActiveProcessorCount=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump1.hprof -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
外部指标
关键指标 | 描述 |
---|---|
总请求次数 | 10W |
吞吐量 | 413 |
总耗时 | 4分2秒 |
内部指标
关键指标 | 描述 |
---|---|
吞吐量 | 38.907% |
平均暂停GC时间 | 20.6 ms |
Full GC总次数 | 2750次 |
Full GC总耗时 | 1 min 50 sec 461 ms |
Minor GC总次数 | 7150 |
Minor GC总耗时 | 1 min 33 sec 308 ms |
ZGC垃圾收集器
JDK17可以使用ZGC
- G1和ZGC等更适合大内存的情况业务:
尤其是16G内存以上的业务
- 内存太小可以使用:
ParallelGC
# 性能优化初始值 堆大小1G,垃圾收集器使用ZGC,其他参数保持不变
-Xms1g
-Xmx1g
-XX:+UseZGC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=32M
-XX:ActiveProcessorCount=8
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
-XX:+PrintCommandLineFlags
-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
# ZGC 1G
-Xms1g -Xmx1g -XX:+UseZGC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=32M -XX:ActiveProcessorCount=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump1.hprof -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
# ZGC 16G
-Xms16g -Xmx16g -XX:+UseZGC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=32M -XX:ActiveProcessorCount=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump1.hprof -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
外部指标(1G ZGC)
关键指标 | 描述 |
---|---|
总请求次数 | 10W |
吞吐量 | 2081 |
错误率 | 91.94% |
总耗时 | 未完成:java.lang.OutOfMemoryError: Java heap space |
### 找到占用8080端口号的进程,kill掉
C:\Users\chao1>netstat -ano | findstr :8080
TCP 127.0.0.1:8080 127.0.0.1:8080 ESTABLISHED 4636
外部指标(16G ZGC)
关键指标 | 描述 |
---|---|
总请求次数 | 10W |
吞吐量 | 810 |
错误率 | 0% |
总耗时 | 2分03秒 |
内部指标(16G ZGC)
ZGC的内部指标没有Full GC,但是在一些阶段也会存在SWT,以下阶段都是SWT,所以会比较重点关注
关键指标 | 描述 |
---|---|
吞吐量 | 99.997% |
平均暂停GC时间 | 0.00818 ms |
Pause Mark Start(初始标记)总次数 | 285次 |
Pause Mark Start(初始标记)总耗时 | 1.28 ms |
Pause Mark End(最终标记)总次数 | 285次 |
Pause Mark End(最终标记)总耗时 | 3.86 ms |
Pause Relocate Start(初始转移)总次数 | 285次 |
Pause Relocate Start(初始转移)总耗时 | 1.85 ms |
JVM诊断工具Arthas
诞生背景
- JDK自带的命令分析工具比较多,但是使用不灵活,排查诊断问题步骤繁琐
- 开源和付费的图形化工具适合开发和测试环境进行分析使用
生产环境里面基本都是Linux命令行界面
虽然支持远程连接,但是需要各种vpn、防火墙等配等
什么是Arthas
- ArthasGitHub
- Arthas官网
- 阿里开源的Java诊断工具,它可以在运行时对ava应用程序进行动态诊断和调试
这个类从哪个jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到IVM 的实时运行状态?
怎么快速定位应用的热点,生成火焰图?
怎样直接从IVM 内查找某个类的实例?
环境说明
- Arthas 支持JDK6+,支持 Linux/Mac/Windows,采用命令行交互模式
- 提供丰富的 rab 自动补全功能,进一步方便进行问题的定位和诊断
- 也支持浏览器直接访问对应的ip+端口,固定端口 8563
- 默认情况下,Arthas 只listen 127.0.0.1,
所以如果想从远程连接,使用
--target -ip参数指定 listen 的IP
安装
http://192.168.10.57:8563
### 必须要有运行的java进程,否则无法监听,`math-game`是一个简单的程序,每隔一秒生成一个随机数,再执行质因数分解,并打印出分解结果。
curl -O https://arthas.aliyun.com/math-game.jar
java -jar math-game.jar
### 安装运行arthas
# 可以通过 --username 选项来指定用户,默认值是arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar --target-ip 0.0.0.0 --password 123
### arthas运行日志
[root@localhost ~]# tail ~/logs/arthas/arthas.log
### 退出arthas
* 如果只是退出当前的连接,其他客户端不受影响,可以用 quit或者exit命令
* 目标进程上的 arthas 还会继续运行,端口保持开放,下次连接时执行java -jar arthas-boot.jar 可以直接连接上
* 如果想完全退出 arthas,可以执行stop命令
### 客户端再次连接
java -jar arthas-boot.jar
### 选择应用 java 进程
根据pid和进程名选择
# dashboard
[arthas@10056]$ dashboard
ID NAME GROUP PRIORITY STATE %CPU DELTA_TIME TIME INTERRUPTE DAEMON
1 main main 5 TIMED_WAIT 0.0 0.000 0:0.087 false false
23 arthas-NettyHttpTelnetBootstrap- system 5 RUNNABLE 0.0 0.000 0:0.078 false true
12 Attach Listener system 9 RUNNABLE 0.0 0.000 0:0.031 false true
17 arthas-NettyHttpTelnetBootstrap- system 5 RUNNABLE 0.0 0.000 0:0.013 false true
24 arthas-command-execute system 5 TIMED_WAIT 0.0 0.000 0:0.002 false true
2 Reference Handler system 10 RUNNABLE 0.0 0.000 0:0.000 false true
3 Finalizer system 8 WAITING 0.0 0.000 0:0.000 false true
4 Signal Dispatcher system 9 RUNNABLE 0.0 0.000 0:0.000 false true
10 Notification Thread system 9 RUNNABLE 0.0 0.000 0:0.000 false true
14 arthas-timer system 9 WAITING 0.0 0.000 0:0.000 false true
18 arthas-NettyWebsocketTtyBootstra system 5 RUNNABLE 0.0 0.000 0:0.000 false true
19 arthas-NettyWebsocketTtyBootstra system 5 RUNNABLE 0.0 0.000 0:0.000 false true
20 arthas-shell-server system 9 TIMED_WAIT 0.0 0.000 0:0.000 false true
21 arthas-session-manager system 9 TIMED_WAIT 0.0 0.000 0:0.000 false true
25 Timer-for-arthas-dashboard-2a3b7 system 5 RUNNABLE 0.0 0.000 0:0.000 false true
11 Common-Cleaner InnocuousThreadG 8 TIMED_WAIT 0.0 0.000 0:0.000 false true
Memory used total max usage GC
heap 26M 64M 988M 2.71% gc.g1_young_generation.count 4
g1_eden_space 5M 18M -1 27.78% gc.g1_young_generation.time(ms) 15
g1_old_gen 18M 43M 988M 1.90% gc.g1_old_generation.count 0
g1_survivor_space 3M 3M -1 100.00% gc.g1_old_generation.time(ms) 0
nonheap 26M 29M -1 88.27%
codeheap_'non-nmethods' 1M 2M 5M 21.81%
metaspace 18M 19M -1 98.78%
codeheap_'profiled_nmethods' 3M 3M 117M 3.01%
compressed_class_space 2M 2M 1024M 0.22%
codeheap_'non-profiled_nmeth 586K 2496K 120036K 0.49%
ods'
mapped 0K 0K - 0.00%
direct 4M 4M - 100.00%
Runtime
os.name Linux
os.version 3.10.0-957.el7.x86_64
java.version 17.0.11
java.home /usr/local/software/jdk17
systemload.average 0.00
processors 2
timestamp/uptime Wed Aug 14 17:55:03 CST 2024/364s
常用基础命令
jvm 相关
- dashboard - 当前系统的实时数据面板(线程、内存、系统运行时)
字段 | 说明 |
---|---|
id | Java 级别的线程 ID |
name | 线程名称 |
group | 线程组名称 |
proirity | 线程优先级,1~10之间的数字,越大优先级越高 |
state | 线程的状态 |
cpu | 线程的 cpu 使用率 |
delta time | 上次采样之后线程运行增量 CPU 时间,数据格式为秒 |
time | 线程运行总 CPU 时间,数据格式为 分:秒 |
interupted | 当前线程是否中断 |
daemon | 是否是 daemon 守护线程 |
字段 | 说明 |
---|---|
used | 当前使用了多少内存 |
total | 总共分配了多少内存 |
max | 最大使用了多少 |
usage | 使用比例 |
gc | 垃圾回收器 |
- getstatic - 查看类的静态属性
- heapdump - dump java heap, 类似 jmap 命令的 heap dump 功能
生成堆栈快照 heapdump /tmp/heapdumpLog.hprof
- jvm - 查看当前 JVM 的信息
- logger - 查看和修改 logger
- mbean - 查看 Mbean 的信息
- memory - 查看 JVM 的内存信息
- ognl - 执行 ognl 表达式
- perfcounter - 查看当前 JVM 的 Perf Counter 信息
- sysenv - 查看 JVM 的环境变量
- sysprop - 查看和修改 JVM 的系统属性
option | 说明 |
---|---|
sysprop | 查看所有属性 |
syspropjava.version | 查看单个属性 |
sysprop user.country CN | 修改某个属性 |
- thread - 查看当前 JVM 的线程堆栈信息
option | 说明 |
---|---|
--all | 显示所有匹配的线程,默认就是第一页线程信息 |
-i | 设置cpu统计时的采样间隔,单位为毫秒 thread-i 2000 (2秒) |
[id] | 查看指定ID的线程堆栈 thread 54 |
-n | 查看CPU使用率最高的TOpN个线程,如果值为-1表示显示所有线程thread-n 3 |
-b | 展示阻塞线程thread -b |
--state | 根据线程状态筛选线程 thread--state TIMED WAITING ; 状态类型:NEW,RUNNABLE, TIMED WAITING, WAITING, BLOCKED,TERMINATED |
class/classloader 相关
- classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource
- dump - dump 已加载类的 byte code 到特定目录
- jad - 反编译指定已加载类的源码
option | 说明 |
---|---|
jad net.soulboy.archwebproject.ProductController | 反编译指定已加载类的源码 |
--source-only | 只显示源码,不显示ClassLoader信息 |
jad net.soulboy.archwebproject.ProductController query | 反编译某个类的某方法(本地修改,线上没有生效),是不是git没有提交好,是否变更成功。比下载到本地使用反编译工具查看效率高很多 |
- mc - 内存编译器,内存编译
.java
文件为.class
文件 - redefine - 加载外部的
.class
文件,redefine 到 JVM 里 - retransform - 加载外部的
.class
文件,retransform 到 JVM 里 - sc - 查看 JVM 已加载的类信息
option | 说明 |
---|---|
sc -d -f net.soulboy.archwebproject.ProductController | 查看JVM 已加载的类信息 -d 详情,-f类属性输出 |
- sm - 查看已加载类的方法信息
option | 说明 |
---|---|
sm net.soulboy.archwebproject.ProductController | 查看JVM 已加载的类的方法信息 |
monitor/watch/trace 相关
注意:请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行
stop或将增强过的类执行
reset命令。
- monitor - 方法执行监控
- stack - 输出当前方法被调用的调用路径
- trace - 方法内部调用路径,并输出方法路径上的每个节点上耗时
- tt - 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
- watch - 方法执行数据观测
profiler/火焰图
- profiler - 使用async-profiler在新窗口打开对应用采样,生成火焰图
- jfr - 动态开启关闭 JFR 记录
鉴权
- auth - 鉴权
options
- options - 查看或设置 Arthas 全局开关
管道
Arthas 支持使用管道对上述命令的结果进行进一步的处理,如sm java.lang.String * | grep 'index'
- grep - 搜索满足条件的结果
- plaintext - 将命令的结果去除 ANSI 颜色
- wc - 按行统计输出结果
后台异步任务
当线上出现偶发的问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,
详情请参考这里
- 使用
>
将结果重写向到日志文件,使用&
指定命令是后台运行,session 断开不影响任务执行(生命周期默认为 1 天) - jobs - 列出所有 job
- kill - 强制终止任务
- fg - 将暂停的任务拉到前台执行
- bg - 将暂停的任务放到后台执行
基础命令
- base64 - base64 编码转换,和 linux 里的 base64 命令类似
- cat - 打印文件内容,和 linux 里的 cat 命令类似
- cls - 清空当前屏幕区域
- echo - 打印参数,和 linux 里的 echo 命令类似
- grep - 匹配查找,和 linux 里的 grep 命令类似
- help - 查看命令帮助信息
- history - 打印命令历史
- keymap - Arthas 快捷键列表及自定义快捷键
- pwd - 返回当前的工作目录,和 linux 命令类似
- quit - 退出当前 Arthas 客户端,其他 Arthas 客户端不受影响
- reset - 重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类
- session - 查看当前会话的信息
- stop - 关闭 Arthas 服务端,所有 Arthas 客户端全部退出
- tee - 复制标准输入到标准输出和指定的文件,和 linux 里的 tee 命令类似
- version - 输出当前目标 Java 进程所加载的 Arthas 版本号
方法调用诊断命令
背景需求
- 监视一个时间段内指定方法的执行次数,成功次数,失败次数,耗时等这些信息
- 输出当前方法被调用的调用路径,方法路径上的每个节点上耗时
- 观察:
返回值 、抛出异常、入参
monitor(方法执行监控)
- 非实时响应,需要对应的方法有被调用才行,所以需要触发web接口请求
- 监视一个时间段中指定方法的执行次数,成功次数,失败次数,耗时等这些信息
option | 描述 |
---|---|
-c [周期值] | 监视器间隔(以秒为单位),默认60秒不包括类名式 |
monitor -c 2 com.soulboy.controller.Product2JVMController find
http://127.0.0.1:8080/api/product2jvm/find?id=5
monitor -c 2 com.soulboy.controller.Product2JVMController query
http://127.0.0.1:8080/api/product2jvm/query
stack(输出当前方法被调用的调用路径)
输出当前方法被调用的调用路径
option | 描述 |
---|---|
-c [周期值] | 监视器间隔(以秒为单位),默认60秒不包括类名式 |
stack com.soulboy.controller.CommonUtil checkIdRange
http://127.0.0.1:8080/api/product2jvm/find?id=5
Product2JVMController
package com.soulboy.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/product2jvm")
public class Product2JVMController {
/**
* 随机产生大小不同的对象
* @return
* @throws InterruptedException
*/
@RequestMapping("/find")
public Map<String,Object> find(int id) throws InterruptedException {
Thread.sleep(500);
boolean check = test1(id);
HashMap<String, Object> map = new HashMap<>(16);
map.put("id", id);
map.put("check",check);
return map;
}
private boolean test1(int id) throws InterruptedException {
Thread.sleep(800);
return test2(id);
}
private boolean test2(int id) throws InterruptedException {
Thread.sleep(200);
return CommonUtil.checkIdRange(id);
}
}
CommonUtil
package com.soulboy.controller;
public class CommonUtil {
public static final boolean checkIdRange(int id) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (id < 0) {
return false;
} else {
return true;
}
}
}
trace(调用链各节点上耗时)
方法内部调用,并输出方法路径上的每个节点上耗时
,定位因 RT 高导致的性能问题,每次只能跟踪一级方法的调用链路
输出结果字段说明
字段名 | 说明 |
---|---|
ts | 时间戳,表示日志记录的时间 |
thread name | 线程名称,表示当前执行该日志记录的线程名称,该字段的值为http-nio-8080-exec-6 |
id | 线程ID,表示当前执行该日志记录的线程ID |
is_daemon | 是否为守护线程,该字段的值为true,表示该线程是守护线程 |
priority | 线程优先级,该字段的值为5,表示该线程的优先级为5 |
TCCL | 线程上下文类加载器,表示当前线程的上下文类加载器为TomcatEmbeddedWebappClassLoader |
示例:*显示整个调用链
find
标红:test1()方法耗时比较严重
trace com.soulboy.controller.Product2JVMController *
http://127.0.0.1:8080/api/product2jvm/find?id=5
query
trace com.soulboy.controller.Product2JVMController *
http://127.0.0.1:8080/api/product2jvm/query
示例:过滤出Product2JVMController类中find方法调用链中大于10毫秒的方法
每次只能跟踪一级方法的调用链路,find下面只会显示test1()
trace com.soulboy.controller.Product2JVMController find '#cost > 10'
http://127.0.0.1:8080/api/product2jvm/find?id=5
默认情况下,trace不会包含jdk里的函数调用
- 如果希望trace jdk里的函数,需要显式设置
--skipJDKMethod false
trace --skipJDKMethod false com.soulboy.controller.Product2JVMController find
http://127.0.0.1:8080/api/product2jvm/find?id=5
watch(方法执行数据观测)
watch-方法执行数据观测,观察:返回值、抛出异常、入参,通过编写OGNL 表达式进行对应变量的查看
输出结果字段说明
字段名 | 说明 |
---|---|
location= | Exit:正常退出 。AtExit:异常退出 |
ts | 调用方法的时间戳 |
cost | 方法耗时 |
result | 入参、目标/当前对象、返回值 |
- 默认的观察表达式,默认值是
{params,target,returnObj}
- 也可以指定观察返回值
watch net.soulboy.archwebproject.Productcontroller * {params,returnObj}
watch com.soulboy.controller.Product2JVMController find
http://127.0.0.1:8080/api/product2jvm/find?id=5
扩展对象级别(1默认),最大值为4 -x 4
watch com.soulboy.controller.Product2JVMController find -x 4
也可以只观察入参和返回值:watch com.soulboy.controller.Product2JVMController * {params,returnObj} -x 4
综合案例
jad
:把字节码文件反编译成源代码(反编译出源码)mc
:在内存中把源代码编译成字节码文件(修改方法逻辑),Memory Compiler/内存编译器,编译.java文件生成.class
# 在内存中编译Hello.java为Hello.class mc /root/Hello.java # 可以通过-d命令指定输出目录(输出对应的路径会有package路径) mc -d /root/test /root/Hello.java
redefine
:把新生成的字节码文件在内存中执行(替换新的字节码文件)
加载外部的.class文件,redefine到JVM里
redefine后的原来的类不能恢复,redefine有可能失败(比如增加了新的field)
不允许新增加field/method,只能修改内部方法和逻辑
正在跑的函数,没有退出不能生效
动态修改方法内部逻辑
修改某个方法的接口返回值,增加多一个参数
字节码增强,如果重启JVM,则增强的部分会失效
步骤
# 使用jad反编译 D:\Project\redlock\Product2JVMController.java
[arthas@14264]$ pwd
D:\Project\redlock
[arthas@14264]$ jad --source-only com.soulboy.controller.Product2JVMController > Product2JVMController.java
# 修改为3个参数(不能新增方法,也不能新增字段)
map.put("id", id);
map.put("check", check);
map.put("productId", id); # 新增
# 使用mc内存中对新的代码编译 D:\Project\redlock\Projectredlock\com\soulboy\controller\Product2JVMController.class
mc Product2JVMController.java -d D:\\Project\\redlock
# 使用redefine命令加载新的字节码
redefine D:\\Project\\redlock\\Projectredlock\\com\\soulboy\\controller\\Product2JVMController.class
# 返回值多了一个参数 http://127.0.0.1:8080/api/product2jvm/find?id=5
{
"productId": 5,
"id": 5,
"check": true
}