JVM

1 概览

同时参考 小林 conding

1.1 JVM 流程

image-20260214092212282

1.2 目录

image-20260214092412872

2 JVM 组成

2.1 程序计数器

线程私有的,每个线程一份,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。

javap -v xx.class 打印堆栈大小,局部变量的数量和方法的参数。

image-20260214102257422

2.2 Java 堆

线程共享的区域:主要用来保存 对象实例,数组 等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出 OutOfMemoryError 异常。

组成:年轻代 + 老年代
年轻代被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区,老年代主要保存生命周期长的对象,一般是一些老的对象

image-20260214102927226

在 JDK 1.8 及以后的版本中,方法区(永久代)被元空间取代,使用本地内存,因为方法区的数据越来越多,防止内存溢出。

2.3 虚拟机栈

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出。
  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
image-20260214104440985

2.3.1 常见问题

  1. 垃圾回收是否涉及栈内存?
    栈内存不被 GC 回收,垃圾回收主要指的是堆内存,当栈帧弹栈以后,内存就会释放。

  2. 栈内存分配越大越好吗?

    未必,默认的栈内存通常为 1024k,栈帧过大会导致线程数变少,例如,机器总内存为 512m,目前能活动的线程数则为 512 个,如果把栈内存改为 2048k,那么能活动的栈帧就会减半。

  3. 方法内的局部变量是否线程安全?

    • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。

    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。

    image-20260214110317514
  4. 栈内存溢出情况

    • 栈帧过多导致栈内存溢出,典型问题:递归调用

    • 栈帧过大导致栈内存溢出

  5. 堆和栈的区别

    • 栈主要用于存储局部变量、方法调用的参数、方法返回地址以及一些临时数据,堆用于存储对象的实例(包括类的实例和数组)。
    • 栈中的数据对线程是私有的,每个线程有自己的栈空间。堆中的数据对线程是共享的,所有线程都可以访问堆上的对象。
    • 栈中的数据具有确定的生命周期,当一个方法调用结束时,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定,对象会在垃圾回收机制(Garbage Collection, GC)检测到对象不再被引用时才被回收。

2.4 方法区

方法区(MethodArea)是各个线程 共享的内存区域,主要存储 类的信息、运行时常量池。虚拟机启动的时候创建,关闭虚拟机时释放。如果方法区域中的内存无法满足分配请求,则会抛出 OutOfMemoryError: Metaspace

image-20260214154148262

2.4.1 常量池

可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

javap -v Application.class # 查看字节码结构(类的基本信息、常量池、方法定义)
image-20260214154405057

常量池是 .class 文件中的,当该类被加载,它的常量池信息就会 *放入运行时常量池**,并把里面的符号地址变为真实地址

image-20260214154559112

2.5 直接内存

下图所示在常规 IO 的数据拷贝流程中,因为 Java 不能直接访问数据,所以数据在内存中存储了两份。

image-20260214155529389

NIO 的做法

image-20260214155821296
  • 直接内存并不属于 JVM 中的内存结构,不由 JVM 进行管理,是虚拟机的系统内存
  • 常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理

3 类加载器

JVM 只会运行二进制文件,类加载器的作用就是将字节码文件加载到 JVM 中,从而让 Java 程序能够启动起来。

3.1 类加载器的种类

image-20260214162150497
  • 启动类加载器(Bootstrap Class Loader):最顶层的类加载器,负责加载 Java 的核心库(如位于 jre/lib/rt.jar 中的类),用 C++ 编写,是 JVM 的一部分。启动类加载器无法被 Java 程序直接引用。
  • 扩展类加载器(Extension Class Loader): Java 语言实现,继承自 ClassLoader 类,负责加载 Java 扩展目录(jre/lib/ext 或由系统变量 Java.ext.dirs 指定的目录)下的 jar 包和类库。扩展类加载器由启动类加载器加载,并且父加载器就是启动类加载器。
  • 系统类加载器(System Class Loader)/ 应用程序类加载器(Application Class Loader): Java 语言实现,负责加载用户类路径(ClassPath)上的指定类库,是平时编写 Java 程序时默认使用的类加载器。系统类加载器的父加载器是扩展类加载器。它可以通过 ClassLoader.getSystemClassLoader()方法获取到。
  • 自定义类加载器(Custom Class Loader):开发者可以根据需求定制类的加载方式,比如从网络加载 class 文件、数据库、甚至是加密的文件中加载类等。自定义类加载器可以用来扩展 Java 应用程序的灵活性和安全性,是 Java 动态性的一个重要体现。

