1. jvm简单介绍

jvm可以理解为一个小型计算机运行再linux,window系统中, 它只和操作系统交互, 不与硬件交互
image.png

1.1 java文件如何被执行

对于HelloWorld.java 文件, jvm是无法识别的, 所有需要编译, 变成.class文件. jvm通过执行class文件, 将其装进类加载器(类加载器会将class文件全部搬进jvm中)
image.png

1.2 jvm组成介绍

jvm包含了方法区, 堆(二者是线程共享的), 虚拟机栈, 本地方法栈, 程序计数器(这三位是线程独享的)
image.png

1.3 简单代码执行流程

image.png
1.编译App文件成class文件, 系统启动, jvm将App.class文件加载到运行时数据区的方法区。这个过程叫App类的加载
2.执行main方法, 在jvm中创建一个Student对象, 此时jvm中没有Student.class类信息, 同样加载放到方法区。
加载完类信息, 在堆中为Student实例分配内存, 然后调用构造函数初始化Student实例
3.执行sayName()时候, jvm找到student对象, 根据student对象定位到方法区中student类信息的方法表, 获取sayName()的字节码地址, 执行sayName方法。

2. 类文件 & 类加载流程 & 类加载器

2.1 类文件结构

Class文件由ClassFile文件定义, 其结构如下:

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段属性
    field_info     fields[fields_count];//一个类可以有多个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}

使用jclasslib插件, 可以展示类文件结构
image.png
详细介绍可参考javaGuide: https://javaguide.cn/java/jvm/class-file-structure.html#_2-2-class-%E6%96%87%E4%BB%B6%E7%89%88%E6%9C%AC%E5%8F%B7-minor-major-version

2.2 类加载过程

类从被加载到虚拟机中, 到卸载出内存为止, 整个生命周期包括: 加载,验证, 准备, 解析, 初始化, 使用, 卸载7个阶段。其中验证, 准备, 解析这三个部分统称为连接。
image.png
加载: 通过类的全限定名获取定义此类的二进制文件, ,在方法区创建该类的数据结构, 并在堆中创建Class对象。
image.png
验证: 确保Class文件的字节流包含的信息符合虚拟机的要求…
准备: 为类变量分配内存并设置类变量(static修饰的变量)的初始化值。 为final修饰的类变量设置具体值。
例如: public static int value = 1; 在准备阶段, value的值为0;
public static final int value = 1; 在准备阶段, value的值为1;
基本类型的零值:
image.png
解析: 将常量池内的符号引用替换为直接引用的过程。(直白一点:得到类,字段,方法在内存中的指针或者偏移量)
初始化: 初始化阶段执行初始化方法()方法的过程。(类变量赋值, 静态语句, new对象…)

/**
 * 触发类初始化机制:
 * 1. 创建类的实例话对象(new, 反射, 序列化), 相应的类会触发类初始化.
 * 2. 调用类的静态方法, 静态变量, 或者为静态变量赋值,  相应的类会触发类初始化.
 * 3. 子类初始化的时候, 父类也要初始化.
 * 4. JVM调用含main方法的启动类.
 */
class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;

    public static void say() {
        System.out.println("my value -> " + value);
    }
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init");
        subValue = 456;
    }

    public static int subValue = 987;
}

class TestClassInit {

    public static int initValue = 999;

    public static void main(String[] args) throws Exception {
        /**
         * new对象:
         * 父类new对象: SuperClass superClass = new SuperClass();
         *      SuperClass init!
         */
        //SuperClass superClass = new SuperClass();
        /**
         * 子类初始化,父类也初始化:
         *  SuperClass init!
         *  SubClass init
         */
        //SubClass subClass = new SubClass();

        /**
         * 反射:
         *      SuperClass init!
         *      SubClass init
         */
        //Class<SubClass> subClassClass = SubClass.class;
        //SuperClass subClassClass2 = (SubClass)subClassClass.newInstance();

        /**
         * 调用父类静态方法 & 调用父类静态方法:
         * 对于静态字段,只有直接定义这个字段的类才会被初始化,
         * 因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
         * SubClass.say();
         *      SuperClass init!
         *      my value -> 123
         *
         * System.out.println(SubClass.subValue);
         *      SuperClass init!
         *      123
         */
        //SubClass.say();
        //System.out.println(SubClass.subValue);
        //System.out.println(SubClass.value);

        /**
         * 为静态变量赋值,也会初始化类
         *      SuperClass init!
         */
        //SubClass.value = 1001;

        /**
         * 初始化含main方法的类
         *      999
         */
        //System.out.println(TestClassInit.initValue);
    }
}

2.3 类加载器 & 双亲委托机制

类加载器作用: 读取class文件到内存中, 将其转为Class的实例。类加载器(ClassLoader,说⽩了也是⼀个类,jvm启动的时候先把类加载器读取到内存当中去,其他的类(⽐如各种jar中的字节码⽂件,⾃⼰开发的代码编译之后的.class⽂件等等))

public static void main(String[] args) {
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

    System.out.println(systemClassLoader);  
    // sun.misc.Launcher$AppClassLoader@18b4aac2 
    // 应用程序类加载器,Application ClassLoader,加载ClassPath中的类库

    System.out.println(systemClassLoader.getParent());  
    // sun.misc.Launcher$ExtClassLoader@61bbe9ba
    // 扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类

    System.out.println(systemClassLoader.getParent().getParent()); 
    // null
    // 启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类
    // native本地方法
}

image.png
如上图所示: 双亲委托机制每次收到类加载请求的时候, 先将请求委派给父类加载器去加载完成, 如果父类无法完成加载,子类尝试加载。
好处: 1. 避免同一个类被多次加载。
2.每个加载器只能加载自己范围内的类。保证核⼼class不能被篡改。通过委托⽅式,不会去篡改核⼼.class,即使篡改也不会去加载,即使加载也不会是同⼀个.class对象了。不同的加载器加载同⼀个.class也不是同⼀个.class对象。这样保证了class执⾏安全

ClassLoader类中的核心方法 java.lang.ClassLoader#loadClass(java.lang.String, boolean) 负责加载类.

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先,检查是否已经加载该类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果尚未加载,则遵循父优先的等级加载机制(所谓双亲委派机制)
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // 模板方法模式:如果还是没有加载成功,调用findClass(),尝试自己加载
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

通过重写loadClass方法,可以打破双亲委托机制。

3. java内存区域 & 垃圾回收

3.1 运行时数据区域

image.png
如图为运行时数据区简图, 大概有这么几个部分: 堆,方法区, 虚拟机栈, 本地方法栈, 程序计数器等。在1.8之后, 结构做了微调, 去除堆中的方法区(永久代), 新增元空间

3.1.1 程序计数器(线程私有)

指示我们下一条要执行的代码

3.1.2 虚拟机栈(线程私有)

所有的java方法(非native方法)调用都是通过虚拟机栈来实现, 每一次方法的调用都会有一个栈帧被压入虚拟机栈, 每一次调用结束都会有一个栈帧弹出
栈帧: 局部变量表, 操作数栈, 动态链接, 方法返回地址四部分
image.png
局部变量: 存放各种数据类型, 对象引用。
操作数栈: 存放方法执行过程中产生的中间计算结果。
动态链接: 一个方法调用其他方法, 将符号引用转为为方法的直接引用.(符号引用都存放于运行时变量池)
image.png
PS: 当我们程序出现循环调用, 会出现StackOverFlowError。原因就是函数陷入无限调用, 栈中压入了太多的栈帧, 导致栈空间过深.当超过最大长度, 就会抛出异常。

3.1.3 本地方法栈(线程私有)

所有的native方法调用都是通过本地方法栈来实现。原理同上👆🏻。

3.1.4 堆(线程共享)

堆中存放对象实例。堆可以细分为新生代, 老年代. 再细分为Eden, Survivor, Old等空间. 在1.8之前, 堆中包含老年代(方法区的具体实现)。1.8之后去除永久代,新增元空间, 元空间使用的是直接内存。
image.png

3.1.5 方法区(线程共享)

