JVM学习笔记之堆

目录

背景

核心概述

设置堆大小、OOM问题

年轻代与老年代

对象分配过程

Minor GC、Major GC、Full GC

堆空间分代思想

内存分配策略

TLAB

堆参数设置小结

堆不是分配对象的唯一选择

结语

背景

整理一下这几天学习宋红康JVM上篇剩下部分的笔记,从堆到垃圾回收器

从堆开始,测试内容都是基于jdk8的

核心概述

进程内唯一,线程共享,但可以划分线程私有缓冲区(Thread Local Allocation Buffer, TLAB)。

堆在JVM启动时被创建,空间大小确定,是JVM管理的最大的内存空间。

堆可以处于物理上不连续的内存,但逻辑上是连续的。

 

用下面的例子查看jvm的堆,两个进程代码完全一样

Demo0:

package jvm.heap;


public class HeapDemo0 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("start..");
        Thread.sleep(1000 * 1000);
    }
}

Demo1:

package jvm.heap;

public class HeapDemo1 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("start..");
        Thread.sleep(1000 * 1000);
    }
}

在编辑配置里,给出堆参数,初始大小(-Xms)和最大大小(-Xmx)分别为10M和20M

demo0:

demo1:

然后运行这两个程序,打开jdk8自带的jvm可视化工具jvisualvm

C:\Users\songzeceng>jvisualvm

从左侧应用程序中点击Demo0,点击右侧监视标签,查看对信息,可以看到,使用的堆大小上限为10M

而Demo1的上限值为20M

 

jvisualvm也可以配合mat来进行GC Roots溯源,具体请参见文章JVM学习之垃圾回收和垃圾回收器的使用MAT进行GC Roots溯源部分。关于更多的可视化JVM监控工具,参见文章JVM学习笔记之GUI监控工具

 

几乎所有对象实例和数组,都要在运行时被分配到堆上。

数组和对象可能永远不会存储到栈上,栈帧保存对象和数组的引用,此引用指向对象或数组在堆中的首地址。

 

如下图所示,栈中局部变量引用堆中的创建的实例,堆中的实例根据方法区中的类及对应方法实现来创建

方法结束后,堆中的对象不会马上被移除,仅仅在GC时才会被移除,所以堆是GC的重点区域。因为方法可能被频繁调用,那么如果方法一结束就销毁堆中对象,就会导致对象的创建和销毁过于频繁,导致GC线程忙碌,从而影响用户线程的执行,降低程序运行效率

 

以以下代码为例,见识一下创建对象和数组时的字节码指令

package jvm.heap;


public class HeapObjArrayNew {
    private int id = 1;


    public HeapObjArrayNew(int id) {
        this.id = id;
    }


    public void show() {
        System.out.println("My id is " + id);
    }


    public static void main(String[] args) {
        HeapObjArrayNew hoan0 = new HeapObjArrayNew(1);
        HeapObjArrayNew hoan1 = new HeapObjArrayNew(2);


        int[] arr0 = new int[5];
        Object[] arr1 = new Object[5];
    }
}

运行javap -c命令查看字节码,其中main()方法的字节码如下所示

其中new就表示创建对象的指令,newarray表示创建数组,anewarray表示创建对象数组

 

内存细分:

jdk8及以后,堆内存逻辑上分为三部分:新生区、老年区、元空间,但计算大小时只计算新生区和老年区

新生区又被分为伊甸园区和幸存区

重新运行HeapDemo0,然后打开jvisualvm,安装visual gc插件,重启jvisualvm,可以在visual gc标签下看到新生代、老年代和元空间

可见,新生代大小为3M,老年区为7M,元空间为1G

在运行参数里加入 -XX:+PrintGCDetails可以打印GC详细信息,以HeapObjArrayNew为例

Heap
PSYoungGen      total 75776K, used 5202K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 8% used [0x000000076b700000,0x000000076bc149a0,0x000000076f680000)
  from space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
  to   space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
ParOldGen       total 173568K, used 0K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 0% used [0x00000006c2400000,0x00000006c2400000,0x00000006ccd80000)
Metaspace       used 3143K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 342K, capacity 388K, committed 512K, reserved 1048576K


Process finished with exit code 0

可以看到新生代、老年代和元空间内存使用情况

设置堆大小、OOM问题

-Xms用于表示堆区起始内存,-Xmx表示堆区最大内存。

默认情况下,堆起始内存大小为物理内存/64,最大大小为物理内存/4。

如果要手动设置,建议把两者大小设置成一样,目的是让jvm垃圾回收完之后不用重新分隔计算堆区大小,从而提高性能。

运行HeapDemo1,然后使用jps查看进程号

D:\develop\ideaWorkspace\kafka-demo\target\classes\jvm\heap>jps
18708
20308 Launcher
11384 HeapDemo1
17480
10444 Jps
12588 RemoteMavenServer36
22076 Main

再使用jstats -gc 进程号查看堆内存分配情况

D:\develop\ideaWorkspace\kafka-demo\target\classes\jvm\heap>jstat -gc 11384
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
512.0  512.0   0.0    0.0    5632.0   3524.2   13824.0      0.0     4480.0 776.7  384.0   76.6       0    0.000   0      0.000    0.000

其中S0、S1、E、O分别表示S0(幸存者0)区、S1区、伊甸园区和老年区,C和U表示总大小和已使用大小。

例如S0C就表示分配给幸存者0区的内存为512K,S0U表示幸存者0区已使用的内存大小为0K。

把所有的四个区所有的C内存加起来:512+512+5632+13824=20480,正是我们指定的堆内存大小20M。

但由于S0和S1不能被同时使用,所以运行时的堆内存只有19.5M。

public class HeapDemo1 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("start..");

        Runtime runtime = Runtime.getRuntime();
        float maxMemory = runtime.maxMemory() / 1024f / 1024;
        float totalMemory = runtime.totalMemory() / 1024f / 1024;
        System.out.println("Max memory = " + maxMemory);
        System.out.println("Total memory = " + totalMemory);

        Thread.sleep(1000 * 1000);
    }
}

运行结果

 

一旦堆区的内存大小大于-Xmx指定的值,就会抛出OutOfMemory异常,例如以下代码

package jvm.heap;


import java.util.ArrayList;
import java.util.List;
import java.util.Random;


public class HeapObjArrayNew {
    private byte[] bytes;

    public HeapObjArrayNew(int len) {
        this.bytes = new byte[len];
    }

    public static void main(String[] args) {
        List<Object> list = new ArrayList<Object>();

        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            list.add(new HeapObjArrayNew(new Random().nextInt(1024 * 1024)));
        }
    }
}

一运行很快就会报错,OOM错误

可以从jvisualvm中看到堆空间的占用过程,视频链接如下

https://download.csdn.net/download/qq_37475168/12442103

当老年代被占满时,就会抛出OOM错误

年轻代与老年代

存储在JVM中的对象可以被分为两类:生命周期短和生命周期长

堆区可以被分为年轻代和老年代,年轻代又可以分为伊甸园区、Survivor0区和Survivor1区

 

一般来说,新生代占用1/3的堆空间,老年代占用2/3的堆空间。伊甸园区占用4/5的新生代空间,剩下1/5由S0和S1均分

配置新生代和老年代的占比:-XX:NewRatio,默认值为-XX:NewRatio=2,表示新生代:老年代=1:2

如果程序中生命周期长的对象比较多,可以把此值调高

可以使用jinfo查看JVM参数

C:\Users\songzeceng>jps
15152
19552 Launcher
20240 Jps
15784 RemoteMavenServer36
18604 HeapDemo0


C:\Users\songzeceng>jinfo -flag NewRatio 18604
-XX:NewRatio=2

在HotSpot中,伊甸园区和两个S区的空间默认占比为8:1:1,可以通过改变-XX:SurvivorRatio来调整此比例,默认值为8

C:\Users\songzeceng>jinfo -flag SurvivorRatio 18604
-XX:SurvivorRatio=8

但实际上伊甸园区和S区的比例不一定是8:1,除非显式设置为8:1(-XX:SurvivorRatio=8)

C:\Users\songzeceng>jps
15152
6640 Jps
20452 Launcher
2516 HeapDemo0
15784 RemoteMavenServer36


C:\Users\songzeceng>jstat -gc 2516
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
20480.0 20480.0  0.0    0.0   163840.0 13107.3   409600.0     0.0     4480.0 776.7  384.0   76.6       0    0.000   0      0.000    0.000

JVM默认开启了自适应机制-XX:+UseAdaptiveSizePolicy,我们可以把+改成-来取消自适应:-XX:-UseAdaptiveSizePolicy。

C:\Users\songzeceng>jps
11456 Jps
15152
8640 Launcher
4948 HeapDemo0
15784 RemoteMavenServer36


C:\Users\songzeceng>jstat -gc 4948
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
25600.0 25600.0  0.0    0.0   153600.0 12288.1   409600.0     0.0     4480.0 776.7  384.0   76.6       0    0.000   0      0.000    0.000

可见如果不显示指定SurvivorRatio,伊甸园区和幸存区的比例大小是6:1

 

几乎所有的java对象都是在伊甸园区被new出来的(除非对象大得只能放到老年区),绝大部分java对象都在新生代被销毁

可以使用-Xmn设置新生代最大内存大小,优先级比NewRatio指定的值高,但一般使用默认值

对象分配过程

1)、new出来的对象先放伊甸园区,此区有大小限制

2)、如果伊甸园区被填满,而又要创建对象,就先对伊甸园区进行GC(Minor GC),然后把新创建的对象放到伊甸园区

3)、然后把伊甸园区剩下的对象放到S0区

4)、如果再次触发GC,S0区还没有被回收的,就会放到S1区

5)、如果再次触发GC,S1区还没有被回收的,就会回到S0区

6)、当经历GC的次数达到一定值(-XX:MaxTenuringThrehold,默认15),下次GC幸存后,就会被放到老年区

过程如下图所示,新的对象会放到伊甸园区,伊甸园区满后,触发Minor GC,红色对象被回收,绿色对象幸存,被移到S0区,年龄计数器数被设置为1

然后继续在伊甸园区新建对象,伊甸园区满后,触发Minor GC。由于S0区被占用,所以伊甸园区和S0区中幸存的绿色对象被移入S1区,同时各自的年龄计数器+1

S0被占用,幸存者移入S1;S1被占用,幸存者移入S0,幸存者的年龄计数器都要+1。如此循环往复,当有幸存者的年龄计数器达到16时,就会被晋升到老年区,此后年龄计数器就成了摆设了。

 

注意,S区满了不会触发Minor GC,而伊甸园区满时触发的Minor GC会连同S区也一起进行GC

 

垃圾回收时,频繁在新生代收集,很少在老年区收集,几乎不在元空间收集。

以下是垃圾回收的流程图

可见:

1)、如果Minor GC后,S区放不下从伊甸园区新来的幸存者,那么那些新的幸存者将直接晋升到老年代

2)、超大对象会先引起伊甸园区的Minor GC,然后发现空的伊甸园区也放不下自己,就直接进入老年代。如果老年代还放不下自己,就触发Full GC,如果Full GC后还是放不下,则抛出OOM

Minor GC、Major GC、Full GC

对于HotSpot,GC分为两种类型:部分收集和整堆收集

1)、部分收集可分为新生代收集(Minor GC、老年代收集(Major GC)和混合收集(Mixed GC)

目前只有CMS GC会有单独收集老年代的行为,混合收集会收集整个新生代和部分老年代,目前只有G1 GC才会有混合收集行为。

2)、整堆收集(Full GC):收集整个java堆和方法区

 

Major GC和Full GC经常一起发生

 

Minor GC触发机制:

1)、年轻代中伊甸园区空间不足会触发Minor GC,Survivor区满不会主动触发

2)、Minor GC会清理伊甸园区和Survivor区

