虚拟机/内存/调优

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

2022-03-06  本文已影响0人  Corey1874

自动内存管理机制

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

程序计数器

Java虚拟机栈

本地方法栈

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

方法区

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

直接内存

对象访问

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

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

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

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

引用计数法

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

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:

四种引用

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收集器

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

优点:

缺点:

G1收集器

避免全区域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

大对象直接进入老年代

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

-XX:PretenureSizeThreshold=10M

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

出生在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

空间分配担保

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

img
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文件)

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 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
上一篇 下一篇

猜你喜欢

热点阅读