美文网首页虚拟机/内存/调优
深入理解Java虚拟机读书笔记(一)

深入理解Java虚拟机读书笔记(一)

作者: Corey1874 | 来源:发表于2022-03-06 10:57 被阅读0次

    自动内存管理机制

    1.Java内存区域与内存溢出异常

    程序计数器

    • 如果正在执行的方法是Java方法,那么记录的是正在执行的虚拟机字节码指令的地址。但如果执行的本地方法,那么值会为空

    Java虚拟机栈

    • 存局部变量表、操作栈、动态链接、方法出口

      • 局部变量表
        • 编译期可知的基本数据类型(long、double占2个局部变量空间,其他占1个)
        • 对象引用
        • returnAddress
        • 局部变量表需要的内存空间,在编译期完成分配

    本地方法栈

    几乎所有对象、数组都在堆上分配(不一定所有)

    • 划分:新生代、老年代
    • 从内存分配角度划分:可划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

    方法区

    存虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码

    • 运行时常量池:

      • class文件中有常量池,放了编译期产生的字面量、符号引用。这些在类加载后,会放入运行时常量池
      • 相比于class文件中的常量池,具备动态性。即不要求一定要先预置入class文件常量池,再进运行时常量池。运行期间也可能放进新的常量到池里。应用:比如String.intern()

    直接内存

    • 并不属于虚拟机运行时数据区。

    • NIO类里,可以直接使用native函数直接分配堆外内存,通过Java堆里的DirectByteBuffer对象作为这块内存的引用。这样做可以避免在Java堆和Native堆里来回复制对象,可以提高性能

    • 所以分配内存的时候,不能只考虑Java堆的大小,还要考虑直接内存。否则受物理内存和处理器寻址空间的限制,同样会内存溢出异常

    对象访问

    Object obj = new Object();其中,Object obj指的是,发生在本地变量表,作为一个reference类型数据,new Object()发生在堆,实例数据。方法区中会存储此对象的类型数据,如对象类型、父类、实现的接口、方法等

    不同虚拟机有不同的实现,分为句柄访问、指针访问

    • 句柄访问:

      • 堆中划分出一块内存--句柄池,reference中存储的就是对象的句柄地址。
      • 句柄中有指向方法区中类型数据的指针,有指向堆中实例数据的指针
      • 优点是对象移动的时候(垃圾回收时发生很普遍),reference本身不需要修改,只需要改动指针指向的地址
    • 指针访问:

      • reference存储的就是对象地址
      • 优点是速度更快,因为节省了一次指针定位的开销
      • Sun HotSpot采用

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

    • 关注的目标:

      • 每个栈帧中分配多少内存,在类结构确定时,基本就是确定(不考虑JIT优化),程序计数器、本地方法栈、虚拟机栈随着方法退出,这些区域的内存分配和回收是确定的,不需过多关心
      • 堆和方法区中的内存,只有运行的时候才知道,内存的分配和回收时动态的。因此垃圾收集器关注的是这部分内存

    如何确定对象已死,需要回收?

    引用计数法

    给对象一个引用计数器,初始值为1,当有地方引用它,计数器值+1,引用失效,计数器值-1,任何时刻只要计数器的值为0,这个对象就是不可能再被使用的,可以被回收

    • 缺陷:

      • 无法解决循环引用的问题,即A、B两个对象互相引用,但是没有外部引用指向他们
    • 测试:

    public class TestCounter {
    
        static class Student{
            private Object instance;
                    // 占用一点内存
            private static final int _1MB = 1024 * 1024;
            private static byte[] b = new byte[20 * _1MB];
        }
    
        public static void main(String[] args) {
            Student studentA = new Student();
            Student studentB = new Student();
            studentA.instance = studentB;
            studentB.instance = studentA;
    
            studentA = null;
            studentB = null;
    
            System.gc();
        }
    }
    [GC (System.gc()) [PSYoungGen: 24289K->872K(73728K)] 24289K->21360K(241664K), 0.0182375 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
    [Full GC (System.gc()) [PSYoungGen: 872K->0K(73728K)] [ParOldGen: 20488K->21201K(167936K)] 21360K->21201K(241664K), [Metaspace: 3299K->3299K(1056768K)], 0.0072952 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
    Heap
     PSYoungGen      total 73728K, used 635K [0x000000076e000000, 0x0000000773200000, 0x00000007c0000000)
      eden space 63488K, 1% used [0x000000076e000000,0x000000076e09ecf8,0x0000000771e00000)
      from space 10240K, 0% used [0x0000000771e00000,0x0000000771e00000,0x0000000772800000)
      to   space 10240K, 0% used [0x0000000772800000,0x0000000772800000,0x0000000773200000)
     ParOldGen       total 167936K, used 21201K [0x00000006ca000000, 0x00000006d4400000, 0x000000076e000000)
      object space 167936K, 12% used [0x00000006ca000000,0x00000006cb4b45a0,0x00000006d4400000)
     Metaspace       used 3306K, capacity 4500K, committed 4864K, reserved 1056768K
      class space    used 360K, capacity 388K, committed 512K, reserved 1048576K
    

    由此可以看到,循环引用的A、B仍然被GC回收了,所以JVM用的不是引用计数法

    根搜索算法

    即可达性分析。从一些GCRoot出发往下搜索,走过的路径称为引用链。当一个对象没有任何引用链相连(图论里称为不可达),则该对象是不可用的。

    GCRoot:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象

    • 方法区中静态属性引用的对象

    • 方法区中常量引用的对象

    • 本地方法栈中本地方法引用的对象

    四种引用

    • 强引用:直接引用

    • 软引用:内存不够不会直接抛错误,而是回收软引用,如果还是不够才抛错误

    • 弱引用:不管内存够不够,下一次gc一定会回收

    • 虚引用:用来回收的时候收到一个通知

    finalize()

    当对象不可达的时候,会进行第一次筛选,并第一标记。判断对象是否有必要执行finalize()方法。如果没必要(1.该对象没有重写finalize()方法或2.方法已执行过一次),直接回收;如果有必要,会进一个F-Queue队列,稍后虚拟机建立一个低优先级的finalizer线程去执行这个方法(虚拟机不会等这个方法执行完成),有一次拯救自己的机会,所以只要有任何一个引用指向它,就会被标记,这样就移出了即将回收的集合。

    /**
     * 2022/2/13
     */
    public class FinalizeEscapeGc {
    
        public static FinalizeEscapeGc SAVE_HOOK = null;
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("finalize method executed");
            FinalizeEscapeGc.SAVE_HOOK = this;
        }
    
        public static void main(String[] args) throws InterruptedException {
            SAVE_HOOK = new FinalizeEscapeGc();
    
            // 第一次自救
            escape();
    
            // 再来一次
            escape();
        }
    
        private static void escape() throws InterruptedException {
            SAVE_HOOK = null;
            System.gc();
    
            // finalize优先级低 这里先暂停一下
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                System.out.println("yes i am alive");
            } else {
                System.out.println("no i am dead");
            }
        }
    }
    

    上述代码可以看出,对象执行finalize有一次自救的机会

    垃圾收集算法

    标记-清除

    最基础,其他的算法基于标记清除改进

    缺点:产生较多内存碎片,如果有大对象就无法分配,只能触发一次额外GC

    复制算法

    把内存划分成两块,保留一块不用,只有其中一块。每次只要这块内存用完了,就把存活的对象复制到另一块,然后一次清除掉用过的内存

    优点:内存分配不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存。简单高效

    缺点:可用内存缩小。

    新生代是采用复制算法

    Eden : Survivor = 8 : 1 : 1

    每次保留其中一块survivor不用,可用空间为90%。新生代大部分对象朝生夕死

    分配担保:当survivor没法放下存活的对象时,可以通过分配担保机制进入老年代

    标记整理

    让所有存活的对象向一端移动,直接清理掉端边界以外的内存

    适合老年代

    垃圾收集器

    img

    Serial收集器

    单线程,垃圾收集时会Stop The World,后台停止用户线程,直到收集结束

    虚拟机运行在client模式下的默认收集器,因为分配的内存不会很大,垃圾收集时间往往只需要几十毫秒,只要不频繁,完全可以接受。

    ParNew收集器

    Serial收集器的多线程版本,很多特性共用。

    运行在Server模式下虚拟机的首选收集器,因为除了Serial,只有ParNew新生代收集器,是可以和真正并发的收集器--CMS收集器配合。因为CMS是无法与新生代收集器Parallel Scavenge收集器配合。

    -XX:+UseConcMarkSweepGC,默认新生代收集器为ParNew

    -XX:+UseParNewGC,强制使用ParNew收集器

    ParNew在单CPU环境下可能效果并不比Serial要好,但是在多CPU环境下,GC时对系统资源利用有好处,默认线程数与CPU数相同

    -XX:ParallelGCThread可以限制垃圾收集的线程数

    Parallel Scavenge收集器

    与Serial、ParNew一样是使用复制算法的新生代收集器,多线程。

    不同之处在于,其他收集器关注尽可能缩短用户线程的停顿时间,而Parallel Scavenge收集器则致力于实现可控制的吞吐量

    三个参数:

    -XX:MaxGCPauseMills 垃圾收集最大停顿时间。缩短停顿时间是以牺牲吞吐量、新生代内存空间为代价。系统把新生代调小,那么收集更少的空间,需要停顿的时间也就更短,但这会导致GC更加频繁,可能反而总的GC时间更多,吞吐量降低

    -XX:GCTimeRatio 直接设置吞吐量。如果设置19,那么GC时间是1/20=5%,吞吐量95%。如果设置99,那么GC时间是1/100=1%,吞吐量99%

    -XX:+UseAdaptiveSizePolicy 这个参数,可以设置GC自适应调节策略。不需要指定新生代大小、Eden与Survivor比例、晋升老年代对象年龄等细节参数,只需要设置最大最小堆大小,设置一直关注的目标,-XX:MaxGCPauseMills或-XX:GCTimeRatio,让虚拟机动态调整参数提供最合适的停顿时间或吞吐量

    Serial Old收集器

    Serial收集器的老年代版本,使用标记-整理算法,单线程,同样在client模式下虚拟机使用

    如果在server模式下使用,有两种用途:1.配合Parallel Scavenge 2.作为CMS的后背预案

    Parallel Old收集器

    Parallel Scavenge的老年代版本。多线程,标记-整理。

    主要用途是配合Parallel Scavenge。因为Parallel Scavenge无法与CMS配合,在Parallel Old出现前,只能和Serial Old这种单线程收集器配合使用,服务端性能拖累

    CMS收集器

    • 四个阶段:

      • 初始标记:标记GCRoot能直接关联到的对象,速度很快

      • 并发标记:GC Roots Tracing

      • 重新标记:修正并发标记期间,由于用户程序运行导致标记变动的那部分对象。停顿时间比初始标记长,但远比并发标记短

      • 并发清除:

    • 其中,初始标记、重新标记,仍然需要Stop the World。

    • 最耗时的是并发标记、并发清除

    CMS收集器采用标记清除算法

    优点:

    • 设计目标:最短回收停顿时间

    缺点:

    • CPU资源敏感

    • 无法处理浮动垃圾

    • 产生内存碎片

    G1收集器

    • 与CMS相比改进点:

      • 基于标记-整理算法,不会产生碎片

      • 精确控制停顿。即可以指定在长度为M毫秒的时间段内,GC时间不超过N毫秒

    • 为什么能够实现不牺牲吞吐量,完成低停顿回收?

    避免全区域GC。把整个Java堆,划分为几个独立区域,跟踪垃圾堆积密度,后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

    GC常用参数

    img

    内存分配与回收策略

    对象优先在Eden分配

    /**
     * 2022/2/26
     * 参数 -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * Eden:10M,其中9M可用
     * Serial+Serial Old组合
     */
    public class TestAllocation {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
            byte[] a1,a2,a3,a4,a5;
            a1 = new byte[2 * _1MB];
            a2 = new byte[2 * _1MB];
            a3 = new byte[2 * _1MB];    // 出现一次Minor GC
            a4 = new byte[4 * _1MB];    
            a5 = new byte[2 * _1MB];
        }
    }
    

    这里的GC日志与书中的日志不相符。

    书中描述,当分配a4的时候,由于共9M的eden空间不足,于是触发MinorGC,但是由于Survivor区只有1M,空间不足以放a1、a2、a3,于是通过分配担保机制,这6M大小进入老年代,4M的a4会分配在Elden

    但是实测下来,分配了5M之前都是在Eden区,但是a1、a2、a3共6M分配之后,就已经开始出现MinorGC。a1、a2共4M进入了老年代,a3分配到了eden

    [GC (Allocation Failure) [DefNew: 6444K->810K(9216K), 0.0042813 secs] 6444K->4906K(19456K), 0.0043266 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 3188K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  29% used [0x00000000fec00000, 0x00000000fee52998, 0x00000000ff400000)
      from space 1024K,  79% used [0x00000000ff500000, 0x00000000ff5ca8f8, 0x00000000ff600000)
      to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
     tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
     Metaspace       used 3303K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 360K, capacity 388K, committed 512K, reserved 1048576K
    

    分配完a4的日志如下:可以看出a3、a4共计6M分配在Eden区,老年代4M

    [GC (Allocation Failure) [DefNew: 6444K->797K(9216K), 0.0033512 secs] 6444K->4893K(19456K), 0.0034188 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 7429K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  80% used [0x00000000fec00000, 0x00000000ff279e48, 0x00000000ff400000)
      from space 1024K,  77% used [0x00000000ff500000, 0x00000000ff5c76a8, 0x00000000ff600000)
      to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
     tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
     Metaspace       used 3262K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 359K, capacity 388K, committed 512K, reserved 1048576K
    

    如果再分配1M的a5,仍然在Eden区,共7M,这里很奇怪,之前6M就已经触发MinorGC,这时没有触发,如果a5大小为2M,会触发第二次MinorGC,a3也会进入老年代,剩下a4、a5共6M在Eden

    [GC (Allocation Failure) [DefNew: 6444K->800K(9216K), 0.0035625 secs] 6444K->4897K(19456K), 0.0036098 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [DefNew (promotion failed) : 7350K->6570K(9216K), 0.0019683 secs][Tenured: 6845K->6845K(10240K), 0.0024806 secs] 11446K->10962K(19456K), [Metaspace: 3258K->3258K(1056768K)], 0.0045052 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Heap
     def new generation   total 9216K, used 6466K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  78% used [0x00000000fec00000, 0x00000000ff250950, 0x00000000ff400000)
      from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
     tenured generation   total 10240K, used 6845K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  66% used [0x00000000ff600000, 0x00000000ffcaf558, 0x00000000ffcaf600, 0x0000000100000000)
     Metaspace       used 3265K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 359K, capacity 388K, committed 512K, reserved 1048576K
    
    • MinorGC:新生代的GC,新生代对象朝生夕死,回收速度比较快
    • MajorGC/FullGC:老年代的GC,出现了MajorGC,一般伴随着MinorGC(不绝对,ParallelScavenge可以选择直接MajorGC)。MajorGC速度比MinorGC慢10倍以上

    大对象直接进入老年代

    大对象就是需要大量连续内存空间的对象,比如上面的byte[],我们应该尽量避免大对象,因为会容易导致内存中海油不少空间就提前触发GC来存放大对象

    -XX:PretenureSizeThreshold=10M

    • 可以设置阈值,大于阈值的对象,会直接分配在老年代

    • 避免Eden区与Survivor区之间发生大量内存拷贝

    • 只对Serial、ParNew两款收集器有效

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

    出生在Eden区的对象,对象年龄为0,经过第一次MinorGC且能被Survivor容纳的话,对象年龄为1。然后每熬过一轮MinorGC,对象年龄+1,当达到阈值(默认15),将进入老年代。阈值通过参数-XX:MaxTenuringThreshold设置

    /**
     * 2022/2/27
     * 进入老年代的阈值-XX:MaxTenuringThreshold
     * -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
     */
    public class TestTenuringThreshold {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
            byte[] a1,a2,a3;
            a1 = new byte[_1MB / 4];
            a2 = new byte[4 * _1MB];
            a3 = new byte[4 * _1MB];
            a3 = null;
            a3 = new byte[4 * _1MB];
        }
    }
    

    实测下来,现象与书中不一致,原因暂时没有想明白。

    MaxTenuringThreshold=1:

    [GC (Allocation Failure) [DefNew
    Desired survivor size 524288 bytes, new threshold 1 (max 1)
    - age   1:    1048576 bytes,    1048576 total
    : 6700K->1024K(9216K), 0.0052175 secs] 6700K->5173K(19456K), 0.0052856 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    [GC (Allocation Failure) [DefNew
    Desired survivor size 524288 bytes, new threshold 1 (max 1)
    - age   1:       1792 bytes,       1792 total
    : 5368K->1K(9216K), 0.0016983 secs] 9517K->5109K(19456K), 0.0017603 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 4235K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff022798, 0x00000000ff400000)
      from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400700, 0x00000000ff500000)
      to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
     tenured generation   total 10240K, used 5107K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  49% used [0x00000000ff600000, 0x00000000ffafce90, 0x00000000ffafd000, 0x0000000100000000)
     Metaspace       used 3324K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 361K, capacity 388K, committed 512K, reserved 1048576K
    

    书中描述:a1大小256k,分配a2的时候触发第一次MinorGC,Survivor足够容纳,进入Survivor,对象年龄为1。第二次MinorGC时,因为阈值为1,此时进入老年代。新生代会干净

    img

    MaxTenuringThreshold=15:

    [GC (Allocation Failure) [DefNew
    Desired survivor size 524288 bytes, new threshold 1 (max 15)
    - age   1:    1048576 bytes,    1048576 total
    : 6700K->1024K(9216K), 0.0029448 secs] 6700K->5148K(19456K), 0.0029874 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [DefNew
    Desired survivor size 524288 bytes, new threshold 15 (max 15)
    - age   1:       1840 bytes,       1840 total
    : 5368K->1K(9216K), 0.0012153 secs] 9492K->5078K(19456K), 0.0012501 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 4235K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff022568, 0x00000000ff400000)
      from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400730, 0x00000000ff500000)
      to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
     tenured generation   total 10240K, used 5076K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  49% used [0x00000000ff600000, 0x00000000ffaf52d0, 0x00000000ffaf5400, 0x0000000100000000)
     Metaspace       used 3261K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 359K, capacity 388K, committed 512K, reserved 1048576K
    

    书中描述:当第二次MinorGC后,a1仍然留在Survivor,仍然有404k空间被占用。a1对象年龄为2

    img

    空间分配担保

    • MinorGC时,虚拟机检测之前每次晋升到老年代的对象平均大小,和老年代剩余空间比较,

      • 如果大于,说明老年代很可能剩余空间不够这次晋升了,发生FullGC

      • 如果小于

        • -XX:-HandlerPromotionFailure,开关打开,只会进行MinorGC。

        • 开关关闭,FullGC

    • 这个开关打开的话,就是认为不怕担保失败,认为这次大概率小于平均值,可以担保成功。如果关闭的话,就是悲观地认为空间肯定不够,干脆直接FullGC

    • 如果本次对象大小突增,远高于平均值,那么就导致担保失败,失败之后会进行一次FullGC

    • 大部分情况下,会把开关打开,避免频繁FullGC

    3.虚拟机性能监控与故障处理工具

    jps

    D:\Java\DemoCode\JVM>jps
    543116 Launcher
    173156
    250948 TestDeadLock
    260692 Jps
    476140 Launcher
    

    可以列出正在运行的进程。由LVMID(Local Virtual Machine Identifier)、主类名称组成

    对于本地虚拟机进程:LVMID和操作系统的进程ID一致

    后缀 功能
    -q 只输出LVMID
    -m 输出传给main()函数的参数
    -l 类的全名,如果执行的是Jar包,输出Jar包路径
    -v 进程启动时JVM参数

    jstat

    • 格式:jstat option vmid [interval[s|ms] [count]]

      • 本地虚拟机vmid和lvmid一致, interval表示查询频率,默认ms,count表示查询次数

      • 如:jstat -gc 250948 250 20

    img
    • -gcutil
    D:\Java\DemoCode\JVM>jstat -gcutil 250948
      S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
      0.00  33.73  43.81   0.01  94.27  88.95      1    0.005     0    0.000    0.005
    

    含义:

    S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
    Survivor Eden区 永久代 YoungGC次数 YoungGC耗时 FullGC次数 FullGC耗时 总GC耗时

    如果是P,表示永久代(Permanent)

    jinfo

    jinfo [option] pid

    如:jinfo 250948

    jmap

    可以获得堆转存储快照(heapdump/dump文件)

    • 获得dump文件的方式:

      • kill -3,恐吓虚拟机
      • -XX:+HeapDumpOnOutOfMemoryError,可以在OOM之后,自动生成dump文件
    • 命令格式:jmap [option] vmid
    img

    jhat

    dump文件的分析工具,一般不用,不会在服务器上直接分析dump文件,因为比较消耗硬件资源,而且功能也少

    一般用:Visual VM或者专业分析dump工具,如Eclipse Memory Analyzer、IBM HeapAnalyzer

    jstack

    用于生成线程堆栈快照(threaddump文件或javacore文件),目的是定位线程长时间停顿原因,如线程死锁、死循环、请求外部资源导致的长时间等待

    jstack [option] vmid

    img
    • 实际项目中,可以通过下面的方法,输出堆栈信息,做成管理员页面
    public static void main(String[] args) {
            for (Map.Entry<Thread, StackTraceElement[]> threadEntry : Thread.getAllStackTraces().entrySet()) {
                Thread thread = threadEntry.getKey();
                StackTraceElement[] stackTrace = threadEntry.getValue();
                if (thread.equals(Thread.currentThread())){
                    continue;
                }
                System.out.print("\n线程:"+ thread.getName()+"\n");
                for (StackTraceElement stackTraceElement : stackTrace) {
                    System.out.print("\t"+ stackTraceElement+"\n");
                }
            }
        }
    

    可视化工具

    Jconsole

    1.内存监控

    /**
     * 2022/3/6
     * -Xms100m -Xmx100m -XX:+UseSerialGC
     */
    public class TestMonitorMemory {
    
        static class OOMObject{
            public byte[] placeHolder = new byte[64 * 1024];
        }
    
        public static void main(String[] args) throws InterruptedException {
            fillHeap(1000);
            System.out.println("执行完了方法");
        }
    
        public static void fillHeap(int num) throws InterruptedException {
            List<OOMObject> list = new ArrayList<>();
            for (int i = 0; i < num; i++) {
                Thread.sleep(50);
               list.add( new OOMObject());
            }
            System.gc();
        }
    }
    

    运行代码,指定堆内存100M。每次生成一个64K的对象,大概1600个对象会把堆填充满,然后发生OOM

    Eden区表现为折线图,一直在增加,满了就回收

    img img
    • 上图中,堆内存表现为一直上涨,即时是循环了1000次,然后执行了System.gc(),被填充到堆中的对象还活着。

    • 这是因为执行的时候,fillHeap方法仍然没有退出,list对象在执行的时候仍然处于作用域内。所以要把它放在fillHeap方法外执行

    • 如果执行2000次,会发生下面的结果,直到OOM,把堆内存清空

    img

    如下,把System.gc()放在方法外执行

    /**
     * 2022/3/6
     * -Xms100m -Xmx100m -XX:+UseSerialGC
     */
    public class TestMonitorMemory {
    
        static class OOMObject{
            public byte[] placeHolder = new byte[64 * 1024];
        }
    
        public static void main(String[] args) throws InterruptedException {
            fillHeap(1000);
            System.gc();
    
            System.out.println("执行完了gc");
            Thread.sleep(100000);
        }
    
        public static void fillHeap(int num) throws InterruptedException {
            List<OOMObject> list = new ArrayList<>();
            for (int i = 0; i < num; i++) {
                Thread.sleep(50);
               list.add( new OOMObject());
            }
        }
    }
    
    img

    相关文章

      网友评论

        本文标题:深入理解Java虚拟机读书笔记(一)

        本文链接:https://www.haomeiwen.com/subject/auwirrtx.html