垃圾回收与检查工具

java内存区域

  运行时数据区域

  1. 程序计数器

  当前线程私有是当前线程所执行的字节码的行号指示器

  1. java虚拟机栈

  当前线程私有每个方法被执行时的栈帧用于储存局部变量表操作数栈动态连接方法出口等信息动态连接作用就是为了将这些方法符号引用转换为调用方法的直接引用因为有的对象调用方法时动态生成的所以在编译器不能确定要执行的符号从而需要在运行时去方法区动态的找

  栈帧内部结构,动态链接及方法的调用

  1. 本地方法栈

  虚拟机使用本地方法

  1. Java heap

  对象实例存放的地方

  1. 方法区

  它是各个线程共享的内存区域存储被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据
  JDK8 之前Hotspot 中方法区的实现是永久代PermJDK8 开始使用元空间MetaspaceJDK7中将永久代的字符串常量静态变量移至堆内存元空间直接在本地内存分配而不是在虚拟机中进行分配
  使用永久代会导致如果类及方法信息如果增加则永久代的大小就不好设置从而可能会导致OOM现在使用metaSpace是将这些内容直接放在本地内存从而可以方便扩展且很少进行垃圾回收
为什么要使用元空间取代永久代的实现

  • 字符串存在永久代中容易出现性能问题和内存溢出
  • 类及方法的信息等比较难确定其大小因此对于永久代的大小指定比较困难太小容易出现永久代溢出太大则容易导致老年代溢出
  • 永久代会为 GC 带来不必要的复杂度并且回收效率偏低
  • 将 HotSpot 与 JRockit 合二为一
  1. 运行时常量池

  运行时常量池是方法区的一部分Class 文件中除了有类的版本字段方法接口等描述信息外还有一项信息是常量池Constant Pool Table用于存放编译期生成的各种字面量和符号引用这部分内容将在类加载后进入方法区的运行时常量池中存放一般来说除了保存 Class 文件中描述的符号引用外还会把翻译出来的直接引用也存储在运行时常量池中运行期间将新的常量放入池中比如String的intern

  1. 直接内存

  直接内存并不是虚拟机运行时数据区的一部分也不是java虚拟机规范中定义的内存区域但是这部分内存也被频繁的使用也可能导致OOM
  比如NIO一种基于通道(Channel)与缓冲区(Buffer)的I/O方式它可以使用Native函数库直接分配堆外内存然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作避免在Java堆和Native堆中来回复制数据

  1. 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 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文件中不会看见有什么明显的异常情况这有可能就是直接内存溢出的原因

垃圾收集器与内存分配策略

  1. 可达性分析算法

在Java技术体系里固定可作为GC Roots的对象包括以下几种

  • 在虚拟机栈中引用的对象譬如当前正在运行的方法所使用到的参数局部变量临时变量
public class Test {

}

private void method() {
    Integer temp = new Test();
    temp = null;
}

temp 作为临时变量<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>引用了new Test(), 从而不会被回收<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>当temp = null 时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>原来的new Test()就会被回收
- 在方法区中类静态属性引用的对象<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>譬如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 作为类的静态属性<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>被赋值new Test();从而这个new Test()不会被回收<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>但是这个temp作为临时变量<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>则它之前被赋值的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对象一些常驻的异常对象(比如NullPointerExceptionOutOfMemoryError)等还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBeanJVMTI中注册的回调本地代码缓存等

  除了这些固定的GC Roots集合以外根据用户所选用的垃圾收集器以及当前回收的内存区域不同还可以有其他对象临时性地加入共同构成完整GC Roots集合譬如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集)必须考虑到内存区域是虚拟机自己的实现细节而不是孤立封闭的所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去才能保证可达性分析的正确性(GC root)

引用(直观理解)

  • 强应用只要强引用关系还在垃圾收集器就永远不会回收掉被引用的对象
  • 软引用只被软引用关联着的对象在系统将要发生内存溢出异常前会把这些对象列进回收范围之中进行第二次回收如果这次回收还没有足够的内存才会抛出内存溢出异常
  • 弱引用只被弱引用关联的对象只能生存到下一次垃圾收集发生为止
  • 虚引用一个对象是否有虚引用的存在完全不会对其生存时间构成影响也无法通过虚引用来取得一个对象实例为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

分代收集理论