方法区是一个概念, 里面要存储类的元信息, 常量, 静态变量, 以及JIT代码。在1.8之前, 其具体实现是永久代, 在1.8之后, 是元空间。(抽象接口和具体实现的关系)
image.png
PS: 为什么要将永久代替换成元空间.
永久代使用的还是jvm内存, 而元空间使用的是直接内存, 受机器内存影响, 溢出的几率降低。方法区大小决定了系统可以保存多少个类, 如果定义太多的类, 也会报内存溢出. (java.lang.OutOfMemoryError: PermGen space 或者 java.lang.OutOfMemoryError: Metaspace)

在1.6时候, 方法区实现是永久代, 存放了如下信息:
image.png
image.png

1.8之后, 新增元空间, 并将静态变量和字符串常量移到堆中。
image.png

3.1.6 栈,堆,方法区关系

image.png
image.png

3.2 对象创建过程

对象的创建大概经历如下几个步骤:
image.png

3.2.1 类初始化检查

创建对象的方式很多(new,反射, 克隆, 序列化与反序列化), 这里我们以new举例. 当虚拟机遇到new指令, 首先判断该类是否已经被加载, 解析, 初始化(类加载流程见2.2).如果没有, 首先要进行加载,解析, 初始化.

3.2.2 分配内存

在类加载完成后, 需要为新生对象分配内存(从堆内存中划分一块合适的内存)。分配内存的方式有两种: 指针碰撞和空闲列表
指针碰撞: 将堆内存分为空闲空间和已分配空间, 使用一个指针作为二者的分界线, 当需要分配空间的时候, 将指针向着空闲空间移动一段距离。
image.png空闲列表: 在虚拟机中维护一个列表, 记录堆中哪块内存空闲可用。在分配空间的时候, 寻找一块合适的空间分配并更新列表。
image.png
上述二中方式的选取依靠于堆空间是否规整, 堆空间是否规整又依赖于垃圾回收算法。

3.2.3 初始化零值

给对象的实例变量赋零值, int类型初始化为0, boolean默认值false, 对象默认值为null。

3.2.4 设置对象头 & 实例对象初始化

对象需要设置一些必要的参数: 对象是哪个类的实例, 对象的Hash码, 对象的GC分代年龄, 是否使用偏向锁…
以及设置对象的实例变量数据。
image.png

下述为实例对象的完整流程:
image.png

4. 垃圾回收详解

4.1 内存分配和回收策略

4.1.1 对象优先分配到Eden区域

大多数情况下, 对象在Eden区域分配, 当Eden区域没有足够空间的时候, 发起minor gc. 对象择机进入s区或者老年代。

4.1.2 大对象直接进入老年代

大量连续内存空间的Java对象,进入老年代。或者设置-XX:PretenureSizeThreshold参数, 大于该值直接进入老年代。

4.1.3 长期存活的对象将进入老年代

一直在s区域存活, 且年龄大于设置值(-XX:MaxTenuringThreshold), 那么晋升老年代。

4.1.4 动态对象年龄判断

在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半年龄大于或等于该年龄的对象就能够直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

4.1.5 空间分配担保

对象进入老年代之前, 会判断能否放得下, 如若放不下, 进行full gc.

/**
* 项目启动初期 设置参数
* -XX:+PrintGCDetails     打印gc堆详细信息
* -Xmx200M    最大堆
* -Xms200M    最小堆
* -Xmn60M     年轻代空间
* <p>
* 在指定堆内存为200M情况下, 新生代占 60M, 老年代占 130M
*/
public class JavaGcTest {

    /**
    * 在已知新生代为60M情况下, 按照(eden:s0:s1 占比为8:1:1)分配, eden区域大概有48M
    */
    public static void priorityDistributionEden() {
        byte[] allocation1, allocation2, allocation3, allocation4, allocation5;
        /**
        * 分配小于eden区域大小的内存   -> 对象优先分配eden区域
        */
        allocation1 = new byte[1 * 1024 * 1024];
        allocation2 = new byte[6 * 1024 * 1024];
        allocation3 = new byte[7 * 1024 * 1024];
        allocation4 = new byte[8 * 1024 * 1024];
        /**
        * 再次分配空间时候, eden没有足够空间, 发生minor gc
        * allocation1, allocation2, allocation3, allocation4中只有 allocation1大小没有超过survivor大小, 进入s区, 其余通过分配担保机制进入老年代.
        * allocation5 还是优先进入eden区域
        *       PSYoungGen      total 53760K, used 23154K
        *          eden space 46080K, 45% used     (46080/1024)*0.45=20M   allocation5对象
        *          from space 7680K, 28% used      (7680/1024)*0.28=2M     allocation1对象
        *          to   space 7680K, 0% used
        *      ParOldGen       total 143360K, used 21512K
        *          object space 143360K, 15% used  (143360/1024)*0.15=21M  allocation2, allocation3, allocation4对象之和。
        */
        allocation5 = new byte[20 * 1024 * 1024];
    }


    /**
    * -XX:+UseSerialGC
    * -XX:PretenureSizeThreshold=10485760   大于这个值的对象直接进老年代
    * <p>
    * 新建远大于48M的对象   -> 大对象直接进入老年代
    * PSYoungGen      total 59904K, used 7250K
    * eden space 51712K, 14% used
    * from space 8192K, 0% used
    * to   space 8192K, 0% used
    * ParOldGen       total 136704K, used 61440K
    * object space 136704K, 44% used
    */
    public static void largeObjectEnterOld() {
        byte[] allocation1, allocation2;
        allocation2 = new byte[9 * 1024 * 1024];
    }


    /**
    * 每每进行一次minor jc, eden区域的对象就会进入survivor区域, 且对象年龄设为1
    * 对象在s区,每经过一次minor jc, 对象年龄都会增加1岁(最大年龄默认15岁)   大于最大年龄, 进入老年代
    * -XX:MaxTenuringThreshold=1 可以通过该参数设置
    */
    public static void longLivedObjectsEnterOld() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * 1024 * 1024];
        allocation2 = new byte[20 * 1024 * 1024];
        allocation3 = new byte[23 * 1024 * 1024];
        /**
        * 在分配allocation3的时候, eden空间不足,触发minor gc
        *   def new generation   total 55296K, used 6620K
        *      eden space 49152K,   50% used    allocation3对象
        *      from space 6144K,  50% used     allocation1对象
        *      to   space 6144K,   0% used
        *  tenured generation   total 143360K, used 40960K
        *    the space 143360K,  14% used      allocation2对象大小大于s区, 直接进入老年代
        */
        System.out.println("-------------------");
        allocation4 = new byte[25 * 1024 * 1024];
        /**
        * 分配allocation4, eden空间再次不足, 触发minor gc
        * def new generation   total 55296K, used 26092K
        *   eden space 49152K,  53% used   allocation4对象
        *   from space 6144K,   0% used    allocation1达到了最大年龄 也进入老年代
        *   to   space 6144K,   0% used
        *  tenured generation   total 143360K, used 46870K
        *    the space 143360K,  32% used  allocation1,allocation2,allocation3对象
        */
    }

    /**
    * 动态对象年龄判断
    * 在s区的对象相同年龄全部对象加起来大小总和大于s区域的一半, 年龄大于等于该年龄的对象直接进入老年代
    * -XX:TargetSurvivorRatio=percent  默认是百分之50
    */
    public static void dynamicObjectAgeJudgment() {
        // 暂无例子
    }


    /**
    * 空间担保
    * 在发生Minor GC以前,虚拟机会先检查老年代的连续空间大小是否大于新生代对象总大小或者历次晋升的平均大小, 若是是则进行Minor GC, 不然将进行Full GC。
    */
    public static void spaceWarranty() {
        // 暂无例子
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[50 * 1024 * 1024];
        allocation2 = new byte[88 * 1024 * 1024];
        allocation2 = null;
        /**
        * 再次分配空间的时候, 发现老年代不够用, 触发full gc
        *  the space 143360K,  72%     allocation1,allocation2对象.allocation2对象内存50M
        */
        allocation2 = new byte[50 * 1024 * 1024];
    }


    public static void main(String[] args) {
        // 对象优先分配eden区域
        //JavaGcTest.priorityDistributionEden();
        // 大对象直接进入老年代
        //JavaGcTest.largeObjectEnterOld();
        // 长期存活的对象将进入老年代
        //JavaGcTest.longLivedObjectsEnterOld();
        // 空间担保
        JavaGcTest.spaceWarranty();
    }
}

