【JVM】06 堆空间

笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)

目录

6.  堆

6.1. 堆(Heap)的核心概述

6.1.1. 堆内存细分

6.1.2. 堆空间内部结构(JDK7)

 6.1.3. 堆空间内部结构(JDK8)

 6.1.4. VisualVM使用

6.2. 设置堆内存大小与OOM

6.2.1. 堆空间大小的设置

 6.2.2. OutOfMemory举例

6.3. 年轻代与老年代

6.4. 图解对象分配过程

6.4.1 图解对象分配(一般情况)

6.4.2 对象分配的特殊情况

6.5. Minor GC,MajorGC、Full GC

6.5.1. 最简单的分代式GC策略的触发条件

年轻代GC(Minor GC)触发机制

 老年代GC(Major GC / Full GC)触发机制

Full GC触发机制(后面细讲)

6.6. 堆空间分代思想

6.7. 内存分配策略

6.8. 为对象分配内存:TLAB

6.8.1. 为什么有TLAB(Thread Local Allocation Buffer)?

6.8.2. 什么是TLAB?

6.8.3. TLAB的再说明

6.9. 小结:堆空间的参数设置

空间分配担保

小结


6.  堆

6.1. 堆(Heap)的核心概述

堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。

image-20200706195127740

  1. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  2. Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。
  3. 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
  4. 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
  5. 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated
  6. 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
  7. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
  8. 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

随着JVM的迭代升级,原来一些绝对的事情,在后续版本中也开始有了特例,变的不再那么绝对。

public class SimpleHeap {
    private int id;//属性、成员变量

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

    public void show() {
        System.out.println("My ID is " + id);
    }
    public static void main(String[] args) {
        SimpleHeap sl = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);

        int[] arr = new int[10];

        Object[] arr1 = new Object[10];
    }
}

image-20200706201904057

6.1.1. 堆内存细分

Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区

  • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
  • Tenure generation space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm

Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间

  • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
  • Tenure generation space 养老区 Old/Tenure
  • Meta Space 元空间 Meta

约定:新生区(代)<=>年轻代 、 养老区<=>老年区(代)、 永久区<=>永久代

6.1.2. 堆空间内部结构(JDK7)

image-20200706203419496

 6.1.3. 堆空间内部结构(JDK8)

堆空间内部结构,JDK1.8之前从永久代 替换成 元空间

image-20200706203835403

VisualVM可视化查看堆内存

运行下面代码

public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            TimeUnit.MINUTES.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("end...");
    }

}

 6.1.4. VisualVM使用

方式一:

1、在IDEA插件中下载VisualVM,下载后点击Apply

 2、运行代码Run with VisualVM

 3、在VisualVM中,工具 -> 插件 -> 安装Visual GC插件

 4、查看VisualVM

方式二:打开命令行输入 JVisualVM即可

6.2. 设置堆内存大小与OOM

6.2.1. 堆空间大小的设置

1.Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xms"和"-Xmx"来进行设置。

  • “-Xms"用于表示堆区的起始内存,等价于-XX:InitialHeapSize
  • “-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapSize

2.一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutofMemoryError异常。

3.通常会将-Xms和-Xmx两个参数配置相同的值

  • 原因:假设两个不一样,初始内存小,最大内存大。在运行期间如果堆内存不够用了,会一直扩容直到最大内存。如果内存够用且多了,也会不断的缩容释放。频繁的扩容和释放造成不必要的压力,避免在GC之后调整堆内存给服务器带来压力。
  • 如果两个设置一样的就少了频繁扩容和缩容的步骤。内存不够了就直接报OOM
  • 为了能够在ava垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能

4.默认情况下:

  1. 初始内存大小:物理电脑内存大小 / 64
  2. 最大内存大小:物理电脑内存大小 / 4
/**
 * 1. 设置堆空间大小的参数
 * -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
 *      -X 是jvm的运行参数
 *      ms 是memory start
 * -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
 *
 * 2. 默认堆空间的大小
 *    初始内存大小:物理电脑内存大小 / 64
 *             最大内存大小:物理电脑内存大小 / 4
 * 3. 手动设置:-Xms600m -Xmx600m
 *     开发中建议将初始堆内存和最大的堆内存设置成相同的值。
 *
 * 4. 查看设置的参数:方式一: jps   /  jstat -gc 进程id
 *                  方式二:-XX:+PrintGCDetails
 */
public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");

        System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
        System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

 电脑内存大小是16G,不足16G的原因是操作系统自身还占据了一些。

设置下参数再看:

VM option:

-Xms600m -Xmx600m 

public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");


        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

-Xms : 575M
-Xmx : 575M

为什么会少25M?

方式一: jps / jstat -gc 进程id

jps:查看java进程

jstat:查看某进程内存使用情况

SOC: Survive 0区 总容量
S1C: Survive 1区 总容量
S0U: Survive 0区 使用的量
S1U: Survive 1区 使用的量
EC: 伊甸园区总容量
EU: 伊甸园区使用的量
OC: 老年代总容量
OU: 老年代使用的量

计算方式:

1、S0C + S1C + EC + OC = 25600+25600+153600+409600 = 614400K

614400 /1024 = 600M
2、S0C或S1C + EC + OC = 25600+153600+409600 = 588800K

588800 / 1024 = 575M

并非巧合,S0区和S1区两个只有一个能使用,另一个用不了

方式二:设置VM option:

-Xms600m -Xmx600m -XX:+PrintGCDetails

 6.2.2. OutOfMemory举例

public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture{
    private byte[] pixels;

    public Picture(int length) {
        this.pixels = new byte[length];
    }
}

1、设置虚拟机参数

-Xms600m -Xmx600m

最终输出结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.lhk.Picture.<init>(OOMTest.java:24)
	at com.lhk.OOMTest.main(OOMTest.java:15)

2、堆内存变化图

 3、原因:不断创建对象丢入堆中导致堆内存溢出

6.3. 年轻代与老年代

存储在JVM中的Java对象可以被划分为两类:

  • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
  • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致

Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)

其中年轻代又可以划分为Eden空间Survivor0空间Survivor1空间(有时也叫做from区、to区)

image-20200707075847954

下面这参数开发中一般不会调:

image-20200707080154039

配置新生代与老年代在堆结构的占比。

  • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

1、在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1(但是实际运行会发现是6:1:1

2、当然开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例。比如-xx:SurvivorRatio=8

3、几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。

  • IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。

4、可以使用选项"-Xmn"设置新生代最大内存大小,这个参数一般使用默认值就可以了。

image-20210510105849497

/**
 * -Xms600m -Xmx600m
 *
 * -XX:NewRatio : 设置新生代与老年代的比例。默认值是2.
 * -XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8
 * -XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略  (暂时用不到)
 * -Xmn:设置新生代的空间的大小。 (一般不设置)
 */
public class EdenSurvivorTest {
    public static void main(String[] args) {
        System.out.println("我只是来打个酱油~");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

6.4. 图解对象分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

1、new的对象先放伊甸园区。此区有大小限制。

2、当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区

3、然后将伊甸园中的剩余对象移动到幸存者0区。

4、如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。

5、如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。

  • 啥时候能去养老区呢?可以设置次数。默认是15次

6、可以设置参数:进行设置-Xx:MaxTenuringThreshold= N

7、在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理

8、若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。

java.lang.OutofMemoryError: Java heap space

6.4.1 图解对象分配(一般情况)

1、我们创建的对象,一般都是存放在Eden区的,当我们Eden区满了后,就会触发GC操作,一般被称为 YGC / Minor GC操作。

 2、当我们进行一次垃圾收集后,红色的对象将会被回收,而绿色的独享还被占用着,然后将绿色的存放在S0(Survivor From)区。同时我们给每个对象设置了一个年龄计数器,经过一次回收后还存在的对象,将其年龄加 1,此时绿色的年龄为1。

3、同时Eden区继续存放对象,当Eden区再次存满的时候,又会触发一个MinorGC操作,此时GC将会把 Eden和Survivor From中的对象进行一次垃圾收集,把存活的对象放到 Survivor To(S1)区,同时让存活的对象年龄 + 1。

下一次再进行GC的时候,

① 这一次的s0区为空,所以成为下一次GC的S1区

② 这一次的s1区则成为下一次GC的S0区

③ 也就是说s0区和s1区在互相转换。(空的区域为To区)

 4、我们继续不断的进行对象生成和垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,也就是将年轻代中的对象晋升到老年代中。

总结:

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
  • 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集


6.4.2 对象分配的特殊情况

1、如果来了一个新对象,先看看 Eden 是否放的下?

  • 如果 Eden 放得下,则直接放到 Eden 区
  • 如果 Eden 放不下,则触发 YGC ,执行垃圾回收,看看还能不能放下?

2、将对象放到老年区又有两种情况:

  • 如果 Eden 执行了 YGC 还是无法放不下该对象,那没得办法,只能说明是超大对象,只能直接放到老年代
  • 那万一老年代都放不下,则先触发FullGC ,再看看能不能放下,放得下最好,但如果还是放不下,那只能报 OOM

3、如果 Eden 区满了,将对象往幸存区拷贝时,发现幸存区放不下啦,那只能便宜了某些新对象,让他们直接晋升至老年区

流程图

image-20200707091058346

常用调优工具

  • JDK命令行
  • Eclipse:Memory Analyzer Tool
  • Jconsole
  • VisualVM
  • Jprofiler
  • Java Flight Recorder
  • GCViewer
  • GC Easy

6.5. Minor GC,MajorGC、Full GC

1、我们都知道,JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW(Stop the World)(用户线程停止,等待垃圾回收进程完成垃圾回收后再启动)的问题,而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上。

2、JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)。

1、部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

   ①新生代收集(Minor GC / Young GC):只是新生代的垃圾收集

   ②老年代收集(Major GC / Old GC):只是老年代的圾收集。

  • 目前,只有CMSGC会有单独收集老年代的行为。
  • 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收

   ③混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。

  • 目前,只有G1 GC会有这种行为

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

6.5.1. 最简单的分代式GC策略的触发条件

年轻代GC(Minor GC)触发机制

  • 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。)
  • 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

image-20200707095606813

 老年代GC(Major GC / Full GC)触发机制

1、指发生在老年代的GC,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了

2、出现了Major Gc,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)

  • 也就是在老年代空间不足时,会先尝试触发Minor Gc。如果之后空间还不足,则触发Major GC

3、Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长

4、如果Major GC后,内存还不足,就报OOM了

Full GC触发机制(后面细讲)

触发Full GC执行的情况有如下五种:

  1. 调用System.gc()时,系统建议执行FullGC,但是不必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  5. 由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
     

说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些。

GC日志分析

/**
 * 测试MinorGC 、 MajorGC、FullGC
 * -Xms9m -Xmx9m -XX:+PrintGCDetails
 */
public class GCTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "atguigu.com";
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }

        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("遍历次数为:" + i);
        }
    }
}

输出:

[GC (Allocation Failure) [PSYoungGen: 2043K->496K(2560K)] 2043K->825K(9728K), 0.0006634 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1964K->480K(2560K)] 2293K->1518K(9728K), 0.0013489 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 1927K->320K(2560K)] 2966K->2766K(9728K), 0.0009950 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1115K->0K(2560K)] [ParOldGen: 6670K->4691K(7168K)] 7785K->4691K(9728K), [Metaspace: 3056K->3056K(1056768K)], 0.0031938 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 4691K->4691K(9728K), 0.0003987 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 4691K->4583K(7168K)] 4691K->4583K(9728K), [Metaspace: 3056K->3056K(1056768K)], 0.0046575 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
遍历次数为:16
Heap
 PSYoungGen      total 2560K, used 106K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 2048K, 5% used [0x00000007bfd00000,0x00000007bfd1a858,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 7168K, used 4583K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
  object space 7168K, 63% used [0x00000007bf600000,0x00000007bfa79da0,0x00000007bfd00000)
 Metaspace       used 3116K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K
分析第一行
[GC (Allocation Failure) [PSYoungGen: 2043K->496K(2560K)] 2043K->825K(9728K), 0.0006634 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
  • [PSYoungGen: 2043K->496K(2560K)]:年轻代总空间为 2560K ,当前占用 2043K ,经过垃圾回收后剩余496K

  • 2043K->825K(9728K):堆内存总空间为 9728K ,当前占用2043K ,经过垃圾回收后剩余825K

6.6. 堆空间分代思想

为什么要把Java堆分代?不分代就不能正常工作了吗?

经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。

  • 新生代:有Eden、两块大小相同的survivor(又称为from/to,s0/s1)构成,to总为空。
  • 老年代:存放新生代中经历多次GC仍然存活的对象。

image-20200707101511025

其实不分代完全可以,分代的唯一理由就是优化GC性能

  • 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。(性能低)
  • 而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。(多回收新生代,少回收老年代,性能会提高很多)。

image-20200707101543871

6.7. 内存分配策略

  1. 如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
  2. 对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代。
  3. 对象晋升老年代的年龄阀值,可以通过选项 -XX:MaxTenuringThreshold 来设置。

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

  1. 优先分配到Eden:开发中比较长的字符串或者数组,会直接存在老年代,但是因为新创建的对象都是朝生夕死的,所以这个大对象可能也很快被回收,但是因为老年代触发Major GC的次数比 Minor GC要更少,因此可能回收起来就会比较慢
  2. 大对象直接分配到老年代:尽量避免程序中出现过多的大对象
  3. 长期存活的对象分配到老年代
  4. 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold中要求的年龄。
  5. 空间分配担保: -XX:HandlePromotionFailure 。
     

6.8. 为对象分配内存:TLAB

6.8.1. 为什么有TLAB(Thread Local Allocation Buffer)?

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

6.8.2. 什么是TLAB?

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
  • 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
  • 每个线程都有一个TLAB空间
  • 当一个线程的TLAB存满时,可以使用公共区域(蓝色)的

image-20210510114110526

6.8.3. TLAB的再说明

  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
  • 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项 “-XX:TLABWasteTargetPercent” 设置TLAB空间所占用Eden空间的百分比大小。
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

TLAB 分配过程

image-20200707104253530

6.9. 小结:堆空间的参数设置

官网地址:javahttps://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

  • ①. -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
  • ②. -XX:+PrintFlagsFinal : 查看所有的参数的最终值(可能会存在修改(:表示修改了),不再是初始值)
  • ③. 具体查看某个参数的指令:
    • (jps:查看当前运行中的进程
    • jinfo -flag SurvivorRatio 进程id)
  • ④. -Xms:初始堆空间内存 (默认为物理内存的1/64)
  • ⑤. -Xmx:最大堆空间内存(默认为物理内存的1/4)
  • ⑥. -Xmn:设置新生代的大小。(初始值及最大值)
  • ⑦. -XX:NewRatio:配置新生代与老年代在堆结构的占比
  • (默认:-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5)

空间分配担保

1、在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。

  •  如果大于,则此次Minor GC是安全的
  • 如果小于,则虚拟机会查看 -XX:HandlePromotionFailure 设置值是否允担保失败。
    •  如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
      •   如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
      •   如果小于,则进行一次Full GC。
  • 如果HandlePromotionFailure=false,则进行一次Full GC。 
     

历史版本

  1. 在JDK6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。
  2. JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。即 HandlePromotionFailure=true
     

小结

  1. 年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
  2. 老年代放置长生命周期的对象,通常都是从Survivor区域筛选拷贝过来的Java对象。
  3. 当然,也有特殊情况,我们知道普通的对象可能会被分配在TLAB上;
  4. 如果对象较大,无法分配在 TLAB 上,则JVM会试图直接分配在Eden其他位置上;
  5. 如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。
  6. 当GC只发生在年轻代中,回收年轻代对象的行为被称为Minor GC。
  7. 当GC发生在老年代时则被称为Major GC或者Full GC。
  8. 一般的,Minor GC的发生频率要比Major GC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。
     

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值