当前的垃圾收集器大多数都遵循了分代收集理论主要是建立两个分代假说之上

  • 弱分代假说绝大多数对象都是朝生夕灭的
  • 强分代假说熬过越多次垃圾收集过程的对象就越难以消亡

  这两个分代假说共同奠定了多款常用的垃圾收集器的一致设计原则收集器应该将Java堆划分出不同的区域然后将回收对象依据其年龄分配到不同的区域之中存储显而易见如果一个区域中大多数对象都是朝生夕灭难以熬过垃圾收集过程的话那么把它们集中放在一起每次回收只关注如何保留少量存活而不是去标记那些大量将要被回收的对象就能以较低代价回收到大量的空间如果剩下的都是难以消亡的对象那把它们集中放在一块虚拟机便可以使用较低频率来回收这个区域这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用

基础的垃圾回收器

  1. 标记-清除算法
     首先标记出所有需要回收的对象在标记完成后统一回收掉所有被标记的对象
    缺点

    • 执行效率不稳定如果堆中大部分是需要被回收的则会导致标记和清除两个过程的执行效率都随对象数量增长而降低
    • 内存空间碎片问题
  2. 标记复制算法
     为了解决标记-清除算法面对大量可回收对象时执行效率低的问题标记-复制算法将可用内存按容量划分为大小相等的两块每次只使用其中的一块当这一块的内存用完了就将还存活着的对象复制到另一块上面然后再把已使用过的内存空间一次清理掉
     IBM有一项研究表明新生代中的对象有98%熬不过第一轮收集因此不需要按照1:1的比例来划分Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间HotSpot设定的比例是8:2每次分配内存只使用Eden和其中一块Survivor空间发生垃圾收集时将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上然后直接清理掉Eden和已用过的那块Survivor空间

    缺点

    • 如果大量对象存活则会导致大量的内存复制开销
    • 内存利用率只有一半
  3. 标记-整理算法
     标记-复制算法在对象存活率较高时就要进行较多的复制操作效率将会降低标记-整理算法是让所有存活对象都向内存空间一端移动然后直接清理掉边界以外的内存
    缺点

    • 当有大量存活对象时则移动对象就会降低回收效率

垃圾收集器

  1. Serial收集器
      单线程工作的收集器它"单线程"的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作更重要的是强调在它进行垃圾收集时必须暂停其他所有工作线程直到它收集结束

      虽然是很早出现的收集器但是有着优于其他收集器的地方那就是简单高效对于内存资源受限的环境它是所有收集器里额外内存消耗最小的对于单核处理器或处理器核心数较少的环境来说Serial收集器由于没有线程交互的开销专心做垃圾收集自然可以获得最高的单线程收集效率

  2. ParNew收集器
      ParNew收集器实质上是Serial收集器的多线程并行版本除了同时使用多条线程进行垃圾收集之外其余的行为包括Serial收集器可用的所有控制参数收集算法Stop The World对象分配策略回收策略等都与Serial收集器完全一致

  3. Parallel Scavenge 收集器
      Parallel Scavenge收集器又称为吞吐量优先收集器和ParNew收集器类似是一个新生代收集器同样是使用标记-复制算法的并行多线程收集器它的目标是达到一个可控制的吞吐量吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:

    $$吞吐量=\cfrac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间}$$

      高吞吐量就可以最高效率地利用处理器资源尽快完成程序的运算任务有一些参数可以控制GC停顿的时间-XX:MaxGCPauseMills还可以通过-XX:GCTimeRatio来控制GC消耗时间所占的比例从而来控制吞吐量此外还可以通过-XX:+UseAdaptiveSizePolicy让JVM来动态调整但是过分的设置高吞吐量可能会导致新生代频繁的回收新生代内存变小来降至提高吞吐量

  4. Serial Old收集器

      Serial Old收集器是Serial收集器的老年版本

  5. Parallel Old收集器
      Parallel Old是Parallel Scavenge收集器的老年代版本支持多线程并行收集基于标记-整理算法实现

  6. CMS收集器
    CMS收集器是一种以获取最短回收停顿时间为目标的收集器整个过程分为四个步骤:

    • 初始标记
    • 并发标记
    • 重新标记
    • 并发清除

  由于是和用户线程一起并发执行从而会降低GC的停顿时间但是由于是和用户线程并发执行从而会导致占用一部分线程而导致应用程序变慢降低总吞吐量如果处理器核心不足则还需要分出算力给收集器线程去执行并且CMS会进行标记清理造成垃圾碎片于是就无法处理浮动垃圾有可能出现Concurrent Mode Failure失败进而导致另一次完全Stop The World的Full GC的产生

  1. Garbage First收集器G1
      G1开创的基于Region的堆内存布局能够控制消耗在垃圾收集器上的时间G1不再根据分代收集要么是整个新生代要么是这个老年代再要么就是整个Java堆(Full GC)G1可以面向堆内存任何部分来组成回收集进行回收衡量标准不再是它属于哪个分代而是哪块内存中存放的垃圾数量最多回收受益最大这就是G1收集器的Mixed GC模式G1把连续的堆划分为多个大小相等的region每一个region可以根据需要扮演不同的空间如EdenSurvivor老年代以及存储大对象的Humongous区域

      用户可以指定期望的停顿时间设置不同的期望停顿时间可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡从G1开始最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率而不追求一次把整个Java堆全部清理干净
      G1是基于标记整理算法的从而不会产生内存碎片但无论是内存占用还是运行时的额外执行负载都要比CMS要高

  2. 低延迟收集器
      在内存占用吞吐量和延迟三项指标里延迟的重要性日益凸显随着硬件性能的提升吞吐量会变高内存会逐渐扩大从而回收大内存就会耗费更多的时间以低延迟出发的收集器ShenandoahZGC收集器