4.2 对象死亡分析 & 垃圾回收流程 & 垃圾收集算法 & 垃圾收集器

4.2.1 对象死亡分析:

引用计数法: 对象中加一个计数器,发生引用, 计数器+1, 当引用失效, 计数器减1. 知道计数器为0.
存在相互引用的问题:
image.png
可达性分析算法: 从GC Roots的根节点对象出发, 向下遍历, 那些没有被遍历到与GC Roots形成通路的对象, 会被标记为回收。
image.png

// 可作为GC Roots的对象:
虚拟机栈中引用的对象(栈帧中的局部变量)
方法区中类静态属性引用的对象(静态类变量)  
方法区中常量引用的对象 (用final修饰的成员类变量)
本地方法栈中JNI(即一般说的native方法)中引用的对象

class Test {
    // 方法区中类静态属性引用的对象
    private static Test t1;
    // 方法区常量引用的对象
    private static final Test t2 = new Test();
    public void say() {
    // 虚拟机栈中引用的对象
        Test t = new Test();
     }
}

PS: 对象在标记为可回收的时候, 不会马上回收, 而是会执行finalize方法。当对象没有覆盖finalize方法或者该方法已经被虚拟机执行过了, 那么该对象真的会被回收。

再谈引用: 强引用, 软引用, 弱引用, 虚引用

强: 最普遍的引用 即使OOM也不会被垃圾回收器进行回收
Object obj=new Object()
String str = “StrongReference”;
软: 在java中使用SoftReference类表示, 对于软引用关联的对象, 只有内存不足的时候jvm才会回收对象
image.png
image.png
当内存足够情况下, 发生gc, 对象还存在.

  弱: 使用WeakReference类表示, 当jvm进行垃圾回收, 无论内存是否充足, 都会回收弱引用关联的对象。 <br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/136682/1653995939058-f0074464-d6e0-4865-b13a-2ed8dae92085.png#clientId=uc108f173-0b49-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=218&id=ua73cf225&margin=%5Bobject%20Object%5D&name=image.png&originHeight=229&originWidth=644&originalType=binary&ratio=1&rotation=0&showTitle=false&size=49708&status=done&style=none&taskId=ue86d21c3-bad4-4e6f-9ea6-3901f0076ae&title=&width=614)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/136682/1653996084997-8eef473d-7a18-4c37-886b-481a06dfa93c.png#clientId=uc108f173-0b49-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=132&id=u36a332e2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=241&originWidth=1113&originalType=binary&ratio=1&rotation=0&showTitle=false&size=50363&status=done&style=none&taskId=u13881574-990c-4168-b2f6-8f8f0bd4136&title=&width=609.5)<br />**只要发生gc, 对象马上回收.**<br />     虚: 用PhantomReference类表示 任何时候都会被垃圾回收器收回, 虚引用主要用来跟踪对象被垃圾回收的活动。<br /> 实际程序设计中, 很少使用弱引用,虚引用. 使用软引用比较多. 软引用可以加速jvm对垃圾内存的回收速度。防止OOM。

4.2.2 垃圾回收流程:

image.png

4.2.3 垃圾回收算法

标记-清除算法: 标记出所有不需要回收的对象, 然后统一回收没有标记的对象。
(速度快, 空间产生大量不连续碎片)
image.png

  1. 第一次扫描, 通过GC roots判断堆内哪些对象可以进行垃圾回收, 进行标记。
  2. 第二次扫描, 将标记为可回收的对象进行回收清除。

标记-整理算法: 第一个是标记,第二步会进行一个空间整理,从而不产生碎片
避免了内存碎片, 对空间的整理使得效率低下
image.png

标记-复制算法: 将管理的内存分为2块区域,from区域与to区域,将那些不需要回收的对象从from区域拷贝到to区域。复制的过程中完成内存区域的整理。之后交换from和to的指向
不会产生内存碎片, 但是需要双倍内存空间, 并且拷贝也需要时间。
image.png

