java内存区域
运行时数据区域
- 程序计数器
当前线程私有,是当前线程所执行的字节码的行号指示器
- java虚拟机栈
当前线程私有,每个方法被执行时的栈帧,用于储存局部变量表、操作数栈,动态连接,方法出口等信息动态连接作用就是为了将这些方法符号引用转换为调用方法的直接引用,因为有的对象调用方法时动态生成的,所以在编译器不能确定要执行的符号,从而需要在运行时,去方法区动态的找。
- 本地方法栈
虚拟机使用本地方法
- Java heap
对象实例存放的地方
- 方法区
它是各个线程共享的内存区域,存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),JDK7中将永久代的字符串常量、静态变量移至堆内存,元空间直接在本地内存分配,而不是在虚拟机中进行分配。
使用永久代会导致如果类及方法信息如果增加,则永久代的大小就不好设置,从而可能会导致OOM。现在使用metaSpace,是将这些内容直接放在本地内存,从而可以方便扩展,且很少进行垃圾回收
为什么要使用元空间取代永久代的实现?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- 将 HotSpot 与 JRockit 合二为一。
- 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行期间将新的常量放入池中,比如String的intern。
- 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是《java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁的使用,也可能导致OOM。
比如NIO,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。避免在Java堆和Native堆中来回复制数据。
- OutOfMemory
-
堆内存异常
-
虚拟机栈异常
-
本地方法栈
public void stackLeak() { stackLeak(); }
-
方法区和运行时常量池溢出
String::intern 是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用,否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
Java里面的String对象到底神奇在什么地方- 运行时常量池溢出
int i = 0; List<String> strList = new ArrayList<>(); while(true){ strList.add(String.valueOf(i++).intern()); }
- 方法区溢出
使用动态代理,比如CGLib直接操作字节码运行时生成大量的动态类,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存,public class RuntimeConstantPoolOOM { static class OOMObject { } public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o, args); } }); enhancer.create(); } } }
在经常运行时生成大量动态类的应用场景中,应该特别关注这些类的回收状况。这类场景除了之前提到的程序使用CGLib字节码增强和动态语言外,常见的还有:大量JSP或动态产生JSP文件的应用、基于OSGi的应用,即使是同一个文件,被不同的加载器加载也会视为不同的类。
- 可以设定元空间最大值: -XX:MaxMetaspaceSize
- 设定元空间初始空间大小: -XX:MetaspaceSize
以上代码虽然创建的临时对象应该被回收,但是通过heap dump发现,每个class都是soft Reference,从而即使是full gc,也不会回收它们。
shallow size and retained size
- Dominator by retain size
这里有个对Dominator tree更加直观的描述支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,它有以下基本性质:
- 对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(retained set),即深堆。
- 如果对象A支配对象B,那么对象A的直接支配者也支配对象B。
- 支配树的边与对象引用图的边不直接对应。
如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B,因此对象C的直接支配者也是根对象。对象F与对象 D相互引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象c,即使是从对象F到对象D的引用,从根节点出发,也是经过对象c的,所以,对象D的直接支配者为对象C。
- 本机直接内存溢出
直接内存的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值一致。
private static final int _1MB = 1024 * 1024;
public static void directMemoryOOM() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,这有可能就是直接内存溢出的原因。
垃圾收集器与内存分配策略
- 可达性分析算法
在Java技术体系里,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈中引用的对象,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量
public class Test {
}
private void method() {
Integer temp = new Test();
temp = null;
}
temp 作为临时变量,引用了new Test(), 从而不会被回收,当temp = null 时,原来的new Test()就会被回收
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
public class Test {
p rivate static Test s;
public static void main(String[] args) {
Test temp = new Test();
temp.s = new Test();
temp = null;
}
}
s 作为类的静态属性,被赋值new Test();从而这个new Test()不会被回收,但是这个temp作为临时变量,则它之前被赋值的new test()就会被回收
- 在方法区中常量引用的对象,譬如字符串常量池里的引用(字符串常量池)
public static void main(String[] args) { String s1 = "abc"; String s2 = "abc"; String s3 = "xxx"; }
- 在本地方法栈中JNI引用的对象
所谓本地方法就是一个 java 调用非 java 代码的接口,该方法并非 Java 实现的,可能由 C 或 Python等其他语言实现的, Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法,还是不明白?见文末参考,对本地方法定义与使用有详细介绍。
当调用 Java 方法时,虚拟机会创建一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointerException、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。譬如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节,而不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。(GC root)
引用(直观理解)
- 强应用:只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 弱引用:只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
- 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
分代收集理论
当前的垃圾收集器,大多数都遵循了“分代收集”理论。主要是建立两个分代假说之上
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
这两个分代假说共同奠定了多款常用的垃圾收集器的一致设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储,显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
基础的垃圾回收器
-
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
缺点:- 执行效率不稳定,如果堆中大部分是需要被回收的,则会导致标记和清除两个过程的执行效率都随对象数量增长而降低
- 内存空间碎片问题
-
标记复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,标记-复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
IBM有一项研究表明,新生代中的对象有98%熬不过第一轮收集,因此不需要按照1:1的比例来划分,Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,HotSpot设定的比例是8:2,每次分配内存只使用Eden和其中一块Survivor空间。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。缺点:
- 如果大量对象存活,则会导致大量的内存复制开销
- 内存利用率只有一半
-
标记-整理算法
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。标记-整理算法是让所有存活对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
缺点:- 当有大量存活对象时,则移动对象就会降低回收效率
- 当有大量存活对象时,则移动对象就会降低回收效率
垃圾收集器
-
Serial收集器
单线程工作的收集器,它"单线程"的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
虽然是很早出现的收集器,但是有着优于其他收集器的地方,那就是简单高效,对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 -
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配策略、回收策略等都与Serial收集器完全一致。
-
Parallel Scavenge 收集器
Parallel Scavenge收集器又称为吞吐量优先收集器,和ParNew收集器类似,是一个新生代收集器。同样是使用标记-复制算法的并行多线程收集器。它的目标是达到一个可控制的吞吐量。吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:
$$吞吐量=\cfrac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间}$$
高吞吐量就可以最高效率地利用处理器资源,尽快完成程序的运算任务。有一些参数可以控制GC停顿的时间:-XX:MaxGCPauseMills,还可以通过-XX:GCTimeRatio来控制GC消耗时间所占的比例,从而来控制吞吐量,此外还可以通过-XX:+UseAdaptiveSizePolicy,让JVM来动态调整。但是过分的设置高吞吐量,可能会导致新生代频繁的回收,新生代内存变小,来降至提高吞吐量。 -
Serial Old收集器
Serial Old收集器是Serial收集器的老年版本 -
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。
-
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。整个过程分为四个步骤:- 初始标记
- 并发标记
- 重新标记
- 并发清除
由于是和用户线程一起并发执行,从而会降低GC的停顿时间。但是由于是和用户线程并发执行,从而会导致占用一部分线程而导致应用程序变慢,降低总吞吐量。如果处理器核心不足,则还需要分出算力给收集器线程去执行。并且CMS会进行标记清理,造成垃圾碎片,于是就无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
-
Garbage First收集器(G1)
G1开创的基于Region的堆内存布局能够控制消耗在垃圾收集器上的时间。G1不再根据分代收集,要么是整个新生代,要么是这个老年代,再要么就是整个Java堆(Full GC),G1可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收受益最大,这就是G1收集器的Mixed GC模式。G1把连续的堆划分为多个大小相等的region,每一个region可以根据需要扮演不同的空间,如Eden、Survivor、老年代,以及存储大对象的Humongous区域。
用户可以指定期望的停顿时间,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率,而不追求一次把整个Java堆全部清理干净。
G1是基于标记整理算法的,从而不会产生内存碎片,但无论是内存占用,还是运行时的额外执行负载都要比CMS要高。 -
低延迟收集器
在内存占用,吞吐量和延迟三项指标里,延迟的重要性日益凸显,随着硬件性能的提升,吞吐量会变高,内存会逐渐扩大,从而回收大内存就会耗费更多的时间。以低延迟出发的收集器:Shenandoah、ZGC收集器。
GC 日志分析
- GC 日志参数:
- -verbose:gc :输出gc日志信息,默认输出到标准输出
- -XX:+PrintGC :等同于-verbose:gc表示打开简化的GC日志
- -XX:+PrintGCDetails :在发生垃圾回收时打印内存回收详细的日志并在进程退出- 时输出当前内存各区域分配情况
- -XX:+PrintGCTimeStamps :输出GC发生时的时间戳
- -XX:+PrintGCDateStamps :输出GC发生时的时间戳(以日期的形式,如- 2021-10-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC :每一次GC前和GC后,都打印堆信息
- -Xloggc:
:把GC日志写入到一个文件中去,而不是打印到标准输出中 - -XX:+TraceClassLoading : 监控类的加载
- -XX:+PrintGCApplicationStoppedTime: 打印GC时线程的停顿时间
- -XX:+PrintGCApplicationConcurrentTime : 垃圾收集之前打印出应用未中- 断的执行时间
- -XX:+PrintReferenceGC : 记录回收了多少种不同引用类型的引用
- -XX:+PrintTenuringDistribution : 让JVM在每次MinorGC后打印出当前使用- 的Survivor中对象的年龄分布
- -XX:+UseGCLogFileRotation : 启用GC日志文件的自动转储
- -XX:NumberOfGClogFiles=1 : GC日志文件的循环数目
- -XX:GCLogFileSize=1M : 控制GC日志文件的大小
- GC 日志分类
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC / Young GC):只是新生代(Eden\S0,S1)的垃圾收集
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
- 目前,只有CMS GC会有单独收集老年代的行为。
- 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC会有这种行为
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
GC日志格式的规律一般都是:
- GC前内存占用->GC后内存占用(该区域内存总大小)
- [PSYoungGen: 5986K->696K(8704K)] 5986K->704K(9216K)
- 中括号内:GC回收前年轻代堆大小,回收后大小,(年轻代堆总大小)
- 括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)
----------------------Minor GC----------------------------
2021-09-09T14:44:04.813+0800: 0.163:
[GC (Allocation Failure) 2021-09-09T14:44:04.813+0800: 0.163:
[DefNew: 139776K->17472K(157248K), 0.0164545 secs] 139776K->45787K(506816K), 0.0165501 secs]
[Times: user=0.00 sys=0.02, real=0.02 secs] 2021-09-09T14:44:04.853+0800: 0.203:
[GC (Allocation Failure) 2021-09-09T14:44:04.853+0800: 0.203:
[DefNew: 157248K->17471K(157248K), 0.0192998 secs] 185563K->84401K(506816K), 0.0193485 secs]
[Times: user=0.02 sys=0.01, real=0.02 secs]
-----------------------Full GC----------------------------
2021-09-09T14:44:05.240+0800: 0.589:
[GC (Allocation Failure) 2021-09-09T14:44:05.240+0800: 0.589:
[DefNew: 157148K->157148K(157248K), 0.0000289 secs]2021-09-09T14:44:05.240+0800: 0.589:
[Tenured: 341758K->308956K(349568K), 0.0459961 secs] 498907K->308956K(506816K),
[Metaspace: 2608K->2608K(1056768K)], 0.0460956 secs]
[Times: user=0.05 sys=0.00, real=0.05secs]
2021-09-09T14:44:04.813-GC事件开始的时间点。+0800表示当前时区为东八区,这只是一个标识。0.163是GC事件相对于JVM启动时间的间隔,单位是秒
GC - 用来区分Minor GC还是Full GC的标志。GC表明这是一次小型GC(Minor GC),即年轻代GC。Allocation Failure 表示触发GC的原因。本次GC事件是由于对象分配失败,即年轻代中没有空间来存放新生成的对象引起的。
DefNew表示垃圾收集器的名称。这个名称表示:年轻到使用的单线程、标记-复制、STW的垃圾收集器。139776K->17472K 表示在垃圾收集之前和之后的年轻代使用量。
(157248K) 表示年轻代的总空间大小。分析可得,GC之后年轻代使用率为11%。
139776K->45787K 表示在垃圾收集前后整个堆内存的使用情况,(506816K)表示整个堆的大小
0.0165501 secs - GC事件的持续时间,单位:秒
[Times: user=0.00 sys=0.02, real=0.02 secs] 表示此次GC事件的持续时间,通过三个部分来衡量:
- user:进程执行用户态代码所耗费的处理器时间
- sys: 进程执行核心态代码所耗费的时间
- real: 执行动作从开始到结束耗费的时钟时间
前面两个是处理器时间,real代表的是时钟时间,它们的区别是处理器时间代表的是线程占用处理器一个核心的耗时计数器,而时钟时间就是现实世界中的时间计数。如果是单核单线程的场景下,这两者可以认为是等价的,但如果是多喝环境下,同一个时钟时间内有多少处理器核心正在工作,就会有多少倍的处理器时间被消耗和记录下来。
虚拟机性能监控和故障处理工具
- Jps
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID。
基本语法为:jps [options] [hostid]- -q:仅仅显示LVMID (local virtual machine id),即本地虚拟机唯一id。不显示主类的名称等
- -1:输出应用程序主类的全类名或如果进程执行的是jar包,则输出jar完整路径
- -m:输出虚拟机进程启动时传递给主类main()的参数
- -v:列出虚拟机进程启动时的JVM参数。比如: -Xms20m -Xmx50m是启动程序指定的jvm参数。
- 说明:以上参数可以综合使用。
- 虚拟机统计信息监视工具
jstat(VM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
-class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等- -t :前面显示Timestamp一列
- -h3 :每隔三次输出标题
- 9000 :进程号
- 1000 :每隔1000毫米输出一次
- 10 :一共输出10次
垃圾回收相关的:
- -gc:显示与GC相关的堆信息。包括Eden区、两个Survivor区、老年代、永久代等的容量、己用空间、GC时间合计等信息。
- -gccapacity:显示内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间。
- -gcutil:显示内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比。
- -gccause:与-gcutil功能一样,但是会额外输出导致最后一次或当前正在发生的GC产生的原因。
- -gcnew:显示新生代Gc状况
- -gcnewcapacity:显示内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
- -geold:显示老年代GC状况
- -gcoldcapacity:显示内容与-gcold基本相同,输出主要关注使用到的最大、最小空间
- -gcpermcapacity:显示永久代使用到的最大、最小空间。
- Jmap :java内存映像工具
jmap(JVM Memory Map):作用一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。- jmap [option]
- jmap [option] <executable
- jmap [option] [server_id@]
- jmap [option]
- -dump: 生成Java堆转储快照: dump文件
- 特别的: -dump:live只保存堆中的存活对象
- -heap: 输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等
- -histo: 输出堆中对象的统计信息,包括类、实例数量和合计容量
- 特别的:-histo:live只统计堆中的存活对象
- -permstat: 以ClassLoader为统计口径输出永久代的内存状态信息
- 仅linux/solaris平台有效
- -finalizerinfo: 显示在F-Queue中等待Finalizer线程执行finalize方法的对象
- 仅linux/solaris平台有效
- -F: 当虚拟机进程对-dump选项没有任何响应时,可使用此选项强制执行生成dump文件
- 仅linux/solaris平台有效
- -h |-help: jmap工具使用的帮助命令
- -J
: 传递参数给jmap启动的jvm
- Jstack: java 堆栈跟踪工具
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。
option参数:
- -F:当正常输出的请求不被响应时,强制输出线程堆栈
- -m:除堆栈外,显示关于锁的附加信息
- -h:如果调用到本地方法的话,可以显示C/C++的堆栈
- Jcmd
它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能。比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等
可视化故障处理工具
- JConsole
- VisualVM
Monitor 显示CPU、Classes、Heap、Metaspace、Threads的Usage
对进程进行heap dump进行分析
dump的结果可以看到class实例的个数,size,每个instance的size,以及分析从父节点开始的支配树 size,来分析引用关系
- mat
对Object的引用关系分析更加准确 - Jprofile
判断是不是 GC 引发的问题?
到底是结果(现象)还是原因,在一次 GC 问题处理的过程中,如何判断是 GC 导致的故障,还是系统本身引发 GC 问题。这里继续拿在本文开头提到的一个 Case:“GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高等四个表象,如何判断哪个是根因?”,笔者这里根据自己的经验大致整理了四种判断方法供参考:
- 时序分析: 先发生的事件是根因的概率更大,通过监控手段分析各个指标的异常时间点,还原事件时间线,如先观察到 CPU 负载高(要有足够的时间 Gap),那么整个问题影响链就可能是:CPU 负载高 -> 慢查询增多 -> GC 耗时增大 -> 线程Block增多 -> RT 上涨。
- 概率分析: 使用统计概率学,结合历史问题的经验进行推断,由近到远按类型分析,如过往慢查的问题比较多,那么整个问题影响链就可能是:慢查询增多 -> GC 耗时增大 -> CPU 负载高 -> 线程 Block 增多 -> RT上涨。
- 实验分析: 通过故障演练等方式对问题现场进行模拟,触发其中部分条件(一个或多个),观察是否会发生问题,如只触发线程 Block 就会发生问题,那么整个问题影响链就可能是:线程Block增多 -> CPU 负载高 -> 慢查询增多 -> GC 耗时增大 -> RT 上涨。
- 反证分析: 对其中某一表象进行反证分析,即判断表象的发不发生跟结果是否有相关性,例如我们从整个集群的角度观察到某些节点慢查和 CPU 都正常,但也出了问题,那么整个问题影响链就可能是:GC 耗时增大 -> 线程 Block 增多 -> RT 上涨。
不同的根因,后续的分析方法是完全不同的。如果是 CPU 负载高那可能需要用火焰图看下热点、如果是慢查询增多那可能需要看下 DB 情况、如果是线程 Block 引起那可能需要看下锁竞争的情况,最后如果各个表象证明都没有问题,那可能 GC 确实存在问题,可以继续分析 GC 问题了。
总结:
我们可以根据进程的内存使用特点,结合实际场景考虑吞吐量,延迟性,内存使用情况,配合参数来进行JVM的调优,同时在出现频繁GC,GC耗时增加,内存使用猛增等情况,我们可以使用一些命令:Jmap、Jcmd,dump整个堆内存以及Visiualize的工具来分析jvm的情况。
在分析是不是GC导致应用出现问题时,要注意时序分析,不要只看GC异常,有可能更早的是CPU等出现了问题,从而引发GC异常,如今项目上可能使用了k8s这些容器技术,那么jvm的控制还需要结合k8s的参数来综合考虑问题。
Reference: