JVM 的内存管理与 GC 调优

概览

JVM 的静态架构

jvm-arch.png

JVM 中和性能相关的关键组件包括:

  • JIT Compiler (Just-in-time Compiler)
  • Heap
  • GC (Garbage Collection)

在很多情况下由现代 JVM 中的 JIT 编译的 Java 代码和 C++ 一样快,几乎不用调整,所以通常对于 JVM 的调优只涉及对 Heap 大小和 GC 算法的调整。

JVM 64-bit 和 32-bit 的主要区别是 64-bit 的 JVM 会使用比 32-bit JVM 更大的指针(64 位需要 8 bytes 指针而 32 位需要 4 bytes 指针),因此会对内存使用产生影响,导致执行速度稍慢。但随着 JDK 和硬件的发展,这种性能损失越来越小。因此与 64-bit 可以获得更大的内存相比,这种性能损基本可以忽略。

运行时的数据区域

JVM 在运行的时候会将其管理的内存从逻辑上划分为不同的数据区域,对 JVM 中的单个线程,JVM 会为其分配程序计数器(Program Counter Register)、线程方法栈和本地方法栈(Native Method Stacks),同时,JVM 会为划分出一块公共区域(Java 堆 Heap,不要求物理连续)供所有线程共享,各线程可以在此创建对象。

jvm-run-time-data-areas.png

线程数据区

线程数据区包括

  • 程序计数器,用来存储当前线程所执行的代码行号
  • 方法栈,用来储存当前线程的 Java 方法执行栈,每个方法对应一个栈帧(Stack Frame),其存储和方法运行相关的数据
  • 本地方法栈,类似与方法栈,用来存储 Native 方法栈

因为 Java 支持递归调用,所以当方法的调用超过了 JVM 所允许的递归调用深度,JVM 会抛出 StackOverflowError,JVM 的栈是允许自动扩展的,但当 JVM 无法为栈申请到足够的内存时,JVM 会抛出 OutOfMemoryError

经验分析表明,对于大多数面向对象的语言,绝大多数对象(可以多达 98%,这取决于您对年轻对象的衡量标准)是在年轻的时候死亡的。JVM 的分代收集器(generializational collector)根据该经验,将堆分为多个代 满足某些提升标准的在年轻代中创建的对象(如经历了特定次数垃圾收集的对象),将被提升到下一更老的代。分代收集器对不同的代可以自由使用不同的收集策略,对各代分别进行垃圾收集。

JVM 的堆由三部分组成:

  • 年轻代(Yong Generation),年轻代又被分为两个空间,一个是 Eden Space 这里用来为新创建的对象分配空间,另一个是 Survivor Space (划分为两部分)。通常在这里进行的 GC 被称为 minor GC
  • 老年代(Old Generation),从年轻代中存活下来的对象,会被拷贝到这里。这里占用的空间要比年轻代多并且发生在这里的 GC 要比年轻代少得多。在这里进行的 GC 被称为 major GC(或者 full GC)
  • 持久代(Permanent Generation),对应运行时的方法区(method area)和运行时常量池(Run-time constant pool)。这个区域不是用来永久的存储那些从老年代存活下来的对象,而是用来保存:已经加载的类的信息(loaded classes information)、常量(constant)、字符串常量(string constant),以及编译期间生成的字面量和符号引用。

JDK 8 以前 32 位机器默认的持久代的大小为 64 M,64 位的机器则为 85 M,并且持久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。而 JDK 8 中用元数据空间来代替持久代,元数据空间分配在本地内存中,元空间的最大可分配空间理论上就是系统可用的内存空间,同时对元数据空间的管理独立于 JVM 对堆的管理(不再依赖不同的 GC 算法)。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的,每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是 JVM 的元空间。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。

各数据区的参数设置

  • -Xmx,设置 JVM 最大可用内存。这里要注意如果使用了 nio 的 ByteBuffer.allocateDirectMappedByteBuffer(堆外内存),还需要考虑这部分内存,堆外内存是由 -XX:MaxDirectMemorySize 控制的,但如果该参数没有设置,则 JVM 能分配的堆外内存是不能超过 -Xmx 设置的大小的
  • -Xms,设置 JVM 初始内存,此值可以与 -Xmx 相同以避免每次垃圾回收完成后 JVM 重新分配内存
  • -Xmn,设置年轻代大小,整个堆大小 = 年轻代大小 + 年老代大小 + 持久代大小,持久代一般为固定大小,所以增大年轻代后,将会减小年老代大小。此值对 GC 的影响较大,官方推荐配置为整个堆的 3/8。
  • -Xss,设置每个线程的栈大小,现在每个线程栈大小默认为 2 M,以前(Java5-)每个线程栈大小为 256 K。在相同物理内存下,减小这个值能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制的,不能无限生成。
  • -XX:NewRatio=<n>,设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代)。如:设置为 4,则年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5
  • -XX:SurvivorRatio=<n>,设置年轻代中 Eden 区与 Survivor 区的大小比值。如:设置为 4,则两个 Survivor 区与一个 Eden 区的比值为 2:4,一个 Survivor 区占整个年轻代的 1/6
  • -XX:MaxTenuringThreshold=<n>,设置对象存活的最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区直接进入年老代,如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,增加对象再年轻代的存活时间
  • -XX:MaxPermSize=<n>(Java8-),设置持久代大小
  • -XX:MaxMetaspaceSize(Java8),设置元数据空间,与该空间相关的调试命令:
    jmap -clstats PID # 打印类加载器数据,Java8 之前,使用 -permstat
    jstat -gc LVMID # 用来打印元空间的信息
    jcmd PID GC.class_stats # 用来输出详尽的类元数据的柱状图
    

内存分配

JVM 的内存分配是在 JVM 的堆(Heap)中以线程为单位进行的,其使用的主要技术包括:

  • Bump-the-pointer,该技术将 Eden 空间分为两部分,一部分是已经分配过内存的对象,并且使用一个指针指向最后创建的对象,另一部分为空闲的内存。当需要创建新对象时,只需要检查是否有足够的剩余空间,如果有,移动指针到新对象大小相等的距离。这种方式可极大地加快内存分配速度,并且避免内存碎片,但要求 Eden 部分的内存绝对规整。因此要求 JVM 能对内存进行压缩(Compact)整理
  • Free List,如果内存不是绝对规整,JVM 就需要维护一个列表记录那些内存是可用的,当为新对象分配内存时,需要在列表中找到一块足够大的内存用于分配对象,这种方式不要求内存的绝对规整,但会产生内存碎片
  • TLAB(Thread Local Allocation Buffer),无论采用以上那种方式去划分内存,都需要考虑在多线程情况下,如何保证以线程安全的方式分配分配内存,即避免两个或多个线程得到同一空闲的内存地址。最简单的办法就是当分配内存时,对整个内存加锁(或 CAS),但这将极大地的影响性能。所以 JVM 通过 TLAB (1.4.2 +)先给每个线程预分配内存,然后每个线程只访问他自己的 TLABs 空间,再使用 bump-the-pointer 为对象分配内存,这样可以在不加锁的情况下分配内存。可以使用 -XX:+UseTLAB 参数来控制 TLAB 的打开或关闭,默认为打开状态,除非使用了 -client

垃圾(内存)回收

JVM 因为要执行 GC 而停止应用程序的执行(stop-the-world),当 stop-the-world 发生时,除了 GC 所需的线程以外,所有线程都处于等待状态直到 GC任务完成。因此 GC 优化很多时候就是指减少 stop-the-world 发生的时间。

当要申请内存的线程无法获得一块足够大的连续空闲空间来存放新创建的对象时,JVM 就会判断是否需要启动 GC(Garbage Collection)来回收内存,即回收那些没有相对于 根集合(GC roots)引用的不可达对象(dead object),其中根集合包括:

  • 由系统类加载器加载的类,这些类从不会被卸载,它们可以通过静态属性的方式持有对象的引用,而自定义的类加载器加载的类不能成为 GC Roots
  • Java 方法栈中的局部变量或者参数
  • JNI 方法栈中的局部变量或者参数
  • JNI 全局引用
  • 用做同步监控的对象
  • 被 JVM 持有的对象,这些对象由于特殊的目的不被 GC 回收。这些对象可能是系统的类加载器,一些重要的异常处理类,一些为处理异常预留的对象,以及一些正在执行类加载的自定义的类加载器。

JVM 规范中,并没有强制要求垃圾回收器 立即 回收不可达对象,只是承诺不可达对象最终会在后续的垃圾回收周期中被释放掉,因此,何时进行 GC,是由 JVM 根据当前 JVM 的上下文来决定的(GC 的不确定性)。例如:在执行 GC 前,垃圾回收器首先会检查当前的 JVM 是否是在一个恰当的时机,即所有的应用程序活动线程都处于安全点(safe point)。例如,当 JVM 为对象分配内存时,或正在优化 CPU 指令时,就不是恰当的时机,因为 JVM 可能会丢失上下文信息,从而引起混乱的结果。所以当在 Java 程序中显示的调用 System.gc() 方法时,可能会触发 GC,也可能不触发。JVM 并不保证 GC 肯定会被立即执行,相反调用 System.gc() 会显著地影响系统性能,应避免使用。

基本算法

垃圾回收的算法基本包括两大类,一类是引用计数(reference counting),该算法的主要的问题是无法处理循环引用,另一类是引用追踪(reference tracing),这类算法可以处理循环引用,但在标记(mark)阶段,JVM 需要暂停程序(stop-the-world),其基本的算法有:

  • 标记-清除(mark-clean)
  • 标记-复制(makr-copying)
  • 标记-清除-压缩(mark-clean-compact)

基本步骤

  1. 任何新对象需要的空间都会在 Eden 空间被分配,并且在 JVM 初始状态,整个 Survivor 空间都是空的
jvm-gc-step1.png
  1. 当 Eden 空间剩余的空间不足以容纳下一个新的对象时,一轮 Minor GC 将会被触发,在 Eden 空间中有引用的对象会被移动到第一个 Survivor 空间(S0),没有引用的对象会被移除
jvm-gc-step2.png
  1. 在接下来的 Minor GC 中,将会重复以上的垃圾回收过程。但是,这时的 Eden 对象会被移动到 S1,同时 S0 的对象也会被移动到 S1,在本轮的 GC 结束后,Eden 和 S0 都会为空,同时存活对象的年龄会增加(survivor 空间始终有一个保持为空)
jvm-gc-step3.png
  1. 接下来的 Minor GC 中,再次重复以上的过程,S0 和 S1 发生交换,这时 Eden 和 S1 空间都是空的
jvm-gc-step4.png
  1. 在重复过 n 次的 Minor GC 后,survivor 空间中对象的年龄达到一定得阀值时(或者 survivor 空间溢出),survior 空间的对象会被移动到老年代
jvm-gc-step5.png
  1. 最后,一轮 Major GC 将会在老年代上发生, 该轮 GC 会清理和压缩对象

垃圾回收器

选择什么样的回收器会和以下两个指标密切相关:

  • 响应(Responsiveness)时间,对程序或服务请求的响应时间
  • 吞吐(Throughout)量,单位时间内可以完成的工作数量或响应数量

The Serial GC

该回收器适用于那些具有客户端特性和对 JVM 暂停时间不敏感的程序,比如单 CUP或嵌入式程序等。该回收器是 JDK 5 以前的 JVM 默认回收器,JDK 5 以后,该回收器是 client(-client)模式下的默认回收器。与该回收器相关的参数:

-XX:+UseSerialGC

The Parallel (Scavenge) GC (Throughput collector)

该回收器的目标是尽可能的增加 JVM 的吞吐能力,即运行用户代码的时间比上 JVM 运行的总时间(包括运行用户代码的时间和 GC 的时间),该回收器使用多线程进行 GC, 默认情况下该回收器的 GC 线程和机器 CPU 数目相等,如果机器是单 CPU,即使设置使用该回收器,JVM 也会忽略。 该回收器通常用于有大量工作需要处理,同时也能接受 JVM 长时间的暂停。例如:科学计算、打印报表或账单、对数据库的大量查询等。该回收器是 server 模式下的默认回收器(JDK 5~8) 与该回收器相关的参数包括:

# 使用多线程对年轻代进行 GC,对老年代仍采用单线程的 GC
-XX:+UseParallelGC 
# 对年轻代和老年代都使用多线程进行 GC,同时使用多线程对
# 老年代进行压缩(compacting)。以 Hotspot 为例,默认情况
# 下,JVM 对年轻代使用 coping,对老年代使用compacting
-XX:+UseParallelOldGC
# 设置 GC 的线程数
-XX:ParallelGCThreads=<desired number>
# 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM 
# 会自动调整年轻代大小,以满足此值。
-XX:MaxGCPauseMillis=<n> 
# 设置 GC 时间的占比
-XX:GCTimeRatio=<n> 
# 设置并行收集器自动选择年轻代区大小和相应的 Survivor 区比例,
# 以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用
# 并行收集器时一直打开
-XX:+UseAdaptiveSizePolicy    

The Concurrent Mark Sweep (CMS) Collector (The concurrent low pause collector)

CMS 与后面的 G1 回收器都允许 GC 线程(collector)与应用线程(mutator)同时(并行)运行。CMS 的目标是尽可能的减少 JVM 的暂停时间,从而尽量的减少 GC 对应用程序的影响。 一般来说,该回收器是针对老年代的垃圾回收器,对年轻代其使用 ParNew 收集器(-XX:+UseParNewGC),其不会对老年代进行复制或压缩仅是标记-清理,因此,该回收器最大的问题是可能会产生 内存碎片,从而导致 OOM 异常,解决该问题的唯一方案就是增大堆的大小。该回收器适用于对暂停时间敏感并可和 GC 共享资源的应用,例如: 桌面的应用程(desktop UI application),web 服务,或者是要对数据查询进行响应的程序。 该回收器的阶段包括

  1. 初始标记阶段(安全点,STW),在老年代中标记可达对象
  2. 并发标记,GC 线程与应用线程一起运行,GC 线程遍历老年代中可达的对象更新其状态,如果有新分配的对象(包括被提升的对象)都会立即被标记为可达
  3. 重新标记(安全点,STW),同步那些在并发标记阶段完成之后由于应用线程更新导致错过的对象
  4. 并发清除,GC 线程与应用线程一起运行,GC 线程清除不可达的对象(碎片)
  5. 重新设定,清理数据结构准备下一次的并发收集

CMS 是一个备选方案,其在设计上值得称道,但它不是万能的,所以在确定 CMS 是正确的垃圾收集策略之前,首先应该确认 Parallel Old 的 STW 停顿确实不能接受,而且已经无法调校。仔细选择 CMS 的原因在于:CMS 使用并发标记,即其允许应用线程和 GC 线程同时运行,但这是有代价的,允许应用线程与 GC线程一起运行,不可避免地会带来一个问题(G1 也会面对同样的问题):GC 线程在标记对象的过程中应用线程可能正在改变对象的引用关系图,从而造成漏标和错标,错标不会影响程序的正确性,只是造成所谓的浮动垃圾(float garbage),但漏标则会导致可达对象被当做垃圾收集掉,从而影响程序的正确性。这种情况必须得以处理,因此 CMS 实际上有两个 stop-the-world 阶段(两次标记):

  • 必须将所有应用线程带到安全点,每次Full GC期间会停顿两次;
  • 尽管垃圾收集与应用同时执行,但应用的吞吐量会降低(通常是50%);
  • 在使用 CMS 进行垃圾收集时,JVM 所用的簿记信息(和CPU周期)远高于其他的并行收集器。

与该回收器相关的参数包括:

# 使用 CMS 并且指定 GC 的线程数
-XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=<n>
# 设置运行多少次 GC 以后对内存空间进行压缩整理(默认 
# JVM 不会压缩整理内存)
-XX:CMSFullGCsBeforeCompaction
# 打开对年老代的压缩。虽然可以消除碎片,但可能会影响
# 性能,不建议采用,还是应该加大堆的大小
-XX:+UseCMSCompactAtFullCollection 

The G1 (Garbage-first) Collector

该回收器主要用于那些具有服务器特性的应用(多 CPU,大内存)。其目标是通过尽可能的减少 JVM 暂停时间和避免(减少)内存分配产生的碎片来取代 CMS。该回收器是在 JDK 9+ 中的默认垃圾收集器(JEP 248),其目标包括:

  • 能使 GC 线程与应用程序线程并发执行
  • 能更快的整理(Compact)空间,不需要长时间的 GC 暂停
  • 能更好的预测 GC 停顿时间
  • 不希望牺牲 JVM 的吞吐量
  • 不需要更大的堆

其实现的特点包括:

  • 不再固定堆中各代的大小,而是将堆划分为大小相等的 Regions,并且相同的代之间不要求连续
  • 增加审计(accounting)数据(RSet 和 CSet),以空间换时间
  • 采用更快的标记算法 SATB(snapshot-at-the-beginning)
  • 使用暂停预测模型(Pause Prediction Model)满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择有限的收集区域数

总结起来,G1 不是消除 Stop The World (无论 G1 的 Young GC 还是 Mixed GC 都是需要 Stop The World 的)而是使每次 Stop The World 的时间尽量可控,尽可能的符合用户期望

相关参数:

# 启用 G1
-XX:+UseG1GC
# 设置 G1 收集过程目标(预期)时间,默认值 200 ms
-XX:MaxGCPauseMillis 
# 设置触发标记周期的 Java 堆占用率阈值。默认值是 45%,
# 这里的堆占比指的是 non_young_capacity_bytes,包括 
# old+humongous
-XX:InitiatingHeapOccupancyPercent
# 设置Region大小,并非最终值
-XX:G1HeapRegionSize=n
# 新生代最小值,默认值 5%
-XX:G1NewSizePercent
# 新生代最大值,默认值 60%
-XX:G1MaxNewSizePercent
# stop-the-world 期间,并行 GC 线程数
-XX:ParallelGCThreads
# 并发标记阶段,并行执行的线程数
-XX:ConcGCThreads=n

G1 需要暂停来拷贝对象,而 CMS 在暂停中只需要标记(mark)对象,那算法上 G1 的暂停时间会比CMS短么?其实 CMS 在较小的堆、合适的 workload 的条件下暂停时间可以很轻松的短于 G1。在 2011 年的时候 Ramki 告诉我堆大小的分水岭大概在 10 GB ~ 15 GB 左右:以下的-Xmx更适合 CMS,以上的才适合试用 G1。现在到了 2014 年,G1 的实现经过一定调优,大概在 6 GB ~ 8 GB 也可以跟 CMS 有一比,我之前见过有在 -Xmx4g 的环境里 G1 比 CMS 的暂停时间更短的案例。
合适的 workload:CMS最严重的暂停通常发生在 remark 阶段,因为它要扫描整个根集合,其中包括整个 young gen。如果在 CMS 的并发标记阶段,mutator 仍然在高速分配内存使得 young gen 里有很多对象的话,那 remark 阶段就可能会有很长时间的暂停。Young gen 越大,CMS remark 暂停时间就有可能越长。所以这是不适合 CMS 的 workload。相反,如果 mutator 的分配速率比较温和,然后给足时间让并发的 precleaning 做好 remark 的前期工作,这样 CMS 就只需要较短的 remark 暂停,这种条件下 G1 的暂停时间很难低于 CMS。

Z Garbage Collector (Java11, Experimental)

该回收器在 Java 11 中引入,其主要目标包括:

  • 暂停时间不超过 10ms
  • 暂停时间不会由于堆大小的变化而改变
  • 可支持的堆大小从百兆(MB)级到 TB 级
  • 简单的调优参数,只需要调整堆大小和并发的 GC 线程数

相关参数:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
-Xmx<size> 
-XX:ConcGCThreads=<n>
-Xlog:gc

Epsilon: A No-Op Garbage Collector (Experimental)

该回收器在 Java 11 中引入,该回收器不进行垃圾回收,主要设计目标是用于性能测试

调优

一个有用的经验法则,200 毫秒或低于 200 毫秒 的 Stop-The-World(STW)通常是没有影响的。实际上在处理一系列现实中的性能问题时,人们发现算法设计是根本问题的几率不足 10%。相反,与算法相比,垃圾收集、数据库访问和配置错误导致应用程序缓慢的可能性更大。当评估是否真的有必要加入缓存时,应该先计划收集一些基本的使用统计信息(比如命中率和未命中率等),以此证明缓存层带来的真正价值。

当对 JVM 进行调优的时候首先要准备合适的环境,包括

  • 使用与生产系统一样的系统
  • 使用真实的数据,对数据进行测量并且多次运行测试数据

然后主要是加快对新对象的内存分配与减少 Stop The World 的停顿时间,比如调整 GC 收集器、增大堆空间、调整 Eden 和 Survivor 的空间、或设置对象存活的最大年龄等