这些类加载器之间的关系形成了 双亲委派模型

3.2 双亲委派

核心思想是当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。

只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

举个例子:自己写了一个 java.lang.String 类,当 AppClassLoader 要加载它时,会先委派给 Extension ClassLoader,再委派给 Bootstrap ClassLoader。而启动类加载器发现自己已经加载过 JDK 自带的 String 类了,就直接返回这个类,不会去加载自定义的 String 类。

image-20260214163146039

JVM 采用双亲委派机制的两个核心原因:

  1. 保证类的唯一性和安全性:避免同一个类被不同加载器重复加载,确保不会被篡改。
  2. 实现类的复用:核心类只需要被顶层加载器加载一次,所有子加载器都能共享这个类,减少内存消耗。

3.2 类加载过程

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段,其中,验证、准备和解析这三个部分统称为连接(linking)。

image-20260214165707593
  • 加载:通过类的全限定名(包名 + 类名),获取到该类的 .class 文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的 Java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

    image-20260214170328788
  • 连接

    • 验证:确保 class 文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的 class 类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证。

    • 准备:为类中的静态字段分配内存,并设置默认的初始值。

      image-20260214170804762
    • 解析:将常量池的「符号引用」直接替换为「直接引用」的过程。

      image-20260214171248180
  • 初始化:对类的 静态变量静态代码块 执行初始化操作,简单来说就是执行类的构造器方法,要注意的是这里的构造器方法并不是开发者写的,而是编译器自动生成的。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

  • 使用:使用类或者创建对象。

  • 卸载:一个类要被 JVM 卸载,条件非常苛刻,需要同时满足以下三点:

    • 该类所有的实例都已经被回收:这是最显而易见的前提。如果堆中还存在这个类的任何一个实例对象,那么定义这个对象的 Class 对象肯定不能被卸载。
    • 加载该类的 ClassLoader 已经被回收:这是 最关键也是最难满足的条件。类与其加载器是双向绑定的共生关系。一个类由哪个类加载器加载,这个信息是存储在 Class 对象里的。要卸载一个类,必须先卸载加载它的类加载器。
    • 类对应的 Java.lang.Class 对象没有任何地方被引用:不能在任何地方通过反射(如静态字段、全局变量)、静态变量、JNI 等途径引用到这个 Class 对象。一旦这个 Class 对象还存在强引用,GC 就不会回收它,那么这个类也就不会被卸载。

4 垃圾回收

垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。

4.1 判断垃圾的方法

4.1.2 引用计数法

  • 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加 1;当引用失效时,计数器减 1。当计数器为 0 时,表示对象不再被任何变量引用,可以被回收。
  • 缺点:不能解决 循环引用 的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为 0,导致对象无法被回收。

4.1.3 可达性分析

Java 虚拟机主要采用此算法来判断对象是否为垃圾。

image-20260215094624415 image-20260215095117194

原理:从一组称为 GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。

4.2 垃圾回收算法

4.2.1 标记清除算法

先标记所有可回收对象,然后直接清理它们的内存。但这样会产生很多内存碎片,后续分配大对象时可能找不到连续空间。

image-20260215095616420

4.2.2 标记整理算法

为了解决碎片问题,有 “标记 - 整理”:标记后不是直接清除,而是把存活对象往一端移动,然后清理边界外的内存,这样内存更整齐,但移动对象会有性能开销。

image-20260215095742826

4.2.3 复制算法

把内存分成两块,每次只用其中一块,回收时把存活对象复制到另一块,然后清空当前块。这种方式没有碎片,但会浪费一半内存,适合存活对象少的场景(比如新生代)。

image-20260215095855465

实际中 JVM 会分代使用这些算法。因为对象的生命周期不同,新生代(刚创建的对象)存活率低,老年代(存活久的对象)存活率高。新生代一般用 “复制算法”:分成 Eden 区和两个 Survivor 区(比如 8:1:1),新对象先放 Eden,满了就触发 Minor GC,把存活对象复制到一个 Survivor,多次存活后移到老年代。老年代用 “标记 - 清除” 或 “标记 - 整理”,因为对象存活率高,复制成本太高。

4.2.4 分代收集算法

上面提到垃圾回收时会分为新生代和老年代,下面详细说明垃圾回收时他们的作用。

在 Java8 中,堆被分为了两份:新生代和老年代 [1:2]。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX: MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

1719974471041-14f6ed7f-358b-426a-b614-2501ceae0035

4.3 垃圾回收器

1712649527581-d6aee0bf-35ab-4406-8a26-270b35ae8771
  • 串行垃圾回收器:Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器的老年代版本;

    垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

    image-20260215104422245
  • 并行垃圾回收器:ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;Parallel Old 收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge 收集器的老年代版本;JDK8 默认使用这两个。

    image-20260215104505620
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。

    image-20260215104909716

4.3.1 G1 垃圾回收器

G1(Garbage First) 收集器 (标记-复制算法 [二者混合使用]): Java 堆并行收集器,G1 收集器是 JDK1.7 提供的一个新收集器,JDK 9 起成为默认 GC。G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范围是整个 Java 堆(包括新生代,老年代 [不分代]),划分成多个区域,每个区域都可以充当 eden,survivor,old,humongous(专为大对象准备),回收时分成三个阶段:新生代回收、并发标记、混合收集,如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

image-20260215113523972

下面详细解释这三个流程:

image-20260215114508361
  1. 新生代回收

    初始时,所有区域都处于空闲状态,创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象,当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程(stop the world)。

    image-20260215120904339

    随着时间流逝,伊甸园的内存又有不足,将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象普升至老年代。

    image-20260215121113801
  2. Young Collection + Concurrent Mark (新生代垃圾回收+并发标记)

    当老年代占用内存超过间值(默认是 45%)后,触发并发标记,这时无需暂停用户线程

    image-20260215121422858
  3. 混合回收

    并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入 混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间自标 优先回收价值高(存活对象少)的区域(这也是 GabageFirst 名称的由来)。混合收集阶段中,参与复制的有 eden、survivor、old。

    image-20260215121551504

    复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集。

    image-20260215121813220

4.4 minorGC、majorGC、fullGC

在 Java 中,垃圾回收机制是自动管理内存的重要组成部分。根据其作用范围和触发条件的不同,可以将 GC 分为三种类型:Minor GC(也称为 Young GC)、Major GC(有时也称为 Old GC)、以及 Full GC。以下是这三种 GC 的区别和触发场景:

Minor GC (Young GC)

  • 作用范围:只针对年轻代进行回收,包括 Eden 区和两个 Survivor 区(S0 和 S1)。
  • 触发条件:当 Eden 区空间不足时,JVM 会触发一次 Minor GC,将 Eden 区和一个 Survivor 区中的存活对象移动到另一个 Survivor 区或老年代(Old Generation)。
  • 特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。

Major GC

  • 作用范围:主要针对老年代进行回收,但不一定只回收老年代。
  • 触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发 Major GC。
  • 特点:相比 Minor GC,Major GC 发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。

Full GC

  • 作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。
  • 触发条件
    • 直接调用 System.gc()Runtime.getRuntime().gc() 方法时,虽然不能保证立即执行,但 JVM 会尝试执行 Full GC。
    • Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发 Full GC,对整个堆内存进行回收。
    • 当永久代(Java 8 之前的版本)或元空间(Java 8 及以后的版本)空间不足时。
  • 特点:Full GC 是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少 Full GC 的触发。

4.5 强引用、软引用、弱引用、虚引用

  1. 强引用:只有所有 GCRoots 对象都不通过【强引|用】引 I 用该对象,该对象才能被垃圾回收

    User u = new User();
    
    image-20260215122518198
  2. 软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收

    User user = new User();
    SoftReference softReference = new SoftReference(user);
    
    image-20260215122631291
  3. 弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象

    User user = new User();:
    WeakReference weakReference = new WeakReference(user);
    
    image-20260215122819541
  4. 虚引用:必须配合引 l 用队列使用,被引 l 用对象回收时,会将虚引 l 用入队,由 ReferenceHandler 线程调用虚引 l 用相关方法释放直接内存

    User user = new User();
    ReferenceQueue referenceQueue = new ReferenceQueue();
    PhantomReference phantomReference = new PhantomReference(user,queue);
    
    image-20260215123049463

5 JVM 实践

5.1 JVM 调优参数

5.1.1 配置参数

  1. war 包部署在 tomcat 中设置

    修改 TOMCAT_HOME/bin/catalina.sh 文件

    image-20260219161038624
  2. jar 包部署在启动参数设置

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

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

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

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

5.1.2 有哪些参数

对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。

https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html

  1. 设置堆空间大小

    设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。

    -Xms:设置堆的初始化大小
    -Xmx:设置堆的最大大小
    -Xms : 1024
    -Xms : 1024k # 不指定单位默认为字节,指定单位,按照指定的单位设置
    -Xms : 1024m
    -Xms : 1g
    

    堆空间设置多少合适?

    • 最大大小的默认值是物理内存的 1/4,初始大小是物理内存的 1/64
    • 堆太小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生 stw,暂停用户线程
    • 堆内存大肯定是好的,存在风险,假如发生了 fullgc,它会扫描整个堆空间,暂停用户线程的时间长
    • 设置参考推荐:尽量大,也要考察一下当前计算机其他程序的内存使用情况
  2. 设置虚拟机栈

    虚拟机栈的设置:每个线程默认会开启 1M 的内存,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

    -Xss 对每个线程stack大小的调整
    -Xss 128k
    
  3. 年轻代中 Eden 区和两个 Survivor 区的大小比例
    设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。通过增大 Eden 区的大小,来减少 YGC 发生的次数,但有时我们发现,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优。

    -XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden=2:8
    
  4. 年轻代晋升老年代阀值

    -XX:MaxTenuringThreshold=threshold
    

    默认为 15,取值范围 0-15

  5. 设置垃圾回收器

    通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。

    -XX:+UseParalleIGC
    -XX:+UseParallelOldGC
    

5.2 JVM 调优工具

