JVM 的艺术 - 对象创立与内存分配机制深度分析
引言
本章将介绍 jvm 的对象创立与内存调配。彻底带你理解 jvm 的创立过程以及内存调配的原理和区域,以及蕴含的内容。
对象的创立
类加载的过程
固定的类加载执行程序: 加载 验证 筹备 初始化 卸载 的执行程序是肯定的 为什么解析过程没有在这个执行程序中?(接下来剖析)
什么时候触发类加载不肯定,然而类的初始化如下四种状况就要求肯定初始化。然而初始化之前 就肯定会执行 加载 验证 筹备 三个阶段。
触发类加载的过程(由初始化过程引起的类加载)
1):应用 new 关键字 获取一个动态属性 设置一个动态属性 调用一个静态方法。
int myValue = SuperClass.value; 会导致父类初始化,然而不会导致子类初始化
SuperClass.Value = 3 ; 会导致父类初始化,不会导致子类初始化。
SubClass.staticMethod(); 先初始化父类 再初始化子类
SubClass sc = new SubClass(); 先初始化父类 子类初始化子类
2):应用反射的时候,若发现类还没有初始化,就会进行初始化
Class clazz = Class.forName(“com.hnnd.classloader.SubClass”);
3):在初始化一个类的时,若发现其父类没有初始化,就会先初始化父类
SubClass.staticMethod(); 先初始化父类 在初始化子类
4):启动虚拟机的时候,须要加载蕴含 main 办法的类.
class SuperClass{
public static int value = 5;
static {System.out.println("Superclass ...... init........");
}
}
class SubClass extends SuperClass {
static {System.out.println("subClass********************init");
}
public static void staticMethod(){System.out.println("superclass value"+SubClass.value);
}
}
1: 加载
1.1)依据全类名获取到对应类的字节码流(字节流的起源 class 文件,网络文件,还有反射的 Proxygeneraotor.generaotorProxyClass)
1.2)把字节流中的静态数据构造加载到办法区中的运行时数据结构
1.3)在内存中生成 java.lang.Class 对象,能够通过该对象来操作方法区中的数据结构(通过反射)
2: 验证
文件格式的验证: 验证 class 文件结尾的 0XCAFFBASE 结尾
验证主次版本号是否在以后的虚拟机的范畴之类
检测 jvm 不反对的常量类型
元数据的校验:
验证本类是否有父类
验证是否继承了不容许继承的类 (final) 润饰的类
验证本类不是抽象类的时候,是否实现了所有的接口和父类的接口
字节码验证:验证跳转指令跳转到 办法以外的指令.
验证类型转换是否为无效的,比方子类对象赋值父类的援用是能够的,然而把父类对象赋值给子类援用是危险的
总而言之: 字节码验证通过,并不能阐明该字节码肯定没有问题,然而字节码验证不通过。那么该字节码文件肯定是有问题:。
符号援用的验证(产生在解析的过程中):
通过字符串形容的全类名是否能找到对应的类。
指定类中是否蕴含字段描述符,以及简略的字段和办法名称。
3: 筹备: 为类变量分配内存以及设置初始值。
比方 public static int value = 123;
在筹备的过程中 value=0 而不是 123,当执行类的初始化的办法的时候,value=123
若是一个动态常量
public static final int value = 9; 那么在筹备的过程中 value 为 9.
4: 解析:把符号援用替换成间接援用
符号援用分类:
CONSTANT_Class_info 类或者接口的符号援用
CONSTANT_Fieldref_info 字段的符号援用
CONSTANT_Methodref_info 办法的符号援用
CONSTANT_intfaceMethodref_info- 接口中办法的符号援用
CONSTANT_NameAndType_info 子类或者办法的符号援用.
CONSTANT_MethodHandle_Info 办法句柄
CONSTANT_InvokeDynamic_Info 动静调用
间接援用:
指向对象的指针
绝对偏移量
操作句柄
5: 初始化:类的初始化时类加载的最初一步: 执行类的结构器,为所有的类变量进行赋值(编译器生成 CLInit<>)
类结构器是什么?:类结构器是编译器依照 Java 源文件总类变量和动态代码块呈现的程序来决定
动态语句只能拜访定义在动态语句之前的类变量,在其后的动态变量能赋值 然而不能拜访。
父类中的动态代码块优先于子类动态代码块执行。
若类中没有动态代码块也没有动态类变量的话,那么编译器就不会生成 Clint<> 类结构器的办法。
public class TestClassInit {public static void main(String[] args) {System.out.println(SubClass.sub_before_v);
}
}
class SubClass extends SuperClass{
public static int sub_before_v = 5;
static {
sub_before_v = 10;
System.out.println("subclass init.......");
sub_after_v=0;
// 抛错,static 代码块中的代码只能赋值前面的类变量 然而不能拜访。sub_before_v = sub_after_v;
}
public static int sub_after_v = 10;
}
class SuperClass {
public static int super_before_v = 5;
static{System.out.println("superclass init......");
}
public static int super_after_v = 10;
}
6: 应用
7: 卸载
1.类加载查看
虚拟机遇到一条 new 指令时,首先将去查看这个指令的参数是否能在常量池中定位到一个类的符号援用,并且查看这个
符号援用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new 指令对应到语言层面上讲是,new 关键词、对象克隆、对象序列化等。
2.分配内存
在类加载查看通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载实现后便可齐全确定,为
对象调配空间的工作等同于把 一块确定大小的内存从 Java 堆中划分进去。
这个步骤有两个问题:
1. 如何划分内存。
2. 在并发状况下,可能呈现正在给对象 A 分配内存,指针还没来得及批改,对象 B 又同时应用了原来的指针来分配内存的
状况。
划分内存的办法:
内存的办法:
“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
假如 Java 堆中内存时残缺的,已调配的内存和闲暇内存别离在不同的一侧,通过一个指针作为分界点,须要分配内存时,
仅仅须要把指针往闲暇的一端挪动与对象大小相等的间隔。应用的 GC 收集器:Serial、ParNew,实用堆内存规整(即没有内存碎片)的状况下。
“闲暇列表”(Free List)
事实上,Java 堆的内存并不是残缺的,已调配的内存和闲暇内存互相交织,JVM 通过保护一个列表,记录可用的内存块信息,当调配操作产生时,从列表中找到一个足够大的内存块调配给对象实例,并更新列表上的记录。应用的 GC 收集器:CMS,实用堆内存不规整的状况下。
解决并发问题的办法:
CAS(compare and swap)
虚拟机采纳 CAS 配上失败重试的形式保障更新操作的原子性来对分配内存空间的动作进行同步解决。
本地线程调配缓冲(Thread Local Allocation Buffer,TLAB)
把内存调配的动作依照线程划分在不同的空间之中进行,即每个线程在 Java 堆中事后调配一小块内存。通过XX:+/
UseTLAB参数来设定虚拟机是否应用 TLAB(JVM 会默认开启XX:+UseTLAB),XX:TLABSize 指定 TLAB 大小。
3.初始化
内存调配实现后,虚拟机须要将调配到的内存空间都初始化为零值(不包含对象头),如果应用 TLAB,这一工作过程也
能够提前至 TLAB 调配时进行。这一步操作保障了对象的实例字段在 Java 代码中能够不赋初始值就间接应用,程序能拜访
到这些字段的数据类型所对应的零值。
什么是 TLAB
TLAB(Thread Local Allocation Buffer,线程本地调配缓冲区)是 Java 中内存调配的一个概念,它是在 Java 堆中划分进去的针对每个线程的内存区域,专门在该区域为该线程创立的对象分配内存。它的次要目标是在多线程并发环境下须要进行内存调配的时候,缩小线程之间对于内存调配区域的竞争,减速内存调配的速度。TLAB 实质上还是在 Java 堆中的,因而在 TLAB 区域的对象,也能够被其余线程拜访。
如果没有启用 TLAB,多个并发执行的线程须要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个地位申请,这时就须要对拟调配的内存区域进行加锁或者采纳 CAS 等操作,保障这个区域只能调配给一个线程。
启用了 TLAB 之后(-XX:+UseTLAB, 默认是开启的),JVM 会针对每一个线程在 Java 堆中预留一个内存区域,在预留这个动作产生的时候,须要进行加锁或者采纳 CAS 等操作进行爱护,防止多个线程预留同一个区域。一旦某个区域确定划分给某个线程,之后该线程须要分配内存的时候,会优先在这片区域中申请。这个区域针对分配内存这个动作而言是该线程公有的,因而在调配的时候不必进行加锁等保护性的操作。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何能力找到类的元数据信息、对
象的哈希码、对象的 GC 分代年龄等信息。这些信息寄存在对象的对象头 Object Header 之中。
在 HotSpot 虚拟机中,对象在内存中存储的布局能够分为 3 块区域:对象头(Header)、实例数据(Instance Data)
和对齐填充(Padding)。HotSpot 虚拟机的对象头包含两局部信息,第一局部用于存储对象本身的运行时数据,如哈
希码(HashCode)、GC 分代年龄、锁状态标记、线程持有的锁、偏差线程 ID、偏差时 间戳等。对象头的另外一部分
是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
对象头在 hotspot 的 C ++ 源码里的正文如下:
1 Bit‐format of an object header (most significant first, big endian layout below):
2 //
3 // 32 bits:
4 // ‐‐‐‐‐‐‐‐
5 // hash:25 ‐‐‐‐‐‐‐‐‐‐‐‐>| age:4 biased_lock:1 lock:2 (normal object)
6 // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
7 // size:32 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
8 // PromotedObject*:29 ‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
9 //
10 // 64 bits:
11 // ‐‐‐‐‐‐‐‐
12 // unused:25 hash:31 ‐‐>| unused:1 age:4 biased_lock:1 lock:2 (normal object)
13 // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
14 // PromotedObject*:61 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
15 // size:64 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
16 //
17 // unused:25 hash:31 ‐‐>| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
18 // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
19 // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ‐‐‐‐‐>| (COOPs && CMS promoted object)
20 // unused:21 size:35 ‐‐>| cms_free:1 unused:7 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (COOPs && CMS free block)
5.执行 <init> 办法
执行 <init> 办法,即对象依照程序员的志愿进行初始化。对应到语言层面上讲,就是为属性赋值(留神,这与下面的赋
零值不同,这是由程序员赋的值),和执行构造方法。
对象大小与指针压缩
对象大小能够用 jolcore 包查看,引入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.9</version> 5 </dependency>
1 import org.openjdk.jol.info.ClassLayout;
2
3 /**
4 * 计算对象大小
5 */
6 public class JOLSample {
7
8 public static void main(String[] args) {9 ClassLayout layout = ClassLayout.parseInstance(new Object());
10 System.out.println(layout.toPrintable());
11
12 System.out.println();
13 ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
14 System.out.println(layout1.toPrintable());
15
16 System.out.println();
17 ClassLayout layout2 = ClassLayout.parseInstance(new A());
18 System.out.println(layout2.toPrintable());
19 }
20
21 // ‐XX:+UseCompressedOops 默认开启的压缩所有指针
22 // ‐XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针 Klass Pointer
23 // Oops : Ordinary Object Pointers
24 public static class A {
25 //8B mark word
26 //4B Klass Pointer 如果敞开压缩‐XX:‐UseCompressedClassPointers 或‐XX:‐UseCompressedOops,则占用 8B
27 int id; //4B
28 String name; //4B 如果敞开压缩‐XX:‐UseCompressedOops,则占用 8B
29 byte b; //1B
30 Object o; //4B 如果敞开压缩‐XX:‐UseCompressedOops,则占用 8B
31 }
32 }
33
34
35 运行后果:36 java.lang.Object object internals:
37 OFFSET SIZE TYPE DESCRIPTION VALUE
38 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) //mark word
39 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) //mark word
40 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (‐134217243) //Klass Pointer
41 12 4 (loss due to the next object alignment)
42 Instance size: 16 bytes
43 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
44
45
46 [I object internals:
47 OFFSET SIZE TYPE DESCRIPTION VALUE
48 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
49 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
50 8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (‐134217363)
51 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
52 16 0 int [I.<elements> N/A
53 Instance size: 16 bytes
54 Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
55
56
57 com.tuling.jvm.JOLSample$A object internals: 58 OFFSET SIZE TYPE DESCRIPTION VALUE
58 OFFSET SIZE TYPE DESCRIPTION VALUE
59 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000
60 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
61 8 4 (object header) 61 cc 00 f8 (01100001 11001100 00000000 11111000) (‐134165407)
62 12 4 int A.id 0
63 16 1 byte A.b 0
64 17 3 (alignment/padding gap)
65 20 4 java.lang.String A.name null
66 24 4 java.lang.Object A.o null
67 28 4 (loss due to the next object alignment)
68 Instance size: 32 bytes 69 Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
什么是 java 对象的 指针压缩?
1.jdk1.6 update14 开始,在 64bit 操作系统中,JVM 反对指针压缩
2.jvm 配置参数:UseCompressedOops,compressed压缩、oop(ordinary object pointer)对象指针
3. 启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops
为什么要进行指针压缩?
1. 在 64 位平台的 HotSpot 中应用 32 位指针,内存应用会多出 1.5 倍左右,应用较大指针在主内存和缓存之间挪动数据,
占用较大宽带,同时 GC 也会接受较大压力
2. 为了缩小 64 位平台下内存的耗费,启用指针压缩性能
3. 在 jvm 中,32 位地址最大反对 4G 内存(2 的 32 次方),能够通过对对象指针的压缩编码、解码形式进行优化,使得 jvm
只用 32 位地址就能够反对更大的内存配置(小于等于 32G)
4. 堆内存小于 4G 时,不须要启用指针压缩,jvm 会间接去除高 32 位地址,即应用低虚拟地址空间
5. 堆内存大于 32G 时,压缩指针会生效,会强制应用 64 位 (即 8 字节) 来对 java 对象寻址,这就会呈现 1 的问题,所以堆内
存不要大于 32G 为好 .
对象内存调配
对象内存调配流程图
对象栈上调配
咱们通过 JVM 内存调配能够晓得 JAVA 中的对象都是在堆上进行调配,当对象没有被援用的时候,须要依附 GC 进行回收内
存,如果对象数量较多的时候,会给 GC 带来较大压力,也间接影响了利用的性能。为了缩小长期对象在堆内调配的数量,JVM 通过 逃逸剖析 确定该对象不会被内部拜访。如果不会逃逸能够将该对象在 栈上调配 内存,这样该对象所占用的
内存空间就能够随栈帧出栈而销毁,就加重了垃圾回收的压力。
对象逃逸剖析:就是剖析对象动静作用域,当一个对象在办法中被定义后,它可能被内部办法所援用,例如作为调用参
数传递到其余中央中。
很显然 test1 办法中的 user 对象被返回了,这个对象的作用域范畴不确定,test2 办法中的 user 对象咱们能够确定当办法结
束这个对象就能够认为是有效对象了,对于这样的对象咱们其实能够将其调配在栈内存里,让其在办法完结时追随栈内
存一起被回收掉。
JVM 对于这种状况能够通过开启逃逸剖析参数 (-XX:+DoEscapeAnalysis) 来优化对象内存调配地位,使其通过 标量替换 优
先调配在栈上 ( 栈上调配),JDK7 之后默认开启逃逸剖析,如果要敞开应用参数(-XX:-DoEscapeAnalysis)
标量替换:通过逃逸剖析确定该对象不会被内部拜访,并且对象能够被进一步合成时,JVM 不会创立该对象,而是将该
对象成员变量合成若干个被这个办法应用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上调配空间,这样就
不会因为没有一大块间断空间导致对象内存不够调配。开启标量替换参数(-XX:+EliminateAllocations),JDK7 之后默认
开启。
标量与聚合量:标量即不可被进一步合成的量,而 JAVA 的根本数据类型就是标量(如:int,long 等根本数据类型以及
reference 类型等),标量的对抗就是能够被进一步合成的量,而这种量称之为聚合量。而在 JAVA 中对象就是能够被进一
步合成的聚合量。
栈上调配示例:
论断:栈上调配依赖于逃逸剖析和标量替换
对象在 Eden 区调配
大多数状况下,对象在新生代中 Eden 区调配。当 Eden 区没有足够空间进行调配时,虚拟机将发动一次 Minor GC。我
们来进行理论测试一下。
在测试之前咱们先来看看 Minor GC 和 Full GC 有什么不同呢?
Minor GC/Young GC:指产生新生代的的垃圾收集动作,Minor GC 十分频繁,回收速度个别也比拟快。
Major GC/Full GC:个别会回收老年代,年老代,办法区的垃圾,Major GC 的速度个别会比 Minor GC 的慢
10 倍以上。
Eden 与 Survivor 区默认 8:1:1
大量的对象被调配在 eden 区,eden 区满了后会触发 minor gc,可能会有 99% 以上的对象成为垃圾被回收掉,残余存活
的对象会被挪到为空的那块 survivor 区,下一次 eden 区满了后又会触发 minor gc,把 eden 区和 survivor 区垃圾对象回
收,把残余存活的对象一次性移动到另外一块为空的 survivor 区,因为新生代的对象都是朝生夕死的,存活工夫很短,所
以 JVM 默认的 8:1:1 的比例是很适合的,让 eden 区尽量的大,survivor 区够用即可,
JVM 默认有这个参数 -XX:+UseAdaptiveSizePolicy(默认开启),会导致这个 8:1:1 比例主动变动,如果不想这个比例有变
化能够设置参数 -XX:-UseAdaptiveSizePolicy
示例:
咱们能够看出 eden 区内存简直曾经被调配齐全(即便程序什么也不做,新生代也会应用至多几 M 内存)。如果咱们再为
allocation2 分配内存会呈现什么状况呢?
1 // 增加运行 JVM 参数:‐XX:+PrintGCDetails
2 public class GCTest {3 public static void main(String[] args) throws InterruptedException {4 byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
5 allocation1 = new byte[60000*1024];
6
7 allocation2 = new byte[8000*1024];
8
9 /*allocation3 = new byte[1000*1024];
10 allocation4 = new byte[1000*1024];
11 allocation5 = new byte[1000*1024];
12 allocation6 = new byte[1000*1024];*/
13 }
14 }
15
16 运行后果:17 [GC (Allocation Failure) [PSYoungGen: 65253K‐>936K(76288K)] 65253K‐>60944K(251392K), 0.0279083 secs] [Times:
user=0.13 sys=0.02, real=0.03 secs]
18 Heap
19 PSYoungGen total 76288K, used 9591K [0x000000076b400000, 0x0000000774900000, 0x00000007c0000000)
20 eden space 65536K, 13% used [0x000000076b400000,0x000000076bc73ef8,0x000000076f400000)
21 from space 10752K, 8% used [0x000000076f400000,0x000000076f4ea020,0x000000076fe80000)
22 to space 10752K, 0% used [0x0000000773e80000,0x0000000773e80000,0x0000000774900000)
23 ParOldGen total 175104K, used 60008K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
24 object space 175104K, 34% used [0x00000006c1c00000,0x00000006c569a010,0x00000006cc700000)
25 Metaspace used 3342K, capacity 4496K, committed 4864K, reserved 1056768K
26 class space used 361K, capacity 388K, committed 512K, reserved 1048576K
简略解释一下为什么会呈现这种状况: 因为给 allocation2 分配内存的时候 eden 区内存简直曾经被调配完了,咱们刚刚讲
了当 Eden 区没有足够空间进行调配时,虚拟机将发动一次 Minor GC,GC 期间虚拟机又发现 allocation1 无奈存入
Survior 空间,所以只好把新生代的对象 提前转移到老年代 中去,老年代上的空间足够寄存 allocation1,所以不会呈现
Full GC。执行 Minor GC 后,前面调配的对象如果可能存在 eden 区的话,还是会在 eden 区分配内存。能够执行如下代码
验证:
1 public class GCTest {2 public static void main(String[] args) throws InterruptedException {3 byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6;
4 allocation1 = new byte[60000*1024];
5
6 allocation2 = new byte[8000*1024];
7
8 allocation3 = new byte[1000*1024];
9 allocation4 = new byte[1000*1024];
10 allocation5 = new byte[1000*1024];
11 allocation6 = new byte[1000*1024];
12 }
13 }
14
15 运行后果:16 [GC (Allocation Failure) [PSYoungGen: 65253K‐>952K(76288K)] 65253K‐>60960K(251392K), 0.0311467 secs] [Times:
user=0.08 sys=0.02, real=0.03 secs]
17 Heap
18 PSYoungGen total 76288K, used 13878K [0x000000076b400000, 0x0000000774900000, 0x00000007c0000000)
19 eden space 65536K, 19% used [0x000000076b400000,0x000000076c09fb68,0x000000076f400000)
20 from space 10752K, 8% used [0x000000076f400000,0x000000076f4ee030,0x000000076fe80000)
21 to space 10752K, 0% used [0x0000000773e80000,0x0000000773e80000,0x0000000774900000)
22 ParOldGen total 175104K, used 60008K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
23 object space 175104K, 34% used [0x00000006c1c00000,0x00000006c569a010,0x00000006cc700000)
24 Metaspace used 3343K, capacity 4496K, committed 4864K, reserved 1056768K
25 class space used 361K, capacity 388K, committed 512K, reserved 1048576K
大对象间接进入老年代
大对象就是须要大量间断内存空间的对象(比方:字符串、数组)。JVM 参数 -XX:PretenureSizeThreshold 能够设置大
对象的大小,如果对象超过设置大小会间接进入老年代,不会进入年老代,这个参数只在 Serial 和 ParNew 两个收集器下
无效。
最初在赠送一张图: