跳转至

JVM 实践

JVM 命令行工具

以下面这段死锁代码为例:

public class DeadLock {

    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public static class RunnableA implements Runnable {
        @Override
        public void run() {
            while(true)
            {
                synchronized (lockA) {
                    System.out.println("threadA has acquired lockA");
                    synchronized (lockB) {
                        System.out.println("threadA has acquired lockB");
                    }
                }
            }
        }
    }

    public static class RunnableB implements Runnable {
        @Override
        public void run() {
            while(true)
            {
                synchronized (lockB) {
                    System.out.println("threadB has acquired lockB");
                    synchronized (lockA) {
                        System.out.println("threadB has acquired lockA");
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread threadA = new Thread(new RunnableA());
        Thread threadB = new Thread(new RunnableB());

        threadA.start();
        threadB.start();

    }
}

jps

查看进程状态信息,得到进程号 + 名称

image-20250220160047048

jstack

jstack 进程号

根据给定的进程号查看栈信息,可以看到每个线程执行到了哪一行(14 / 29)

image-20250220160323377

jhsdb jmap

查看堆内存信息

jhsdb jmap --heap --pid 24636

可以看到使用了哪个垃圾收集器,分代情况,堆内存的使用情况等。堆大小最大默认为内存的 1/4

image-20250220161433812

生成到 dump 文件

jmap -dump:format=b,file=heap.hprof pid

以二进制格式生成到指定文件中

image-20250220162346024

这个文件需要用某些工具打开

dump 文件是进程或系统在某一给定时间的快照。比如进程崩溃时,甚至是任何时候,都可以通过工具备份出进程快照。

包括线程信息、栈信息、异常信息等

jstat

jstat -gcutil pid

image-20250220162750954

这里 M 表示元数据区(方法区在 J8 后的实现)空间使用使用百分比,CGC表示并发回收次数,CGCT表示并发回收消耗的总时间(秒)

可视化工具

jconsole

C:\Program Files\Java\jdk-17 目录下有 jconsole.exe(Windows)

image-20250220164343441

image-20250220164502024

VisualVM

jdk8 中自带,高版本似乎要去官网下载

参数设置

这里讨论 springboot 项目通过 jar 包部署的启动参数设置:

在 linux 系统下直接加参数启动 springboot 项目

nohup java -Xms512m -Xmx1024m -jar xxx.jar --spring.profiles.active=prod &

nohup:在系统后台不挂断地运行命令,退出终端不会影响程序运行

&:让命令在后台执行,退出终端后仍旧执行命令

  • 设置堆空间的大小

    一般初始化大小和最大大小设置为相同的值,防止垃圾收集器收缩堆产生额外的时间,一般最大大小为物理内存的 1/4;但是也不能太大,如果发生了 fullgc,会扫描整个堆浪费时间

    -Xms 堆的初始化大小
    -Xmx 堆的最大大小
    

    不指定大小默认为字节

  • 虚拟机栈设置

    每个线程默认开启 1M 内存,一般设置为 256K 就够用。设置的太大会减少可用的线程数

    -Xss 每个线程虚拟机栈的大小
    
  • Eden 区和每个 Survivor 区的大小比例

    -XXSurvivorRatio=8 # 表示 Eden : Survivors = 8 : 2
    
  • 年轻代晋升老年代阈值

    默认为 15,取值范围在 0 ~ 15

    -XX:MaxTenuringThreshold=15
    
  • 设置使用哪种垃圾回收器

    -XX:+UseParallelGC
    -XX:+UseParallelOldGC
    
    -XX:+UseG1GC
    

内存溢出排查

image-20250220170111397

方法区 OOM 的主要原因是类加载的太多了,主要考虑排查堆区的内存溢出:

  1. 使用 jmap -dump 得到 dump 文件。但是如果程序已经崩溃,则此方法失败。则通过 JVM 参数的方式生成 dump 文件

    在 idea 中可以通过 Edit Configurations 设置:

    image-20250220195959053

    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/home/app/dumps/
    
  2. 使用 VisualVM 来打开 hprof 文件,查看 OOM 问题可能的位置

CPU 飙高排查

linux 服务器上,使用 top 命令列出 cpu 使用情况,记录一下进程 pid

随后查找进程中 CPU 利用率过高的线程 tid

ps H -eo pid, tid, %cpu | grep pid

快速查看十六进制下的 tid

printf "%x\n" tid

接下来利用进程 pid jstack 一下,查看各个线程栈(此时 tid 是以十六进制表示的),在其中找到目标线程的日志,可以定位哪一行代码出现问题