5.2.1 命令行

  1. jps:进程状态信息

    image-20260219164748353
  2. jstack:查看 Java 进程内线程的堆栈信息

    jstack [option] < pid >

    image-20260219164720391
  3. jmap:用于生成堆转内存快照、内存使用情况

    jmap -heap pid # 显示Java堆的信息
    jmap -dump:format=b,file=heap.hprof pid
    

    format = b 表示以 hprof 二进制格式转储 Java 堆的内存

    file = < filename > 用于指定快照 dump 文件的文件名。

    例如:

    C:\Userslyuhon>jmap -heap 53280
    Attaching to process ID 53280, please wait..Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.321-b07
    using thread-local object allocation.
    Parallel GC with 8 thread(s) # 并行的垃圾回收器
    Heap Configuration: # 堆配置
    MinHeapFreeRatio = 0 # 空闲堆空间的最小百分比
    MaxHeapFreeRatio = 100 # 空闲堆空间的最大百分比
    MaxHeapSize = 178257920(170.0MB)# 新生代堆空间的默认值
    NewSize = 8524922880(8130.0MB)# 堆空间允许的最大值
    MaxNewSize = 2841640960(2710.0MB)# 新生代堆空间允许的最大值
    Oldsize = 356515840(340.0MB)# 老年代堆空间的默认值
    NewRatio = 2# 新生代与老年代的堆空间比值,表示新生代:老年代=1:2
    SurvivorRatio = 8# 两个survivor区和Eden区的堆空间比值为8,表示so:S1:Eden=1:1:8
    Metaspacesize = 21807104(20.796875MB)# 元空间的默认值
    CompressedClassSpaceSize =1073741824(1024.0MB) # 压缩类使用空间大小
    MaxMetaspaceSize = 175921860444151MB # 元空间允许的最大值
    G1HeapRegionSize = 0(O.OMB)# 在使用 G1 垃圾回收算法时,JVM 会将 Heap 空间分隔为若干个 Region,该参数用来指定每个 Region 空间的大小
    
    Heap Usage:
    PS Young Generation
    Eden Space:# Eden使用情况
    	capacity = 134217728 (128.0MB)
    	used = 10737496(10.240074157714844MB)
    	free = 123480232 (117.75992584228516MB)
    	8.000057935714722% used
    From Space:# Survivor-From 使用情况
    	capacity = 22020096 (21.0MB)
    	used = 0(0.0MB)
    	free = 22020096(21.0MB)
    	0.0% used
    To Space:capacity # Survivor-To使用情况
    	capacity = 22020096(21.0MB)
    	used =  0(0.0MB)
    	free = 22020096(21.0MB)
    	0.0% used
    PS oid Generation # 老年代 使用情况
    	capacity = 356515840 (340.0MB)
    	used = 0(0.0MB)
    	free = 356515840(340.0MB)
    	0.0% used
    
    3185 interned Strings occupying 261264 bytes
    

    它是一个进程或系统在某一给定的时间的快照。比如在进程崩溃时,甚至是任何时候,我们都可以通过工具将系统或某进程的内存备份出来供调试分析用。dump 文件中包含了程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据,方便系统技术人员进行错误排查。

    例如:jmap -dump:format=b,file=D:/learn_Java/demo.hprof 24612

  4. jstat:是 JVM 统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等

    image-20260219172817303

5.2.2 可视化

  1. jconsole:用于对 jvm 的内存,线程,类的监控,是一个基于 jmx 的 GUI 性能监控工具
    打开方式:java 安装目录 bin 目录下直接启动 jconsole.exe 就行。例如:D:\learn_Java\jdks\ms-17.0.17\bin\jconsole.exe

  2. VisualVM:能够监控线程,内存情况,查看方法的 CPU 时间和内存中的对象,已被 GC 的对象,反向查看分配的堆栈
    打开方式:java 安装目录 bin 目录下直接启动 jvisualvm.exe 就行。但是只有 JDK 8 内置,以上版本需要官网上下载。

    image-20260219173601034

5.3 内存泄漏排查思路 ⭐

通过 jmap 指定打印他的内存快照 dump(Dump 文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到 dump 文件中)

  • 使用 jmap 命令获取运行中程序的 dump 文件

    jmap -dump: format = b, file = heap.hprof pid
    
  • 使用 vm 参数获取 dump 文件
    有的情况是内存溢出之后程序则会直接中断,而 jmap 只能打印在运行中的程序,所以建议通过参数的方式的生成 dump 文件

    -XX:+HeapDumpOnOutOfMemoryError
    
    -XX: HeapDumpPath =/home/app/dumps/
    

生成的 **.hprof 文件可以使用 VisualVM 打开。通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题,找到对应的代码,通过阅读上下文的情况,进行修复即可。

image-20260219204713367

5.4 CPU 飙高的排查方案

  1. top 查看进程使用情况,发现 2266 进程占用 CPU 使用率高。
image-20260219205111515
  1. 打印 2266 进程的所有线程。

image-20260219205130886

  1. 使用 jstack 2266 查看进程内线程的堆栈信息,但 jstack 内输出的是十六进制的线程信息,2276 的十六进制为 0x8e4。

    image-20260219205648585
posted @ 2026-03-19 16:58  solarlemon  阅读(0)  评论(0)    收藏  举报