当应用陷入困境,并且怀疑是 GC 的问题时,很多应用团队的反应就是增加堆的大小。在某些情况下,这样做可以快速见效,而且为我们留出了时间来考虑更周详的解决方案。然而,如果没有充分理解性能问题的原因,这种策略反而会让事情变得更糟糕。考虑一个编码非常糟糕的应用程序,它正在产生很多领域对象 (它们的生存时间很有代表性,比如说是 2-3 秒)。如果分配率高到一定程度,垃圾收集会频繁进行,这样领域对象会被提升到老年代。领域对象几乎是一进入年老代,生存时间就结束了,从而直接死亡,但它们直到下一次 Full GC 时才会被回收。如果增加了应用的堆大小,我们所做的不过是增加了相对短命的对象进入和死亡所用的空间。这会导致 Stop-The-World 停顿时间更长,对应用并无益处。在修改堆大小或者调校其他参数之前,理解对象的分配和生存时间的动态是很有必要的。没有测量性能数据就盲目行动,只会使情况更糟糕。在这里,垃圾收集器的老年代分布情况特别重要。

记录与分析 GC 日志数据

GC 的日志数据可以使用 jstat 命令观测或使用 JVM 参数 -Xloggc:<log_file_path> -verbose:gc <params> 打开 GC 日志,例如:-Xloggc:/home/test/gc.log -verbose:gc -XX:+PrintGCDateStamps 其中 -verbose:gc 的可用参数包括:

-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution # 显示JVM所使用的将对象提升进入老年代的年龄阈值
-XX:+HeapDumpOnOutOfMemoryError # 生成堆内存转储文件, 例如: java_xxx.hprof

得到 GC 的日志数据之后,应该主要从以下几个方面分析

  • GC 的类型
  • GC 发生的时间
  • GC 运行的时间
  • GC 各代的比率,如:分配率(Allocation Rate),年轻代的大小除以年轻代的收集时间、提升率(Promotion Rate),老年代随时间变化的使用率(不包括收集时间)和存活率(The Survivor Death Ratio),年龄是 N 的幸存者大小除以年龄是 N-1 的岁幸存者大小
  • GC 引起的 Stop The World 的时间
  • GC 的结果(释放了多少内存)

生成与分析 dump 文件

Warning:无论是生成 heap dump 还是 thread dump,都会造成 JVM 的停顿,因此需要再生产环境上谨慎执行

生成与分析 dump 文件也是常用来分析 JVM 运行的一种手段,JVM 的 dump 文件包括两类

  • Thread Dump(文本格式),保存某一时刻 JVM 中各线程的运行快照,用于分析死锁、资源竞争等线程共享的问题
  • Heap Dump(二进制格式),保存某一时刻 JVM 堆(heap)中对象使用内存的情况,用于分析内存泄露等于内存相关的问题

如果要产生 heap dump 文件可以使用 jmap -dump:live,format=b,file=heap-dump-file pid 命令产生,其中 live 选项会触发一次 Full GC 只 dump 存活对象,如果不加该选项可以得到历史对象。而要产生 thread dump 文件则可以使用 jstack [options] pid > thread-dump-file(推荐)或 kill -3 向 JVM 发送 SIGQUIT 信号,JVM 接收到该信号后打印线程栈到标准输出中,产生 dump 文件。

OpenJDK 使用 jstack 会有问题,可能需要安装:sudo yum --enablerepo='*-debug*' install java-1.6.0-openjdk-debuginfo.x86_64

产生 dump 文件后,可以使用 jvisualvmEclipse Memory Analyzer (MAT)IBM Thread and Monitor Dump Analyzer for Java 进行分析

其它工具

JVM 本身提供了许多有用的工具用来调试 Java 程序,这些程序可以在 $JDK_HOME/bin 下找到, 如:

  • jps 查找 Java 进程
  • jinfo 打印 Java 进程的相关配置信息(参数)
  • jmc(Java Mission Control)JMC 采用采样技术而不是传统的代码植入的技术,其对应用性能的影响非常小,可以用来实时监控、分析 Java 程序

其它的开源工具:

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,413评论 3 83
  • Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进来,墙里面的人想出来。 对象...
    胡二囧阅读 1,002评论 0 4
  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,878评论 2 31
  • 转载blog.csdn.net/ning109314/article/details/10411495/ JVM工...
    forever_smile阅读 5,291评论 1 56
  • Version:1.0 StartHTML:000000214 EndHTML:000030235 StartFr...
    小七奇奇阅读 613评论 0 0