分代收集算法: 新生代使用标记-复制算法, 老年代选择标记-清除/标记-整理算法
(新生代对象生命周期短, 垃圾回收频繁, 老年代很长时间才会垃圾回收)

4.2.4 垃圾回收器

详解: https://www.yuque.com/wangyanyang/qz7fwa/ho948m

image.png

5. JVM参数 & 监控定位 & 调优

5.1 jvm参数

5.1.1 堆内存相关

显示指定堆内存
-Xmx 最大堆(默认物理内存1/4)
-Xms最小堆(默认物理内存1/64)
ps: -Xmx100M -Xms10M
显示指定新生代内存
-Xmn 新生代内存大小(推荐配置为堆的3/8) 或者 -XX:NewRatio 新生代与老年代的占比
-XX:SurvivorRatio 默认设置为8, 相当于Eden:s0:s1 = 8:1:1
-XX:PretenureSizeThreshold=10485760 对象超过大小直接进老年代
-XX:MaxTenuringThreshold=1 年轻代最大年龄, 大于阀值, 对象由s区进入到老年代
ps: -Xmn256M -XX:NewRatio=1
显示指定永久代,元空间大小
1.8 之前: -XX:PermSize=N(永久代初始化大小) -XX:MaxPermSize=N(永久代最大值)
1.8 之后: -XX:MetaspaceSize=N(元空间初始化大小) -XX:MaxMetaspaceSize=N(元空间最大值)
显示指定栈大小 & 直接内存
-Xss1M
-XX:MaxDirectMemorySize=256m

5.1.2 垃圾收集相关

-XX:+UseSerialGC 串行垃圾收集器
-XX:+UseParallelGC 并行垃圾收集器
-XX:+UseConcMarkSweepGC CMS垃圾收集器
-XX:+UseG1GC G1垃圾收集器

5.1.3 GC记录

-XX:+PrintGCDetails 打印gc信息
-Xloggc:/xxx/gc.log 输出gc信息到文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/wangyanyang/Downloads/dump-test.hprof
发生OOM的时候,输出dump文件

5.2 jdk监控(配合JProfiler 和 Arthas使用)

5.2.1 jdk自带命令

jps -l 查看java进程并输出主类名
image.png
jps -v 查看启动时候jvm参数
image.png

jinfo 实时查看虚拟机各项参数
image.png

5.2.2 分析dump文件

jvm运行的时候内存的快照, 利用它可以分析是否存在内存泄漏, 内存管理是否合格, 当发生oom的时候可以定位问题。

1. 如何获取dump文件:

 1. jmap -dump:format=b,file=xxx.hprof <br />     2. 设置参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xxx.hprof 发生oom的时候记录文件(推荐使用)<br />     3. 可以使用arthas的heapdump命令(heapdump xxx.hprof)

2. 分析dump文件:

1.通过java自带的jhat命令分析(jhat xxx.hprof)
2.使用JProfiler工具分析

寻找最大对象并定位代码位置
image.png
image.png
image.png

5.2.3 分析线程快照(虚拟机中每一条正在执行的方法堆栈集合)

线程快照主要用于定位线程间死锁, 死循环, 请求外部资源长时间等待导致的线程停顿。

1. 如何获取线程快照:

 1. 使用jstack命令(jstack xxx)    <br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/136682/1653923959365-d502a61d-5cf9-4e77-bb4b-55d8a0674ff6.png#clientId=uad3f4ae3-2192-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=519&id=ud0c649d9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=519&originWidth=729&originalType=binary&ratio=1&rotation=0&showTitle=false&size=74105&status=done&style=none&taskId=u2f121eaf-29c9-4663-aa37-b74a93b2585&title=&width=729)<br />     2. 使用arthas里的dashboard可以直观看到<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/136682/1653923983327-cc412ceb-9e98-4393-8188-f6f00e4b4fdc.png#clientId=uad3f4ae3-2192-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=1138&id=u5b9645f1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1138&originWidth=2678&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1064117&status=done&style=none&taskId=u621bf55a-e8f7-4c96-a342-43c92f8ab99&title=&width=2678)