GC 日志分析

  1. 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日志文件的大小
  1. 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堆和方法区的垃圾收集


  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表明这是一次小型GCMinor GC即年轻代GCAllocation 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代表的是时钟时间它们的区别是处理器时间代表的是线程占用处理器一个核心的耗时计数器而时钟时间就是现实世界中的时间计数如果是单核单线程的场景下这两者可以认为是等价的但如果是多喝环境下同一个时钟时间内有多少处理器核心正在工作就会有多少倍的处理器时间被消耗和记录下来

虚拟机性能监控和故障处理工具

  1. Jps
      可以列出正在运行的虚拟机进程并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID
     基本语法为:jps [options] [hostid]
    • -q:仅仅显示LVMID (local virtual machine id)即本地虚拟机唯一id不显示主类的名称等
    • -1:输出应用程序主类的全类名或如果进程执行的是jar包则输出jar完整路径
    • -m:输出虚拟机进程启动时传递给主类main()的参数
    • -v:列出虚拟机进程启动时的JVM参数比如: -Xms20m -Xmx50m是启动程序指定的jvm参数
    • 说明:以上参数可以综合使用
  2. 虚拟机统计信息监视工具
      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:显示永久代使用到的最大最小空间

  1. Jmap java内存映像工具
     jmap(JVM Memory Map):作用一方面是获取dump文件堆转储快照文件二进制文件它还可以获取目标Java进程的内存相关信息包括Java堆各区域的使用情况堆中对象的统计信息类加载信息等
    • jmap [option]
    • jmap [option] <executable
    • jmap [option] [server_id@]
  • -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
  1. Jstack: java 堆栈跟踪工具
      生成线程快照的作用:可用于定位线程出现长时间停顿的原因如线程间死锁死循环请求外部资源导致的长时间等待等问题这些都是导致线程长时间停顿的常见原因当线程出现停顿时就可以用jstack显示各个线程调用的堆栈情况

option参数

  • -F当正常输出的请求不被响应时强制输出线程堆栈
  • -m除堆栈外显示关于锁的附加信息
  • -h如果调用到本地方法的话可以显示C/C++的堆栈
  1. Jcmd
      它是一个多功能的工具可以用来实现前面除了jstat之外所有命令的功能比如:用它来导出堆内存使用查看Java进程导出线程信息执行GCJVM运行时间等

 可视化故障处理工具

  • JConsole
  • VisualVM
    Monitor 显示CPUClassesHeapMetaspaceThreads的Usage

对进程进行heap dump进行分析

dump的结果可以看到class实例的个数size每个instance的size以及分析从父节点开始的支配树 size来分析引用关系

  • mat
      对Object的引用关系分析更加准确
  • Jprofile

判断是不是 GC 引发的问题

  到底是结果现象还是原因在一次 GC 问题处理的过程中如何判断是 GC 导致的故障还是系统本身引发 GC 问题这里继续拿在本文开头提到的一个 CaseGC 耗时增大线程 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的调优同时在出现频繁GCGC耗时增加内存使用猛增等情况我们可以使用一些命令JmapJcmddump整个堆内存以及Visiualize的工具来分析jvm的情况
  在分析是不是GC导致应用出现问题时要注意时序分析不要只看GC异常有可能更早的是CPU等出现了问题从而引发GC异常如今项目上可能使用了k8s这些容器技术那么jvm的控制还需要结合k8s的参数来综合考虑问题

  
  

Reference: