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 主要组成部分
1+---------------------------+--------------------
2| 类加载子系统 (Class Loader Subsystem) |
3+---------------------------+--------------------
4| 运行时数据区 (Runtime Data Area) |
5| - 方法区 (Method Area) |
6| - 堆 (Heap) |
7| - Java 栈 (Java Stacks) |
8| - 本地方法栈 (Native Method Stacks) |
9| - 程序计数器 (Program Counter) |
10+---------------------------+--------------------
11| 执行引擎 (Execution Engine) |
12| - 解释器 (Interpreter) |
13| - 即时编译器 (Just-In-Time Compiler, JIT) |
14| - 垃圾收集器 (Garbage Collector, GC) |
15+---------------------------+--------------------
16| 本地接口 (Native Interface) |
17+---------------------------+--------------------
18| 本地方法库 (Native Method Libraries) |
19+---------------------------+--------------------
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
查看当前栈帧信息
1package com.soulboy.jvm;
2
3public class StackFrameDemo {
4 public static void main(String[] args) {
5 StackFrameDemo stackFrameDemo = new StackFrameDemo();
6 stackFrameDemo.method1();
7 }
8 public void method1(){
9 String str = "Hello";
10 method2(str);
11 System.out.println("method1--完成");
12 }
13
14 private void method2(String str) {
15 int num = 123;
16 method3(str, num);
17 System.out.println("method2--完成");
18 }
19
20 private void method3(String str, int num) {
21 double d = 3.14;
22 method4(str, num, d);
23 System.out.println("method3--完成");
24 }
25
26 private void method4(String str, int num, double d) {
27 //查看当前栈帧信息
28 System.out.println("打印当前栈帧信息");
29 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
30 for (StackTraceElement stackTraceElement : stackTrace) {
31 System.out.println(stackTraceElement.toString());
32 }
33 System.out.println("method4--完成");
34 }
35}
输出
1打印当前栈帧信息
2java.base/java.lang.Thread.getStackTrace(Thread.java:1610)
3com.soulboy.jvm.StackFrameDemo.method4(StackFrameDemo.java:29)
4com.soulboy.jvm.StackFrameDemo.method3(StackFrameDemo.java:22)
5com.soulboy.jvm.StackFrameDemo.method2(StackFrameDemo.java:16)
6com.soulboy.jvm.StackFrameDemo.method1(StackFrameDemo.java:10)
7com.soulboy.jvm.StackFrameDemo.main(StackFrameDemo.java:6)
8method4--完成
9method3--完成
10method2--完成
11method1--完成
虚拟机栈溢出
栈越小,递归调用的次数就越少,因为栈空间不足导致栈溢出异常
栈越大,递归调用的次数就越多,因为有足够的栈空间来存储方法调用的信息
1public class StackSizeDemo {
2 private static int count = 0;
3
4 public static void main(String[] args) {
5 try {
6 recursiveMethod();
7 } catch (Throwable t) {
8 System.out.println("栈溢出,运行 " + count + " 次后抛出异常");
9 t.printStackTrace();
10 }
11 }
12 private static void recursiveMethod() {
13 count++;
14 recursiveMethod();
15 System.out.println("递归次数:" + count);
16 }
17}
1### 调整虚拟机栈大小之前输出
2栈溢出,运行 27709 次后抛出异常
3
4### 调整虚拟机栈大小之前输后 -Xss100k
5The Java thread stack size specified is too small. Specify at least 180k
6Error: Could not create the Java Virtual Machine.
7Error: A fatal exception has occurred. Program will exit.
8
9### 调整虚拟机栈大小之前输后 -Xss180k
10栈溢出,运行 1696 次后抛出异常
JVM 虚拟机堆参数调整(Jmeter5 压测)
通过调整不同的VM堆参数,查看相关指标
调整参数一:-Xms64m -Xmx64m
调整参数二:-Xms640m -Xmx640m
压测接口 URI: http://192.168.10.88:8080/api/v1/data/compute
Controller
1package com.soulboy.controller;
2
3import org.springframework.web.bind.annotation.RequestMapping;
4import org.springframework.web.bind.annotation.RestController;
5
6@RestController
7@RequestMapping("/api/v1/data")
8public class DataController {
9
10 @RequestMapping("compute")
11 public String compute(){
12 //每次申请1MB大小(在堆中)
13 Byte[] bytes = new Byte[1 * 1024 * 1024];
14 return "success";
15 }
16
17}
调整参数一:-Xms64m -Xmx64m
控制台输出
1java.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 即不限制,使用的是本地内存,不像旧版的永久代是堆内存;如果不限制元空间的大小,可能会导致元空间占用过多的内存,从而引起内存溢出 |
查看元空间大小
代码
1package com.soulboy.jvm;
2
3public class HeapDemo {
4 public static void main(String[] args) throws InterruptedException {
5 System.out.println("Hello World!");
6 Thread.sleep(10000000);
7 }
8}
查看命令
1### jsp 查看进程号
2D:\Project\redlock\src\main\java\com\soulboy\thread>jps
320052 HeapDemo
4
5### jinfo 查看 Metaspace分配内存空间 默认是21MB 22020096/1024/1024=21
6jinfo -flag MetaspaceSize 20052
7-XX:MetaspaceSize=22020096
8
9### jinfo 查看 Metaspace最大空间
10jinfo -flag MaxMetaspaceSize 20052
11-XX:MaxMetaspaceSize=18446744073709551615
调整:自定义 Metaspace 大小
1# 调整
2-XX:MetaspaceSize=126m -XX:MaxMetaspaceSize=524m
3
4# 调整后查看 132120576 ÷ 1024 ÷ 1024 = 126
5jinfo -flag MetaspaceSize 10784
6-XX:MetaspaceSize=132120576
7
8# 调整后查看 49453824 ÷ 1024 ÷ 1024 = 524
9jinfo -flag MaxMetaspaceSize 10784
10-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
1/*
2 * Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
3 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
4 *
5 *
6 *
7 *
8 *
9 *
10 *
11 *
12 *
13 *
14 *
15 *
16 *
17 *
18 *
19 *
20 *
21 *
22 *
23 *
24 */
25
26/**
27 * Defines the foundational APIs of the Java SE Platform.
28 *
29 * <dl class="notes">
30 * <dt>Providers:</dt>
31 * <dd> The JDK implementation of this module provides an implementation of
32 * the {@index jrt jrt} {@linkplain java.nio.file.spi.FileSystemProvider
33 * file system provider} to enumerate and read the class and resource
34 * files in a run-time image.
35 * The jrt file system can be created by calling
36 * {@link java.nio.file.FileSystems#newFileSystem
37 * FileSystems.newFileSystem(URI.create("jrt:/"))}.
38 * </dd>
39 * </dl>
40 *
41 * @toolGuide java java launcher
42 * @toolGuide keytool
43 *
44 * @provides java.nio.file.spi.FileSystemProvider
45 *
46 * @uses java.lang.System.LoggerFinder
47 * @uses java.net.ContentHandlerFactory
48 * @uses java.net.spi.URLStreamHandlerProvider
49 * @uses java.nio.channels.spi.AsynchronousChannelProvider
50 * @uses java.nio.channels.spi.SelectorProvider
51 * @uses java.nio.charset.spi.CharsetProvider
52 * @uses java.nio.file.spi.FileSystemProvider
53 * @uses java.nio.file.spi.FileTypeDetector
54 * @uses java.security.Provider
55 * @uses java.text.spi.BreakIteratorProvider
56 * @uses java.text.spi.CollatorProvider
57 * @uses java.text.spi.DateFormatProvider
58 * @uses java.text.spi.DateFormatSymbolsProvider
59 * @uses java.text.spi.DecimalFormatSymbolsProvider
60 * @uses java.text.spi.NumberFormatProvider
61 * @uses java.time.chrono.AbstractChronology
62 * @uses java.time.chrono.Chronology
63 * @uses java.time.zone.ZoneRulesProvider
64 * @uses java.util.spi.CalendarDataProvider
65 * @uses java.util.spi.CalendarNameProvider
66 * @uses java.util.spi.CurrencyNameProvider
67 * @uses java.util.spi.LocaleNameProvider
68 * @uses java.util.spi.ResourceBundleControlProvider
69 * @uses java.util.spi.ResourceBundleProvider
70 * @uses java.util.spi.TimeZoneNameProvider
71 * @uses java.util.spi.ToolProvider
72 * @uses javax.security.auth.spi.LoginModule
73 *
74 * @moduleGraph
75 * @since 9
76 */
77module java.base {
78 exports java.io;
79 exports java.lang;
80 exports java.lang.annotation;
81 exports java.lang.constant;
82 exports java.lang.invoke;
83 exports java.lang.module;
84 exports java.lang.ref;
85 exports java.lang.reflect;
86 exports java.lang.runtime;
87 exports java.math;
88 exports java.net;
89 exports java.net.spi;
90 exports java.nio;
91 exports java.nio.channels;
92 exports java.nio.channels.spi;
93 exports java.nio.charset;
94 exports java.nio.charset.spi;
95 exports java.nio.file;
96 exports java.nio.file.attribute;
97 exports java.nio.file.spi;
98 exports java.security;
99 exports java.security.cert;
100 exports java.security.interfaces;
101 exports java.security.spec;
102 exports java.text;
103 exports java.text.spi;
104 exports java.time;
105 exports java.time.chrono;
106 exports java.time.format;
107 exports java.time.temporal;
108 exports java.time.zone;
109 exports java.util;
110 exports java.util.concurrent;
111 exports java.util.concurrent.atomic;
112 exports java.util.concurrent.locks;
113 exports java.util.function;
114 exports java.util.jar;
115 exports java.util.random;
116 exports java.util.regex;
117 exports java.util.spi;
118 exports java.util.stream;
119 exports java.util.zip;
120 exports javax.crypto;
121 exports javax.crypto.interfaces;
122 exports javax.crypto.spec;
123 exports javax.net;
124 exports javax.net.ssl;
125 exports javax.security.auth;
126 exports javax.security.auth.callback;
127 exports javax.security.auth.login;
128 exports javax.security.auth.spi;
129 exports javax.security.auth.x500;
130 exports javax.security.cert;
131 exports com.sun.crypto.provider to jdk.crypto.cryptoki;
132 exports com.sun.security.ntlm to java.security.sasl;
133 exports jdk.internal.access to
134 java.desktop,
135 java.logging,
136 java.management,
137 java.naming,
138 java.rmi,
139 jdk.charsets,
140 jdk.incubator.foreign,
141 jdk.jartool,
142 jdk.jfr,
143 jdk.jlink,
144 jdk.net;
145 exports jdk.internal.access.foreign to jdk.incubator.foreign;
146 exports jdk.internal.event to jdk.jfr;
147 exports jdk.internal.invoke to jdk.incubator.foreign;
148 exports jdk.internal.javac to
149 java.compiler,
150 jdk.compiler,
151 jdk.jshell;
152 exports jdk.internal.jimage to jdk.jlink;
153 exports jdk.internal.jimage.decompressor to jdk.jlink;
154 exports jdk.internal.jmod to
155 jdk.compiler,
156 jdk.jlink;
157 exports jdk.internal.loader to
158 java.instrument,
159 java.logging,
160 java.naming,
161 jdk.incubator.foreign;
162 exports jdk.internal.logger to java.logging;
163 exports jdk.internal.misc to
164 java.desktop,
165 java.logging,
166 java.management,
167 java.naming,
168 java.net.http,
169 java.rmi,
170 java.security.jgss,
171 jdk.attach,
172 jdk.charsets,
173 jdk.compiler,
174 jdk.crypto.cryptoki,
175 jdk.incubator.foreign,
176 jdk.incubator.vector,
177 jdk.internal.vm.ci,
178 jdk.jfr,
179 jdk.jshell,
180 jdk.nio.mapmode,
181 jdk.unsupported;
182 exports jdk.internal.module to
183 java.instrument,
184 java.management.rmi,
185 jdk.incubator.foreign,
186 jdk.jartool,
187 jdk.jfr,
188 jdk.jlink,
189 jdk.jpackage;
190 exports jdk.internal.org.objectweb.asm to
191 jdk.jartool,
192 jdk.jfr,
193 jdk.jlink;
194 exports jdk.internal.org.objectweb.asm.commons to jdk.jfr;
195 exports jdk.internal.org.objectweb.asm.tree to
196 jdk.jfr,
197 jdk.jlink;
198 exports jdk.internal.org.objectweb.asm.util to jdk.jfr;
199 exports jdk.internal.org.xml.sax to jdk.jfr;
200 exports jdk.internal.org.xml.sax.helpers to jdk.jfr;
201 exports jdk.internal.perf to
202 java.management,
203 jdk.internal.jvmstat,
204 jdk.management.agent;
205 exports jdk.internal.platform to
206 jdk.jfr,
207 jdk.management;
208 exports jdk.internal.ref to
209 java.desktop,
210 jdk.incubator.foreign;
211 exports jdk.internal.reflect to
212 java.logging,
213 java.sql,
214 java.sql.rowset,
215 jdk.dynalink,
216 jdk.incubator.foreign,
217 jdk.internal.vm.ci,
218 jdk.unsupported;
219 exports jdk.internal.util to jdk.incubator.foreign;
220 exports jdk.internal.util.jar to jdk.jartool;
221 exports jdk.internal.util.random to jdk.random;
222 exports jdk.internal.util.xml to jdk.jfr;
223 exports jdk.internal.util.xml.impl to jdk.jfr;
224 exports jdk.internal.vm to
225 jdk.internal.jvmstat,
226 jdk.management.agent;
227 exports jdk.internal.vm.annotation to
228 java.instrument,
229 jdk.incubator.foreign,
230 jdk.incubator.vector,
231 jdk.internal.vm.ci,
232 jdk.jfr,
233 jdk.unsupported;
234 exports jdk.internal.vm.vector to jdk.incubator.vector;
235 exports sun.invoke.util to
236 jdk.compiler,
237 jdk.incubator.foreign;
238 exports sun.net to
239 java.net.http,
240 jdk.naming.dns;
241 exports sun.net.dns to
242 java.security.jgss,
243 jdk.naming.dns;
244 exports sun.net.ext to jdk.net;
245 exports sun.net.util to
246 java.desktop,
247 java.net.http,
248 jdk.jconsole,
249 jdk.sctp;
250 exports sun.net.www to
251 java.net.http,
252 jdk.jartool;
253 exports sun.net.www.protocol.http to java.security.jgss;
254 exports sun.nio.ch to
255 java.management,
256 jdk.crypto.cryptoki,
257 jdk.incubator.foreign,
258 jdk.net,
259 jdk.sctp;
260 exports sun.nio.cs to jdk.charsets;
261 exports sun.nio.fs to
262 jdk.net,
263 jdk.zipfs;
264 exports sun.reflect.annotation to jdk.compiler;
265 exports sun.reflect.generics.reflectiveObjects to java.desktop;
266 exports sun.reflect.misc to
267 java.datatransfer,
268 java.desktop,
269 java.management,
270 java.management.rmi,
271 java.rmi,
272 java.sql.rowset;
273 exports sun.security.action to
274 java.desktop,
275 java.security.jgss,
276 jdk.crypto.ec,
277 jdk.incubator.foreign;
278 exports sun.security.internal.interfaces to jdk.crypto.cryptoki;
279 exports sun.security.internal.spec to
280 jdk.crypto.cryptoki,
281 jdk.crypto.mscapi;
282 exports sun.security.jca to
283 java.smartcardio,
284 jdk.crypto.cryptoki,
285 jdk.crypto.ec,
286 jdk.naming.dns;
287 exports sun.security.pkcs to
288 jdk.crypto.ec,
289 jdk.jartool;
290 exports sun.security.provider to
291 java.rmi,
292 java.security.jgss,
293 jdk.crypto.cryptoki,
294 jdk.crypto.ec,
295 jdk.security.auth;
296 exports sun.security.provider.certpath to
297 java.naming,
298 jdk.jartool;
299 exports sun.security.rsa to
300 jdk.crypto.cryptoki,
301 jdk.crypto.mscapi;
302 exports sun.security.timestamp to jdk.jartool;
303 exports sun.security.tools to jdk.jartool;
304 exports sun.security.util to
305 java.desktop,
306 java.naming,
307 java.rmi,
308 java.security.jgss,
309 java.security.sasl,
310 java.smartcardio,
311 java.xml.crypto,
312 jdk.crypto.cryptoki,
313 jdk.crypto.ec,
314 jdk.crypto.mscapi,
315 jdk.jartool,
316 jdk.security.auth,
317 jdk.security.jgss;
318 exports sun.security.util.math to jdk.crypto.ec;
319 exports sun.security.util.math.intpoly to jdk.crypto.ec;
320 exports sun.security.validator to jdk.jartool;
321 exports sun.security.x509 to
322 jdk.crypto.cryptoki,
323 jdk.crypto.ec,
324 jdk.jartool;
325 exports sun.util.cldr to jdk.jlink;
326 exports sun.util.locale.provider to
327 java.desktop,
328 jdk.jlink,
329 jdk.localedata;
330 exports sun.util.logging to
331 java.desktop,
332 java.logging,
333 java.prefs;
334 exports sun.util.resources to jdk.localedata;
335
336 uses java.lang.System.LoggerFinder;
337 uses java.net.ContentHandlerFactory;
338 uses java.net.spi.URLStreamHandlerProvider;
339 uses java.nio.channels.spi.AsynchronousChannelProvider;
340 uses java.nio.channels.spi.SelectorProvider;
341 uses java.nio.charset.spi.CharsetProvider;
342 uses java.nio.file.spi.FileSystemProvider;
343 uses java.nio.file.spi.FileTypeDetector;
344 uses java.security.Provider;
345 uses java.text.spi.BreakIteratorProvider;
346 uses java.text.spi.CollatorProvider;
347 uses java.text.spi.DateFormatProvider;
348 uses java.text.spi.DateFormatSymbolsProvider;
349 uses java.text.spi.DecimalFormatSymbolsProvider;
350 uses java.text.spi.NumberFormatProvider;
351 uses java.time.chrono.AbstractChronology;
352 uses java.time.chrono.Chronology;
353 uses java.time.zone.ZoneRulesProvider;
354 uses java.util.random.RandomGenerator;
355 uses java.util.spi.CalendarDataProvider;
356 uses java.util.spi.CalendarNameProvider;
357 uses java.util.spi.CurrencyNameProvider;
358 uses java.util.spi.LocaleNameProvider;
359 uses java.util.spi.ResourceBundleControlProvider;
360 uses java.util.spi.ResourceBundleProvider;
361 uses java.util.spi.TimeZoneNameProvider;
362 uses java.util.spi.ToolProvider;
363 uses javax.security.auth.spi.LoginModule;
364 uses jdk.internal.logger.DefaultLoggerFinder;
365 uses sun.text.spi.JavaTimeDateTimePatternProvider;
366 uses sun.util.locale.provider.LocaleDataMetaInfo;
367 uses sun.util.resources.LocaleData.CommonResourceBundleProvider;
368 uses sun.util.resources.LocaleData.SupplementaryResourceBundleProvider;
369 uses sun.util.spi.CalendarProvider;
370 provides java.nio.file.spi.FileSystemProvider with jdk.internal.jrtfs.JrtFileSystemProvider;
371 provides java.util.random.RandomGenerator with
372 java.security.SecureRandom,
373 java.util.Random,
374 java.util.SplittableRandom;
375}
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
1package com.soulboy.jvm;
2
3import java.io.*;
4
5public class MyClassLoader extends ClassLoader {
6
7 private String path;
8
9 public MyClassLoader(String path) {
10 this.path = path;
11 }
12
13 @Override
14 protected Class<?> findClass(String name) throws ClassNotFoundException {
15 String fileName = path + name + ".class";
16 System.out.println(fileName);
17
18 try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
19 ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
20 int len;
21 byte[] data = new byte[1024];
22 //不等于-1就说明还有数据
23 while ((len = bis.read(data)) != -1) {
24 bos.write(data, 0, len);
25 }
26 //拿到字节数组(文件在内存中)
27 byte[] byteArray = bos.toByteArray();
28 //返回class对象
29 Class<?> defineClass = defineClass(null, byteArray, 0, byteArray.length);
30 return defineClass;
31
32 } catch (IOException e) {
33 e.printStackTrace();
34 }
35
36 return super.findClass(name);
37 }
38}
测试
ClassLoaderTest
1package com.soulboy.jvm;
2
3public class ClassLoaderTest {
4 public static void main(String[] args) throws Exception {
5 // 拓展加载源:从网络,数据库等地方加载类
6 MyClassLoader myClassLoader = new MyClassLoader("D:\\Project\\redlock\\src\\main\\java\\com\\soulboy\\jvm\\");
7 // 加载指定类 D:\Project\redlock\src\main\java\com\soulboy\jvm>javac HeapDemo.java
8 Class<?> clazz = myClassLoader.loadClass("HeapDemo");
9 // 拿到class类可以实例化对象
10 Object obj = clazz.getDeclaredConstructor().newInstance();
11 System.out.println(obj.getClass().getName() + "----------" + obj.getClass().getClassLoader().getClass().getName() + "类加载器加载" );
12 }
13}
控制台输出
1D:\Project\redlock\src\main\java\com\soulboy\jvm\HeapDemo.class
2com.soulboy.jvm.HeapDemo----------com.soulboy.jvm.MyClassLoader类加载器加载
两个不同的类加载器加载同一个 class 类,JVM 是否认为它们相同?
不同类加载器会加载同名的 class 类,这些类在JVM 中是不同的,即它们的 Class 对象是不同的
定义类相同的条件
- 类加载器相同
- class 文件相同
编码验证
1public static void test2() throws Exception{
2 // 自定义类加载器
3 // 拓展加载源:从网络,数据库等地方加载类
4 MyClassLoader myClassLoader = new MyClassLoader("D:\\Project\\redlock\\src\\main\\java\\com\\soulboy\\jvm\\");
5 // 加载指定类 D:\Project\redlock\src\main\java\com\soulboy\jvm>javac HeapDemo.java
6 Class<?> clazz = myClassLoader.loadClass("HeapDemo");
7 // 拿到class类可以实例化对象
8 Object obj = clazz.getDeclaredConstructor().newInstance();
9
10 // class com.soulboy.jvm.HeapDemo
11 System.out.println(obj.getClass());
12
13 // false obj属于自定义类加载器 HeapDemo属于JVM的类加载器
14 System.out.println(obj instanceof HeapDemo);
15
16 // JVM本身的类加载器 VS 自定义类加载器
17 // com.soulboy.jvm.HeapDemo----------com.soulboy.jvm.MyClassLoader类加载器加载
18 System.out.println(obj.getClass().getName() + "----------" + obj.getClass().getClassLoader().getClass().getName() + "类加载器加载" );
19 //com.soulboy.jvm.HeapDemo----------jdk.internal.loader.ClassLoaders$AppClassLoader类加载器加载
20 System.out.println(HeapDemo.class.getName() + "----------" + HeapDemo.class.getClassLoader().getClass().getName() + "类加载器加载" );
21
22 // 应用程序类加载器(Application ClassLoader)
23 // app
24 System.out.println(HeapDemo.class.getClassLoader().getName());
25
26 // 平台类加载器(Platform ClassLoader)
27 // platform
28 System.out.println(HeapDemo.class.getClassLoader().getParent().getName());
29
30 // 启动类加载器(Bootstrap ClassLoader)
31 // null (C++编写) .getName() 会报空指针异常
32 System.out.println(HeapDemo.class.getClassLoader().getParent().getParent());
33 }
总结
- 在 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 没有采用这个引用计数器方案,而是采用可达性分析算法
1pubiic class Main{
2 public static void main(string[]args) {
3 A a = new A();
4 B b = new B();
5 a.setB(b);
6 b.setA(a);
7 a = null;
8 b = null;
9 System.gc();
10 }
11}
12
13class A {_
14 private B b;
15
16 public void setB(B b){
17 this.b = b;
18 }
19}
20
21
22class B {
23 private A a;
24
25 public void setA(A a){
26 this.a = a;
27 }
28}
可达性分析法(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 示例代码
1public class GCTest {
2 public static void main(String[] args) throws Exception {
3
4 // product 是栈帧中的本地变量,指向了title=springboot课程 这个 Product对象
5 Product product = new Product("springboot课程");
6
7 // 当product=null;由于此时当product充当了 Gc Root 的作用,当product与原来指向 当product对象断开了连接
8 // 所以这个 new Product("springboot课程") 对象会被回收
9 product = null;
10
11 }
12}
对象可回收,就一定会被回收吗?
不一定会回收,对象的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
1-XX:+UseParallelGC
JDK11
1-XX:+UseG1Gc
JDK17
1-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
1### 添加虚拟机参数
2-XX:+UseSerialGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
3
4### 控制台打印输出
5-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 开始已经不再被推荐使用)
1### 添加虚拟机参数
2-XX:+UseParNewGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
3
4### 控制台打印输出
5-XX:InitialHeapsize=33554432 -XX:MaxHeapSize-33554432 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParNewGC
6
7Java 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 环境)
1### 添加虚拟机参数
2-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
3
4### 控制台打印输出
5-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 验证成功)
1### 添加虚拟机参数
2-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
3
4### 控制台打印输出
5-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
6
7### 警告信息
8Option 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
1-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
制定计划1### 成本排序案例 2现在有Region1、Region2和Region3三个区域 31、Region1预计可以回收1.5MB内存,预计耗时2MS,投产比 RO1=1.5/2=0.75 42、Region2预计可以回收1MB内存,预计耗时1MS,投产比 ROI=1/1=1 53、Region3预计可以回收0.5MB内存,预计耗时1MS,投产比 ROI=0.5/1=0.5 6 7那Region1、Region2和Region3各自的回收价值与成本比值分别是:0.75、1、0.5 8 9比值越高说明同样的付出收益越高,如果此时只能回收一个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
:查看命令行相关参数
1### 参数使用
2
3# JVM参数
4-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags
5
6# 控制台输出
7-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 环境)
1### 虚拟机输入参数
2-XX:+UseZGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
3
4### 控制台输出
5-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]
睡眠(一直运行)
1package com.soulboy.jvm;
2
3public class JvmTest {
4 public static void main(String[] args) throws InterruptedException {
5 System.out.println("程序运行!");
6 Thread.sleep(100000);
7 }
8}
1# JVM参数设置
2-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintCommandLineFlags -Xms524m -Xmx524m
3
4# 控制台输出
5-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
6程序运行!
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 诊断线程死锁
1D:\Project\redlock>jstack -l 15072
22024-08-13 09:49:38
3Full thread dump Java HotSpot(TM) 64-Bit Server VM (17.0.8+9-LTS-211 mixed mode, sharing):
4
5Threads class SMR info:
6_java_thread_list=0x0000027b74547a30, length=13, elements={
70x0000027b54a2d6d0, 0x0000027b6f838b80, 0x0000027b6f83a150, 0x0000027b6f8526b0,
80x0000027b6f855070, 0x0000027b6f857a30, 0x0000027b6f8e9f50, 0x0000027b6f8583e0,
90x0000027b6f85c1e0, 0x0000027b6f8eb370, 0x0000027b6fb1d3d0, 0x0000027b6fb1cf00,
100x0000027b6fb1d8a0
11}
12
13"main" #1 prio=5 os_prio=0 cpu=62.50ms elapsed=24.68s tid=0x0000027b54a2d6d0 nid=0x4334 waiting on condition [0x0000007e278ff000]
14 java.lang.Thread.State: TIMED_WAITING (sleeping)
15 at java.lang.Thread.sleep(java.base@17.0.8/Native Method)
16 at com.soulboy.jvm.JvmTest.main(JvmTest.java:6)
17
18 Locked ownable synchronizers:
19 - None
20
21"Reference Handler" #2 daemon prio=10 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f838b80 nid=0x2b1c waiting on condition [0x0000007e27fff000]
22 java.lang.Thread.State: RUNNABLE
23 at java.lang.ref.Reference.waitForReferencePendingList(java.base@17.0.8/Native Method)
24 at java.lang.ref.Reference.processPendingReferences(java.base@17.0.8/Reference.java:253)
25 at java.lang.ref.Reference$ReferenceHandler.run(java.base@17.0.8/Reference.java:215)
26
27 Locked ownable synchronizers:
28 - None
29
30"Finalizer" #3 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f83a150 nid=0x948 in Object.wait() [0x0000007e280ff000]
31 java.lang.Thread.State: WAITING (on object monitor)
32 at java.lang.Object.wait(java.base@17.0.8/Native Method)
33 - waiting on <0x00000000fff0d5d0> (a java.lang.ref.ReferenceQueue$Lock)
34 at java.lang.ref.ReferenceQueue.remove(java.base@17.0.8/ReferenceQueue.java:155)
35 - locked <0x00000000fff0d5d0> (a java.lang.ref.ReferenceQueue$Lock)
36 at java.lang.ref.ReferenceQueue.remove(java.base@17.0.8/ReferenceQueue.java:176)
37 at java.lang.ref.Finalizer$FinalizerThread.run(java.base@17.0.8/Finalizer.java:172)
38
39 Locked ownable synchronizers:
40 - None
41
42"Signal Dispatcher" #4 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8526b0 nid=0x15e4 waiting on condition [0x0000000000000000]
43 java.lang.Thread.State: RUNNABLE
44
45 Locked ownable synchronizers:
46 - None
47
48"Attach Listener" #5 daemon prio=5 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f855070 nid=0x3048 waiting on condition [0x0000000000000000]
49 java.lang.Thread.State: RUNNABLE
50
51 Locked ownable synchronizers:
52 - None
53
54"Service Thread" #6 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f857a30 nid=0x38f8 runnable [0x0000000000000000]
55 java.lang.Thread.State: RUNNABLE
56
57 Locked ownable synchronizers:
58 - None
59
60"Monitor Deflation Thread" #7 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8e9f50 nid=0x3654 runnable [0x0000000000000000]
61 java.lang.Thread.State: RUNNABLE
62
63 Locked ownable synchronizers:
64 - None
65
66"C2 CompilerThread0" #8 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8583e0 nid=0x2a0c waiting on condition [0x0000000000000000]
67 java.lang.Thread.State: RUNNABLE
68 No compile task
69
70 Locked ownable synchronizers:
71 - None
72
73"C1 CompilerThread0" #11 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f85c1e0 nid=0x29b4 waiting on condition [0x0000000000000000]
74 java.lang.Thread.State: RUNNABLE
75 No compile task
76
77 Locked ownable synchronizers:
78 - None
79
80"Sweeper thread" #12 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8eb370 nid=0x2c64 runnable [0x0000000000000000]
81 java.lang.Thread.State: RUNNABLE
82
83 Locked ownable synchronizers:
84 - None
85
86"Common-Cleaner" #13 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=24.61s tid=0x0000027b6fb1d3d0 nid=0x1930 in Object.wait() [0x0000007e288ff000]
87 java.lang.Thread.State: TIMED_WAITING (on object monitor)
88 at java.lang.Object.wait(java.base@17.0.8/Native Method)
89 - waiting on <0x00000000ffe59058> (a java.lang.ref.ReferenceQueue$Lock)
90 at java.lang.ref.ReferenceQueue.remove(java.base@17.0.8/ReferenceQueue.java:155)
91 - locked <0x00000000ffe59058> (a java.lang.ref.ReferenceQueue$Lock)
92 at jdk.internal.ref.CleanerImpl.run(java.base@17.0.8/CleanerImpl.java:140)
93 at java.lang.Thread.run(java.base@17.0.8/Thread.java:833)
94 at jdk.internal.misc.InnocuousThread.run(java.base@17.0.8/InnocuousThread.java:162)
95
96 Locked ownable synchronizers:
97 - None
98
99"Monitor Ctrl-Break" #14 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=24.50s tid=0x0000027b6fb1cf00 nid=0x1acc runnable [0x0000007e28afe000]
100 java.lang.Thread.State: RUNNABLE
101 at sun.nio.ch.SocketDispatcher.read0(java.base@17.0.8/Native Method)
102 at sun.nio.ch.SocketDispatcher.read(java.base@17.0.8/SocketDispatcher.java:46)
103 at sun.nio.ch.NioSocketImpl.tryRead(java.base@17.0.8/NioSocketImpl.java:261)
104 at sun.nio.ch.NioSocketImpl.implRead(java.base@17.0.8/NioSocketImpl.java:312)
105 at sun.nio.ch.NioSocketImpl.read(java.base@17.0.8/NioSocketImpl.java:350)
106 at sun.nio.ch.NioSocketImpl$1.read(java.base@17.0.8/NioSocketImpl.java:803)
107 at java.net.Socket$SocketInputStream.read(java.base@17.0.8/Socket.java:966)
108 at sun.nio.cs.StreamDecoder.readBytes(java.base@17.0.8/StreamDecoder.java:270)
109 at sun.nio.cs.StreamDecoder.implRead(java.base@17.0.8/StreamDecoder.java:313)
110 at sun.nio.cs.StreamDecoder.read(java.base@17.0.8/StreamDecoder.java:188)
111 - locked <0x00000000ff599a60> (a java.io.InputStreamReader)
112 at java.io.InputStreamReader.read(java.base@17.0.8/InputStreamReader.java:177)
113 at java.io.BufferedReader.fill(java.base@17.0.8/BufferedReader.java:162)
114 at java.io.BufferedReader.readLine(java.base@17.0.8/BufferedReader.java:329)
115 - locked <0x00000000ff599a60> (a java.io.InputStreamReader)
116 at java.io.BufferedReader.readLine(java.base@17.0.8/BufferedReader.java:396)
117 at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:53)
118
119 Locked ownable synchronizers:
120 - <0x00000000ff590360> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
121
122"Notification Thread" #15 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=24.50s tid=0x0000027b6fb1d8a0 nid=0x1144 runnable [0x0000000000000000]
123 java.lang.Thread.State: RUNNABLE
124
125 Locked ownable synchronizers:
126 - None
127
128"VM Thread" os_prio=2 cpu=0.00ms elapsed=24.66s tid=0x0000027b6f8334d0 nid=0x2edc runnable
129
130"GC Thread#0" os_prio=2 cpu=0.00ms elapsed=24.68s tid=0x0000027b54a5cc40 nid=0x37e8 runnable
131
132"G1 Main Marker" os_prio=2 cpu=0.00ms elapsed=24.68s tid=0x0000027b54a63520 nid=0x38e4 runnable
133
134"G1 Conc#0" os_prio=2 cpu=0.00ms elapsed=24.68s tid=0x0000027b54a63f30 nid=0x2c74 runnable
135
136"G1 Refine#0" os_prio=2 cpu=0.00ms elapsed=24.67s tid=0x0000027b6f778a30 nid=0x42e0 runnable
137
138"G1 Service" os_prio=2 cpu=0.00ms elapsed=24.67s tid=0x0000027b6f779450 nid=0x21d8 runnable
139
140"VM Periodic Task Thread" os_prio=2 cpu=0.00ms elapsed=24.50s tid=0x0000027b744ddb90 nid=0x31b8 waiting on condition
141
142JNI global refs: 15, weak refs: 0
jstack 生产案例:分析 CPU 占用过高的 Java 线程案例
生产环境 JVM 中,会出现由于代码问题导致CPU占用过高
,需要诊断出来具体是哪个 Java 代码导致
CPU 占用过高代码:CpuTest
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3
4public class CpuTest {
5 private static ExecutorService executorService = Executors.newFixedThreadPool(5);
6
7 public static Object lock = new Object();
8
9 public static void main(String[] args) {
10 Task task1 = new Task();
11 Task task2 = new Task();
12 executorService.execute(task1);
13 executorService.execute(task2);
14 }
15
16 static class Task implements Runnable {
17 @Override
18 public void run() {
19 synchronized (lock) {
20 long sum = 0L;
21 while (true) {
22 sum += 1;
23 }
24 }
25 }
26 }
27
28}
运行诊断
1# 后台运行 nohup java CpuTest.java &
2[root@localhost tmp]# java CpuTest.java
诊断出具体哪个类哪行代码
-
从宏观到细节
宏观:CPU、内存、网络/O、磁盘I/O
细节:哪个进程导致的
-
确认问题:是否是 CPU 过高导致的应用程序性能问题
-
确认进程 ID,使用 top 命令查找该【进程】下 CPU 使用最高的【线程】,并记录线程 ID:
top -Hp 进程id
1### 发现线程9479占据了大量CPU资源 2[root@localhost ~]# top -Hp 9457
-
把十进制的线程 id 转为 16 进制:
printf "%x\n" 线程id
1[root@localhost ~]# printf "%x\n" 9479 22507
-
分析线程堆栈信息:使用 jstack 命令查看 Java 应用程序中所有线程的堆栈信息。定位问题线程堆栈信息,一般会生成快照到文本文件里面进行分析:
jstack -l [pid] > /tmp/log.txt
1[root@localhost ~]# jstack -l 9457 > /tmp/log.txt 2[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
1[root@localhost tmp]# jhsdb jmap --pid 9479 --heap
2Attaching to process ID 9479, please wait...
3Debugger attached successfully.
4Server compiler detected.
5JVM version is 17.0.11+7-LTS-207
6
7using thread-local object allocation.
8Garbage-First (G1) GC with 2 thread(s)
9
10Heap Configuration:
11 MinHeapFreeRatio = 40 #最小堆空闲比例
12 MaxHeapFreeRatio = 70 #最大堆空闲比例
13 MaxHeapSize = 1035993088 (988.0MB) #最大堆大小
14 NewSize = 1363144 (1.2999954223632812MB) #新生代大小
15 MaxNewSize = 620756992 (592.0MB) #最大新生代大小
16 OldSize = 5452592 (5.1999969482421875MB) #老年代大小
17 NewRatio = 2 #新生代和老年代的比例
18 SurvivorRatio = 8 #新生代中eden区和survivor区的比例
19 MetaspaceSize = 22020096 (21.0MB) #元空间大小
20 CompressedClassSpaceSize = 1073741824 (1024.0MB) #压缩类空间大小
21 MaxMetaspaceSize = 17592186044415 MB #最大元空间大小
22 G1HeapRegionSize = 1048576 (1.0MB) #G1垃圾收集器每个Region大小
23
24Heap Usage:
25G1 Heap:
26 regions = 988 #堆中区域数量
27 capacity = 1035993088 (988.0MB) #堆的总容量
28 used = 13600416 (12.970367431640625MB) #堆已使用的容量
29 free = 1022392672 (975.0296325683594MB) #堆未使用的容量
30 1.3127902258745572% used #堆的使用率
31G1 Young Generation: #G1垃圾收集器中的年轻代
32Eden Space: #年轻代中的Eden区域
33 regions = 9
34 capacity = 12582912 (12.0MB)
35 used = 9437184 (9.0MB)
36 free = 3145728 (3.0MB)
37 75.0% used
38Survivor Space: #年轻代中的survivor区域
39 regions = 0
40 capacity = 1048576 (1.0MB)
41 used = 906912 (0.864898681640625MB)
42 free = 141664 (0.135101318359375MB)
43 86.4898681640625% used
44G1 Old Generation: #G1垃圾收集器中的老年代
45 regions = 5
46 capacity = 53477376 (51.0MB)
47 used = 3256320 (3.10546875MB)
48 free = 50221056 (47.89453125MB)
49 6.089154411764706% used #老年代的使用率
- 将 Java 堆中存活的对象信息转储到/tmp/dump.bin 文件:
jmap -dump:live,format=b,file=/tmp/dump.bin 进程pid
1### 生成进程的dump.bin文件 2[root@localhost tmp]# jmap -dump:live,format=b,file=/tmp/dump.bin 9457 3 4### jhat 用于分析jmap生成的heap dump堆转储快照,内置HTTP服务器,对生成的dump文件分析后,在浏览器中查看分析结果 5# 注意:jhat命令在IDK9、IDK10中已经被删除,官方建议用VisualVM代替,简单了解即可 6# 有很多可视化工具可以帮助查看和分析,例如:JConsole 7[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 应用程序的配置等 |
测试代码
1import java.util.ArrayList;
2import java.util.List;
3
4public class JConsoleTest {
5 public static void main(String[] args) throws InterruptedException {
6 List<Object> list = new ArrayList<Object>();
7 for (int i = 0; i < 5000; i++) {
8 Thread.sleep(200);
9 list.add(new byte[1024 * 1024]); // 1MB
10 }
11 }
12}
运行
1# idea运行程序
2
3# 找到进程ID
4D:\Project\redlock>jps
512736 JConsoleTest
616832 RemoteMavenServer36
712244 Launcher
816004 Jps
99464
10
11# 启动JConsole
12D:\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 日志 |
配置案例实战
1### JDK11
2-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xloggc:gc.log
3
4### JDK17
5-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
1-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc=debug,gc+metaspace:gc.log
-
JVM 提供了通配符 * 来解决精确匹配的问题,比如想要所有 tag 为 gc 的 debug 级别日志 -x1og:gc*=debug
1### gc*=debug:指定输出GC相关日志,级别为debug,* 表示所有包含GC标签都会输出日志,debug是最高的日志级别,通配符,日志文件比较详细 2-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc*=debug:gc.log
日志内容解析
1-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc*=info:gc.log
1### 日志内容1
2[24.824s][info][gc] GC(8) Pause Young (Concurrent Start) (G1 Humongous Allocation) 251M->251M(524M) 0.437ms
3
4这条GC日志记录了程序运行了24.824秒时发生的8次Young GC,回收了0M的内存空间,耗时0.437毫秒
5
6# 字段拆解
7[24.824s]:GC发生的时间戳,表示程序运行的时间
8[info]:日志级别,表示这是一条信息级别的日志
9[gc]:日志标签,表示这是一条Gc相关的日志
10GC(8):GC的编号,表示这是第8次GC
11
12# Pause Young (Concurrent Start) (G1 Humongous Allocation)
13GC的类型,表示这是一次YoungGC,同时也是一次Humongous Allocation的GC,其中concurrent start表示并发启动的GC
14
15# 251M->251M(524M)
16GC前后堆内存的使用情况,其中251M表示GC前的已使用内存,251M表示GC后的已使用内存,524M表示堆内存的总大小
17
18# 0.437ms
19GC的耗时,表示这次GC的执行时间
20
21
22### 日志内容2
23[24.824s][info][gc] GC(9) Concurrent Mark Cycle
24[24.825s][info][gc] GC(9) Pause Remark 253M->253M(524M) 0.197ms
25[24.825s][info][gc] GC(9) Pause Cleanup 253M->253M(524M) 0.035ms
26[24.826s][info][gc] GC(9) Concurrent Mark Cycle 1.947ms
27
28这段GC日志记录了程序运行了24.824秒一次Mixed GC,mixed Gc回收了0M的内存空间,耗时1.947毫秒
29
30# [24.824s][info][gc] GC(9) Concurrent Mark Cycle
31Mixed Gc的相关信息,表示这是一次Med Gc的开始
32
33# [24.825s][info][gc] GC(9) Pause Remark 253M->253M(524M) 0.197ms
34Mixed GC的相关信息,表示这是一次Remark阶段的GC,回收了0M的内存空间,耗时0.197毫秒。
35
36# [24.825s][info][gc] GC(9) Pause Cleanup 253M->253M(524M) 0.035ms
37Mixed GC的相关信息,表示这是一次cleanup阶段的GC,回收了0M的内存空间,耗时0.035毫秒。
38
39# [24.826s][info][gc] GC(9) Concurrent Mark Cycle 1.947ms
40Mixed GC的相关信息,表示这是一次Mixed GC的结束,耗时1.947毫秒。
GC 日志输出到文件中配置
option | 描述 |
---|---|
:file=[/path/file.log] | GC 日志输出到文件中 |
filesize=104857600 | 指定单个日志文件大小为 100MB,超过这个大小会自动切换到新的日志文件 |
filecount=n | 指定日志文件数量不超过 n 个,超过这个数量会删除最早的日志文件 |
1### 参数配置
2-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags -Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=5,filesize=1M
3
4该配置使用G1垃圾回收器,设置最大垃圾回收暂停时间为100毫秒,JVM堆的初始和最大大小均为524MB,并打印JVM启动参数和输出GC日志到文件portal_gc.log中,文件数量为5个一每个文件大小为1MB-日志格式为info级别 句令时间戳、级别和标签
5
6-XX:+UseG1GC 使用G1垃圾回收器
7-XX:MaxGCPauseMillis=100 设置最大垃圾回收暂停时间为100毫秒
8-Xms524m 设置JVM堆的初始大小为524MB
9-Xmx524m 设置JVM堆的最大大小为524MB
10-XX:+PrintCommandLineFlags 打印JVM启动参数
11-Xlog:gc*=info:file=portal_gc.log:utctime:,level,tags:filecount=5,filesize=1M
12
13Xlog 指定日志输出方式为日志文件
14gc* 指定日志输出类型为Gc相关的日志
15info 指定输出日志的级别为info级别
16file=portal_gc.log 指定日志输出的文件名为portal_gc.log
17utctime 指定日志输出的时间戳使用UTC时间
18level,tags 指定日志输出的格式包含级别和标签信息
19filecount=5 指定最多保存5个日志文件
20filesize=1M 指定每个日志文件的大小为1MB
JVM 内存溢出 OOM 堆栈快照配置
背景
- 服务器配置是 8 核 16g 内存,需要部署一个 SpringBoot 写的电商项目,日访问量 100 万左右的 UV
- 给一份生产环境配置的 Jvm 参数的值,要求基于 jdk11+ 配置 oom 时的堆栈快照信息
注意:使用 G1 收集器的时候,不用指定-Xmn
G1垃圾收集器不需要显式地指定-Xmn参数,通过自适应的方式来优化内存的使用和垃圾收集的效率
在G1中,堆内存被划分为多个区域,每个区域都可以作为年轻代或老年代的一部分
G1的年轻代采用了不同于传统的基于分代的HotSpot垃圾收集器的方式,因此不需要指定-Xmn参数来设置年轻代的大小
G1利用自适应的内存分配策略来动态地调整年轻代的大小
根据堆的使用情况来确定哪些区域应该作为年轻代,以及年轻代的大小
1# 参考配置
2-server
3-XX:+UseG1GC
4-Xms524m
5-Xmx524m
6-XX:MaxGCPauseMillis=200
7-XX:G1HeapRegionSize=32M
8-XX:ActiveProcessorCount=8
9-XX:+HeapDumpOnOutOfMemoryError
10-XX:HeapDumpPath=/tmp/heapdump.hprof
11-XX:+PrintCommandLineFlags
12
13
14# 参数说明
15-server 指定JVM使用服务器模式运行,优化性能
16-XX:+UseG1GC 指定使用G1垃圾收集器
17-Xms524m 指定JVM堆内存最小值为8G
18-Xmx524m 指定JVM堆内存最大值为8G
19-XX:MaxGCPauseMillis=200 定最大垃圾回收暂停时间为200毫秒
20-XX:G1HeapRegionSize=32M 指定G1垃圾收集器的堆区域大小为32MB
21-XX:ActiveProcessorCount=8 指定并行垃圾回收器的线程数为8,在JDK9及之后的版本中,Paral1elGcrhreads参数已被替代为-XX:ActiveprocessorCount参数,用于自动计算并行垃圾回收线程数
22-XX:+HeapDumpOnOutOfMemoryError 指定在发生内存溢出时生成堆转储文件
23-XX:HeapDumpPath=/tmp/heapdump.hprof 指定堆转储文件的路径
24-XX:+PrintCommandLineFlags 打印JVM参数
25-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 时,执行指定的命令 |
1# 将在发生OOM时生成一个名为heapdump.hprof的堆栈快照文件,并将其保存到/var/log目录下
2-XX:HeapDumpOnOutofMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
3
4# linux
5-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
6
7# windows
8-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\\log\\heapdump.hprof
9
10# 融合其他配置在一起
11-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
1package com.soulboy.controller;
2
3public class Product {
4 private int price;
5
6 private String title;
7
8 public Product() {
9 }
10
11 public Product(int price, String title) {
12 this.price = price;
13 this.title = title;
14 }
15
16 public int getPrice() {
17 return price;
18 }
19
20 public void setPrice(int price) {
21 this.price = price;
22 }
23
24 public String getTitle() {
25 return title;
26 }
27
28 public void setTitle(String title) {
29 this.title = title;
30 }
31
32 @Override
33 public String toString() {
34 return "Product{" +
35 "price=" + price +
36 ", title='" + title + '\'' +
37 '}';
38 }
39}
Product2JVMController
1package com.soulboy.controller;
2
3import org.springframework.web.bind.annotation.RequestMapping;
4import org.springframework.web.bind.annotation.RestController;
5
6import java.util.ArrayList;
7import java.util.HashMap;
8import java.util.Map;
9
10@RestController
11@RequestMapping("/api/product2jvm")
12public class Product2JVMController {
13
14 /**
15 * 随机产生大小不同的对象
16 * @return
17 * @throws InterruptedException
18 */
19 @RequestMapping("/query")
20 public Map<String,Object> query() throws InterruptedException {
21 //随机数
22 int num = (int) (Math.random() * 100) + 1;
23
24 //5MB大小字节数组
25 Byte[] bytes = new Byte[5 * 1024 * 1024];
26
27 ArrayList<Product> productList = new ArrayList<>();
28 for (int i = 0; i < num; i++) {
29 Product product = new Product();
30 product.setPrice((int) Math.random() * 100);
31 product.setTitle("小象NO." + i);
32 productList.add(product);
33 }
34
35 Thread.sleep(5);
36 HashMap<String, Object> map = new HashMap<>(16);
37 map.put("data", productList);
38 return map;
39 }
40
41}
Jmeter 压测工具准备,测试计划 200 并发,循环 500 次
堆大小配置,FullGC 次数的性能影响
性能优化初始值(堆大小 1G)
1# 性能优化初始值 堆大小1G
2-Xms1g
3-Xmx1g
4-XX:+UseG1GC
5-XX:MaxGCPauseMillis=200
6-XX:G1HeapRegionSize=32M
7-XX:ActiveProcessorCount=8
8-XX:+HeapDumpOnOutOfMemoryError
9-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
10-XX:+PrintCommandLineFlags
11-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
12
13-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
1# 性能优化调整后堆大小8G
2-Xms8g
3-Xmx8g
4-XX:+UseG1GC
5-XX:MaxGCPauseMillis=200
6-XX:G1HeapRegionSize=32M
7-XX:ActiveProcessorCount=8
8-XX:+HeapDumpOnOutOfMemoryError
9-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
10-XX:+PrintCommandLineFlags
11-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
12
13-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,其他参数保持不变
1# 性能优化初始值 堆大小1G,垃圾收集器使用ParallelGC,其他参数保持不变
2-Xms1g
3-Xmx1g
4-XX:+UseParallelGC
5-XX:MaxGCPauseMillis=200
6-XX:G1HeapRegionSize=32M
7-XX:ActiveProcessorCount=8
8-XX:+HeapDumpOnOutOfMemoryError
9-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
10-XX:+PrintCommandLineFlags
11-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
12
13-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
1# 性能优化初始值 堆大小1G,垃圾收集器使用ZGC,其他参数保持不变
2-Xms1g
3-Xmx1g
4-XX:+UseZGC
5-XX:MaxGCPauseMillis=200
6-XX:G1HeapRegionSize=32M
7-XX:ActiveProcessorCount=8
8-XX:+HeapDumpOnOutOfMemoryError
9-XX:HeapDumpPath=E:\\log\\heapdump1.hprof
10-XX:+PrintCommandLineFlags
11-Xlog:gc*=info:file=portal_gc.log:utctime,level,tags:filecount=50,filesize=100M
12
13# ZGC 1G
14-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
15
16# ZGC 16G
17-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 |
1### 找到占用8080端口号的进程,kill掉
2C:\Users\chao1>netstat -ano | findstr :8080
3 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
1### 必须要有运行的java进程,否则无法监听,`math-game`是一个简单的程序,每隔一秒生成一个随机数,再执行质因数分解,并打印出分解结果。
2
3curl -O https://arthas.aliyun.com/math-game.jar
4java -jar math-game.jar
5
6### 安装运行arthas
7# 可以通过 --username 选项来指定用户,默认值是arthas
8curl -O https://arthas.aliyun.com/arthas-boot.jar
9java -jar arthas-boot.jar --target-ip 0.0.0.0 --password 123
10
11### arthas运行日志
12
13[root@localhost ~]# tail ~/logs/arthas/arthas.log
14
15### 退出arthas
16
17* 如果只是退出当前的连接,其他客户端不受影响,可以用 quit或者exit命令
18* 目标进程上的 arthas 还会继续运行,端口保持开放,下次连接时执行java -jar arthas-boot.jar 可以直接连接上
19* 如果想完全退出 arthas,可以执行stop命令
20
21### 客户端再次连接
22
23java -jar arthas-boot.jar
24
25### 选择应用 java 进程
26
27根据pid和进程名选择
28
29# dashboard
30
31[arthas@10056]$ dashboard
32ID NAME GROUP PRIORITY STATE %CPU DELTA_TIME TIME INTERRUPTE DAEMON
331 main main 5 TIMED_WAIT 0.0 0.000 0:0.087 false false
3423 arthas-NettyHttpTelnetBootstrap- system 5 RUNNABLE 0.0 0.000 0:0.078 false true
3512 Attach Listener system 9 RUNNABLE 0.0 0.000 0:0.031 false true
3617 arthas-NettyHttpTelnetBootstrap- system 5 RUNNABLE 0.0 0.000 0:0.013 false true
3724 arthas-command-execute system 5 TIMED_WAIT 0.0 0.000 0:0.002 false true
382 Reference Handler system 10 RUNNABLE 0.0 0.000 0:0.000 false true
393 Finalizer system 8 WAITING 0.0 0.000 0:0.000 false true
404 Signal Dispatcher system 9 RUNNABLE 0.0 0.000 0:0.000 false true
4110 Notification Thread system 9 RUNNABLE 0.0 0.000 0:0.000 false true
4214 arthas-timer system 9 WAITING 0.0 0.000 0:0.000 false true
4318 arthas-NettyWebsocketTtyBootstra system 5 RUNNABLE 0.0 0.000 0:0.000 false true
4419 arthas-NettyWebsocketTtyBootstra system 5 RUNNABLE 0.0 0.000 0:0.000 false true
4520 arthas-shell-server system 9 TIMED_WAIT 0.0 0.000 0:0.000 false true
4621 arthas-session-manager system 9 TIMED_WAIT 0.0 0.000 0:0.000 false true
4725 Timer-for-arthas-dashboard-2a3b7 system 5 RUNNABLE 0.0 0.000 0:0.000 false true
4811 Common-Cleaner InnocuousThreadG 8 TIMED_WAIT 0.0 0.000 0:0.000 false true
49Memory used total max usage GC
50heap 26M 64M 988M 2.71% gc.g1_young_generation.count 4
51g1_eden_space 5M 18M -1 27.78% gc.g1_young_generation.time(ms) 15
52g1_old_gen 18M 43M 988M 1.90% gc.g1_old_generation.count 0
53g1_survivor_space 3M 3M -1 100.00% gc.g1_old_generation.time(ms) 0
54nonheap 26M 29M -1 88.27%
55codeheap_'non-nmethods' 1M 2M 5M 21.81%
56metaspace 18M 19M -1 98.78%
57codeheap_'profiled_nmethods' 3M 3M 117M 3.01%
58compressed_class_space 2M 2M 1024M 0.22%
59codeheap_'non-profiled_nmeth 586K 2496K 120036K 0.49%
60ods'
61mapped 0K 0K - 0.00%
62direct 4M 4M - 100.00%
63Runtime
64os.name Linux
65os.version 3.10.0-957.el7.x86_64
66java.version 17.0.11
67java.home /usr/local/software/jdk17
68systemload.average 0.00
69processors 2
70timestamp/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
1package com.soulboy.controller;
2
3import org.springframework.web.bind.annotation.RequestMapping;
4import org.springframework.web.bind.annotation.RestController;
5
6import java.util.ArrayList;
7import java.util.HashMap;
8import java.util.Map;
9
10@RestController
11@RequestMapping("/api/product2jvm")
12public class Product2JVMController {
13
14 /**
15 * 随机产生大小不同的对象
16 * @return
17 * @throws InterruptedException
18 */
19 @RequestMapping("/find")
20 public Map<String,Object> find(int id) throws InterruptedException {
21 Thread.sleep(500);
22 boolean check = test1(id);
23
24 HashMap<String, Object> map = new HashMap<>(16);
25 map.put("id", id);
26 map.put("check",check);
27 return map;
28 }
29
30 private boolean test1(int id) throws InterruptedException {
31 Thread.sleep(800);
32 return test2(id);
33 }
34
35 private boolean test2(int id) throws InterruptedException {
36 Thread.sleep(200);
37 return CommonUtil.checkIdRange(id);
38
39 }
40}
CommonUtil
1package com.soulboy.controller;
2
3public class CommonUtil {
4 public static final boolean checkIdRange(int id) {
5 try {
6 Thread.sleep(100);
7 } catch (InterruptedException e) {
8 throw new RuntimeException(e);
9 }
10
11 if (id < 0) {
12 return false;
13 } else {
14 return true;
15 }
16 }
17}
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
1# 在内存中编译Hello.java为Hello.class 2mc /root/Hello.java 3 4# 可以通过-d命令指定输出目录(输出对应的路径会有package路径) 5mc -d /root/test /root/Hello.java
redefine
:把新生成的字节码文件在内存中执行(替换新的字节码文件)
加载外部的.class文件,redefine到JVM里
redefine后的原来的类不能恢复,redefine有可能失败(比如增加了新的field)
不允许新增加field/method,只能修改内部方法和逻辑
正在跑的函数,没有退出不能生效
动态修改方法内部逻辑
修改某个方法的接口返回值,增加多一个参数
字节码增强,如果重启 JVM,则增强的部分会失效
步骤
1# 使用jad反编译 D:\Project\redlock\Product2JVMController.java
2[arthas@14264]$ pwd
3D:\Project\redlock
4[arthas@14264]$ jad --source-only com.soulboy.controller.Product2JVMController > Product2JVMController.java
5
6# 修改为3个参数(不能新增方法,也不能新增字段)
7map.put("id", id);
8map.put("check", check);
9map.put("productId", id); # 新增
10
11# 使用mc内存中对新的代码编译 D:\Project\redlock\Projectredlock\com\soulboy\controller\Product2JVMController.class
12mc Product2JVMController.java -d D:\\Project\\redlock
13
14# 使用redefine命令加载新的字节码
15redefine D:\\Project\\redlock\\Projectredlock\\com\\soulboy\\controller\\Product2JVMController.class
16
17# 返回值多了一个参数 http://127.0.0.1:8080/api/product2jvm/find?id=5
18
19{
20"productId": 5,
21"id": 5,
22"check": true
23}