3)、由于java对象大多都是朝生夕灭,所以Minor GC非常频繁,而且速度一般比较快

4)、Minor GC会引发STW(Stop The World),暂停其他用户线程,直到GC结束

触发机制的图示如下所示

 

Major GC触发机制:

1)、如果出现Major GC,则经常会伴随至少一次Minor GC(但在Parallel Scavenge收集器的收集策略里,就有直接进行Major GC的策略选择过程)。也就是老年代空间不足时,先尝试触发Minor GC。如果之后空间还是不足,就触发Major GC

2)、Major GC速度一般是Minor GC的10倍,STW时间更长

3)、如果Major GC后,内存还不够,就OOM

 

Full GC触发机制:

1)、调用System.gc()时,系统建议执行Full GC,但不是必然

2)、老年代空间不足

3)、方法区空间不足

4)、通过Minor GC后进入老年代的平均大小,大于老年代可用内存

5)、由伊甸园区、幸存者0(或1)区向幸存者1(或0)区复制时,对象大小大于目的区可用内存,则把此对象转存老年代,且老年代可用内存大小小于此对象大小

Full GC在开发或调优中尽量避免。

 

GC日志,使能 -XX:+PrintGCDetails后,某次GC日志如下所示

[GC (Allocation Failure) [PSYoungGen: 65019K->10732K(75776K)] 65019K->61402K(249344K), 0.0077635 secs] [Times: user=0.08 sys=0.05, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 75441K->10750K(140800K)] 126111K->126001K(314368K), 0.0083794 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 10750K->0K(140800K)] [ParOldGen: 115250K->125732K(266752K)] 126001K->125732K(407552K), [Metaspace: 3251K->3251K(1056768K)], 0.0122143 secs] [Times: user=0.13 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 129655K->10740K(140800K)] 255387K->255378K(407552K), 0.0138950 secs] [Times: user=0.08 sys=0.05, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 10740K->0K(140800K)] [ParOldGen: 244637K->255143K(485888K)] 255378K->255143K(626688K), [Metaspace: 3256K->3256K(1056768K)], 0.0362159 secs] [Times: user=0.28 sys=0.01, real=0.04 secs]
[GC (Allocation Failure) [PSYoungGen: 129796K->10745K(211456K)] 384940K->382181K(697344K), 0.0127149 secs] [Times: user=0.05 sys=0.11, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 10745K->0K(211456K)] [ParOldGen: 371435K->382032K(667648K)] 382181K->382032K(879104K), [Metaspace: 3256K->3256K(1056768K)], 0.0401030 secs] [Times: user=0.28 sys=0.00, real=0.04 secs]
[GC (Allocation Failure) [PSYoungGen: 200428K->10738K(254976K)] 582460K->582144K(922624K), 0.0199750 secs] [Times: user=0.06 sys=0.09, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 10738K->0K(254976K)] [ParOldGen: 571405K->581870K(973824K)] 582144K->581870K(1228800K), [Metaspace: 3256K->3256K(1056768K)], 0.0233874 secs] [Times: user=0.16 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 244224K->92661K(269312K)] 826094K->825371K(1243136K), 0.0260891 secs] [Times: user=0.13 sys=0.19, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 268958K->201201K(377856K)] 1001669K->1001187K(1351680K), 0.0247663 secs] [Times: user=0.03 sys=0.13, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 201201K->27807K(377856K)] [ParOldGen: 799986K->973215K(1408000K)] 1001187K->1001023K(1785856K), [Metaspace: 3256K->3256K(1056768K)], 0.0485004 secs] [Times: user=0.23 sys=0.08, real=0.05 secs]
[GC (Allocation Failure) [PSYoungGen: 204447K->203672K(486400K)] 1177663K->1176888K(1894400K), 0.0231965 secs] [Times: user=0.13 sys=0.19, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 440216K->281597K(518144K)] 1413432K->1412035K(1926144K), 0.0581611 secs] [Times: user=0.31 sys=0.30, real=0.06 secs]
[GC (Allocation Failure) [PSYoungGen: 517775K->398315K(652288K)] 1648214K->1647242K(2060288K), 0.0755326 secs] [Times: user=0.17 sys=0.55, real=0.08 secs]
[Full GC (Ergonomics) [PSYoungGen: 398315K->239109K(652288K)] [ParOldGen: 1248927K->1407983K(1874432K)] 1647242K->1647092K(2526720K), [Metaspace: 3256K->3256K(1056768K)], 0.1080069 secs] [Times: user=0.80 sys=0.13, real=0.11 secs]
[GC (Allocation Failure) [PSYoungGen: 493061K->461815K(715776K)] 1901044K->1900074K(2590208K), 0.0469455 secs] [Times: user=0.38 sys=0.02, real=0.05 secs]
[GC (Allocation Failure) [PSYoungGen: 715767K->461820K(804352K)] 2154026K->2145484K(2678784K), 0.0945251 secs] [Times: user=0.31 sys=0.55, real=0.09 secs]
[Full GC (Ergonomics) [PSYoungGen: 461820K->271099K(804352K)] [ParOldGen: 1683663K->1874139K(2419712K)] 2145484K->2145239K(3224064K), [Metaspace: 3767K->3767K(1056768K)], 0.2400625 secs] [Times: user=1.74 sys=0.19, real=0.24 secs]
[GC (Allocation Failure) [PSYoungGen: 613627K->461818K(804352K)] 2487767K->2485884K(3224064K), 0.0675779 secs] [Times: user=0.38 sys=0.14, real=0.07 secs]
[GC (Allocation Failure) [PSYoungGen: 804346K->461818K(915968K)] 2828412K->2826541K(3335680K), 0.0940708 secs] [Times: user=0.30 sys=0.56, real=0.09 secs]
[Full GC (Ergonomics) [PSYoungGen: 461818K->406827K(915968K)] [ParOldGen: 2364722K->2419530K(2771968K)] 2826541K->2826358K(3687936K), [Metaspace: 3767K->3767K(1056768K)], 0.1312200 secs] [Times: user=1.14 sys=0.01, real=0.13 secs]
[GC (Allocation Failure) --[PSYoungGen: 860971K->860971K(915968K)] 3280502K->3632753K(3687936K), 0.1318065 secs] [Times: user=0.45 sys=0.44, real=0.13 secs]
[Full GC (Ergonomics) [PSYoungGen: 860971K->461160K(915968K)] [ParOldGen: 2771781K->2771697K(2771968K)] 3632753K->3232858K(3687936K), [Metaspace: 3767K->3767K(1056768K)], 0.1855559 secs] [Times: user=1.56 sys=0.00, real=0.19 secs]
[Full GC (Ergonomics) [PSYoungGen: 915121K->915155K(915968K)] [ParOldGen: 2771697K->2771697K(2771968K)] 3686819K->3686853K(3687936K), [Metaspace: 3767K->3767K(1056768K)], 0.0985984 secs] [Times: user=0.89 sys=0.02, real=0.10 secs]
[Full GC (Ergonomics) [PSYoungGen: 915490K->915465K(915968K)] [ParOldGen: 2771697K->2771697K(2771968K)] 3687187K->3687162K(3687936K), [Metaspace: 3767K->3767K(1056768K)], 0.0112236 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 915465K->915465K(915968K)] [ParOldGen: 2771697K->2771643K(2771968K)] 3687162K->3687108K(3687936K), [Metaspace: 3767K->3767K(1056768K)], 0.3501174 secs] [Times: user=2.80 sys=0.05, real=0.35 secs]
Heap
PSYoungGen      total 915968K, used 915490K [0x000000076b700000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 454144K, 100% used [0x000000076b700000,0x0000000787280000,0x0000000787280000)
  from space 461824K, 99% used [0x00000007a3580000,0x00000007bf808948,0x00000007bf880000)
  to   space 461824K, 9% used [0x0000000787280000,0x0000000789c992e8,0x00000007a3580000)
ParOldGen       total 2771968K, used 2771647K [0x00000006c2400000, 0x000000076b700000, 0x000000076b700000)
  object space 2771968K, 99% used [0x00000006c2400000,0x000000076b6afed0,0x000000076b700000)
Metaspace       used 3797K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 414K, capacity 428K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at jvm.heap.HeapObjArrayNew.<init>(HeapObjArrayNew.java:12)
    at jvm.heap.HeapObjArrayNew.main(HeapObjArrayNew.java:40)


Process finished with exit code 1

其中某次Minor GC日志截取如下

[GC (Allocation Failure) [PSYoungGen: 65019K->10732K(75776K)] 65019K->61402K(249344K), 0.0077635 secs] [Times: user=0.08 sys=0.05, real=0.01 secs]

PSYoungGen: 65019K->10732K(75776K)表示新生代占用内存从65019K减少到了10732K,目前总共有75776K,里面的10732K想必就是幸存者区占用的空间了

后面的65019K->61402K(249344K)表示原来的堆空间占用了65019K,GC后变成了61402K,目前堆空间总共249344K。GC后堆占用空间(61402K)比幸存者区(10732K)大,说明有些数据直接进入了老年代

其中某次Full GC日志截取如下

[Full GC (Ergonomics) [PSYoungGen: 10740K->0K(140800K)] [ParOldGen: 244637K->255143K(485888K)] 255378K->255143K(626688K), [Metaspace: 3256K->3256K(1056768K)], 0.0362159 secs] [Times: user=0.28 sys=0.01, real=0.04 secs]

由PSYoungGen: 10740K->0K(140800K)可见新生代完全被清空了,而且新生代空间占用数也由之前的75776K涨到了140800K,翻了一倍,说明JVM为堆空间申请了新的内存

关于GC日志的参数设置,可以参见文章JVM学习之垃圾回收和垃圾回收器的GC日志分析部分,也可以参见文章JVM学习笔记之GC日志分析

堆空间分代思想

其实JVM不分代完全可以,分代的唯一理由就是优化GC性能。经常把新创建的、临时的对象进行回收,会腾出很大一块空间出来,而且效率会很高。

内存分配策略

针对不同年龄段的分配原则如下所示:

1)、优先分配到伊甸园区

2)、大对象直接分配到老年代(尽量避免程序中出现过多的大对象,更不要让大对象朝生夕死)

3)、长期存活的对象分配到老年代

4)、动态对象年龄判断:如果幸存者区相同年龄的所有对象总大小大于幸存者区空间的一半(即S0或S1的大小),年龄大于等于此年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄

5)、空间分配担保:-XX:HandlePromotionFailure

以如下代码为例,展示大对象会直接进入老年代

package jvm.heap;

public class HeapObjArrayNew {
    public static void main(String[] args) {
        byte[] bytes = new byte[1024 * 1024 * 20];
    }
}

代码构造了一个长度为20M的字节数组,程序配置堆内存为60M,打印GC日志

-Xms60m -Xmx60m -XX:+PrintGCDetails

GC日志输出如下

Heap
PSYoungGen      total 17920K, used 3126K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
  eden space 15360K, 20% used [0x00000000fec00000,0x00000000fef0d8d8,0x00000000ffb00000)
  from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)
  to   space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
ParOldGen       total 40960K, used 20480K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
  object space 40960K, 50% used [0x00000000fc400000,0x00000000fd800010,0x00000000fec00000)
Metaspace       used 3239K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K


Process finished with exit code 0

没有GC输出,说明没有垃圾回收;新生代总共只有17920K,不到20M,而老年区总共40960K,显示已经用了20480K,说明数组对象直接进入了老年区。

TLAB

堆是线程共享区域,由于对象的创建在JVM中非常频繁,所有在并发环境下从堆区划分内存空间是线程不安全的。为了避免多个线程操作同一地址,需要使用加锁等机制,但因此会影响分配速度

从内存模型而不是垃圾回收的角度,JVM对伊甸园区继续划分,为每一个线程分配了一个私有缓存区域TLAB(Thread Local Allocation Buffer) ,其包含在伊甸园区内

多线程同时分配内存时,使用TLAB可以避免一系列的线程不安全问题,同时提升了内存分配吞吐量,这种内存分配方式称之为快速分配策略。

当内存使用完自己的TLAB时,再使用公共的伊甸园区域

 

尽管不是所有的对象实例都能在TLAB中成功分配,但它依旧是内存分配的首选

可以通过选项-XX:+UseTLAB设置是否开启TLAB空间,默认开启

默认的TLAB空间非常小,只占整个伊甸园空间的1%,可以通过-XX:TLABWasteTargetPercent来设置TLAB占伊甸园空间的百分比

如果对象在TLAB中分配内存失败,JVM就会尝试使用加锁机制确保数据操作的原子性,从而在伊甸园公共空间中分配内存

 

使用TLAB时的对象分配流程图如下所示

堆参数设置小结

-XX:+PrintFlagsInitial:查看所有参数默认值

-XX:+PrintFlagsFinal: 查看所有参数实际值

-Xms:初始堆内存大小(默认物理内存1/64)

-Xmx:最大堆内存大小(默认物理内存1/4)

-Xmn:新生代大小,初始值与最大值

-XX:NewRatio:新生代与老年代在堆空间的占比(默认2,表示新生代:老年代为1:2)

-XX:SurvivorRatio:新生代中伊甸园区与幸存区占比(默认8,表示伊甸园区:幸存区为8:1:1)

-XX:MaxTenuringThreshold:设置新生代垃圾最大年龄

-XX:PrintGCDetails:打印详细GC日志

-XX:PrintGC:打印简要GC日志

-XX:HandlePromotionFailure:是否设置空间分配担保

查看某个具体参数值:

1)、jps:查看目标进程的进程号

2)、jinfo -flag XXX pid:查看指定pid进程的XXX参数值

 

对于空间分配担保的描述如下:

在Minor GC之前,JVM会检查老年代最大可用连续空间是否大于新生代所有对象的总大小

如果大于,则此次Minor GC安全

否则,虚拟机会查看-XX:HandlePromotionFailure

    如果值为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小

        如果大于,尝试进行一次Minor GC,但这次依旧有风险

        否则,会进行一次Full GC

    如果值为false,则直接进行Full GC

现在,此参数已经没有意义,直接为true。所以只要老年代的连续空间大小大于新生代对象总大小或历次晋升到老年代对象的平均大小,则进行Minor GC;否则进行Full GC.

堆不是分配对象的唯一选择

逃逸分析是JIT使用的一种有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法

逃逸分析的基本行为就是分析对象的动态作用域:

1)、当一个对象在方法内部被定义,并且只在方法内部使用,则认为没有发生逃逸

2)、当一个对象在方法内部被定义,却在外部方法被引用,则认为发生了逃逸

 

使用逃逸分析,编译器可以对代码做如下优化:

1)、栈上分配

2)、同步省略:如果发现锁对象只能被一个线程访问,那么就会取消对它的同步。例如以下代码

public void f() {
    Object o = new Object();
    synchronized (o) {
        System.out.println(o.toString());
    }
}

就会被优化成

public void f() {
    Object o = new Object();
    System.out.println(o.toString());
}

3)、分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的全部也可以被存放到栈中。

在JIT阶段,如果一个对象不会逃逸出方法,就会把此对象拆分成若干个成员变量来代替

class Point {
    private int x;
    private int y;
    private Size size;
}


class Size {
    float size;
}


public void g() {
    Point p = new Point();
    p.x = 1;
    p.y = 2;
    p.size = new Size();
    p.size.size = p.x * p.y;
    System.out.println(p.toString());
}

以上代码中p只在g()函数中被使用,那么p就可以被拆分成两个int和一个float放到栈上,这个过程就叫做标量替换

这三种优化都可以减少GC频率,减少代码执行时间。

 

不过HotSpot目前没有开启逃逸分析,所以它所有对象都在堆上被分配

结语

以上是关于堆的全部内容,下一篇笔记JVM学习笔记之方法区是关于方法区的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值