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

自动内存管理机制

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 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 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 list = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            Thread.sleep(50);
           list.add( new OOMObject());
        }
    }
}
img

你可能感兴趣的:(深入理解Java虚拟机读书笔记(一))