在Java应用开发中,内存泄漏和CPU飙升是两类高频出现的生产问题,也是常见的面试问题。这里通过一些demo进行实践。
内存泄漏
private static List<byte[]> leakList = new ArrayList<>();@GetMapping("/memory/leak")
public void test2() {try {while (true) {// 分配1MB的内存块byte[] block = new byte[1024 * 1024];leakList.add(block);// 每隔100毫秒分配一次,模拟内存不断积累Thread.sleep(100);}} catch (Exception e) {e.printStackTrace();}
}
java -jar -Xms256m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/wx/workspace/app.hprof test.jarjstat -gcutil <pid> 1000 # 观察Old区使用率持续上涨
-XX:+HeapDumpOnOutOfMemoryError
: 在发生 OutOfMemoryError 时自动生成堆转储文件(Heap Dump)
-XX:HeapDumpPath
: 指定堆转储文件的保存位置,可以使用工具(如 MAT、JProfiler 等)打开和分析
可以通过jmap
命令生成hprof
报告
jmap -dump:live,format=b,file=heap_dump.bin <pid># 查看Java进程
jps -l
jcmd
ps -ef | grep java
使用JProfiler
打开*.hprof
文件,查看最大内存占有的数据类型
点击目标数据类型,选择incoming references
incoming references
:查看目标数据类型被哪些对象引用了outgoing references
:查看目标数据类型引用了哪些对象
找到泄漏对象 LeakedObject
由于被静态集合引用而无法被垃圾回收,从而确定位置
CPU飙升
@GetMapping("/cpu")
public void test3() {CompletableFuture<Void> t1 = CompletableFuture.supplyAsync(() -> {while (true) {// 密集计算,无退出条件double result = 0;for (int i = 0; i < 100000; i++) {result += Math.sqrt(i) * Math.tan(i);}}}, threadPoolExecutor);CompletableFuture<Void> t2 = CompletableFuture.supplyAsync(() -> {while (true) {try {Thread.sleep(1000);System.out.println("正常业务运行中...");} catch (InterruptedException e) {e.printStackTrace();}}}, threadPoolExecutor);CompletableFuture.allOf(t1, t2).join();
}
- 定位Java进程,
top -c
- 查看线程级别CPU占用,
top -Hp <PID>
- 抓去线程快照,
jstack <PID> > thread.txt
- 线程ID转换
printf "%x\n" <线程ID>
- 进行分析
"WXW-Thread-1" #35 daemon prio=5 os_prio=31 cpu=150071.37ms elapsed=150.75s tid=0x00007fdfb58fea00 nid=0x7207 runnable [0x0000700006795000]java.lang.Thread.State: RUNNABLEat com.example.controller.TestThreadController.lambda$test3$2(TestThreadController.java:94)at java.util.concurrent.CompletableFuture$AsyncSupply.run(java.base@17.0.13/CompletableFuture.java:1768)at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.13/ThreadPoolExecutor.java:1136)at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.13/ThreadPoolExecutor.java:635)at java.lang.Thread.run(java.base@17.0.13/Thread.java:840)
通过日志查找问题所在
使用JProfiler分析CPU视图,定位异常线程和代码
内存结构
JVM(Java虚拟机)的内存结构是其运行时数据区域的核心组成部分,用于管理程序执行过程中的内存分配和回收
程序计数器
作用
记录当前线程执行的位置(字节码指令地址)
特性
- 线程私有,每个线程独立存储
- 唯一不会发生 OutOfMemoryError 的区域
虚拟机栈
作用
存储方法调用栈帧(每个方法对应一个栈帧),管理局部变量、操作数栈、动态链接和方法返回地址
栈帧结构
- 局部变量表:存放方法参数和方法内定义的局部变量(基本类型和对象引用)
- 操作数栈:执行自己饿吗指令的临时操作数存储区
- 动态链接:指向运行时常量池中该方法的符号引用
- 返回地址:方法退出后需要返回的位置
特点
- 线程私有
- 可能抛出的异常
StackOverflowError
:栈深度超过限制(无限递归)OutOfMemoryError
:扩展栈是无法申请足够内存
本地方法栈
作用
为Native方法提供服务
特性
线程私有
方法区
作用
存储类信息、常量、静态变量、即时编译器编译后的代码,包含运行时常量池,存放编译期生成的字面量、符号引用及运行时添加的常量
特性
- 线程共享
- 可能抛出
OutOfMemoryError
堆
堆是Java应用程序最重要的一个内存结构
作用
存放所有对象实例和数组(通过 new 关键字创建的对象)
特性
- 线程共享
- 可能抛出
OutOfMemoryError
- 通过
-Xmx
和-Xms
设置最大和初始堆大小,最好是设置一致
堆内存划分
- 新生代
- Eden区:对象初次分配的区域
- Survivor区:存放经过Minor GC后存活的对象
- 老年代:存放长期存活的对象(经过多次GC未被回收)
- 老年代:新生代=2:1,Eden区:Survivor0区:Survivor1区=8:1:1