运行时堆内存的划分取决于使用的垃圾回收器,截止到2020年6月份,已知的除 Epsilon、ZGC、Shenandoah
之外的垃圾回收期都使用的逻辑分代,其中G1是逻辑分代但物理堆内存不分代,其余的不仅逻辑分代而且物理堆内存也分代。简单来说:
Epsilon、ZGC、Shenandoah
,不分代G1
,逻辑分代,物理堆内存不分代(指不会划分物理内存)- 其余的逻辑+物理堆内存分代
内存分代算法
内存分代算法主要是为了提高速度而出现的,针对不同区域有不同的回收算法。内存分代算法适用于内存分代模型,内存分代模型分为新生代、老年代和永久代(JDK1.8以后,永久代移除),其中新生代、老年代存放在堆区,永久代存放在非堆区。
点击查看【processon】
新生代
通过实践得出绝大多数对象创建的快消亡的也快(忘记哪里有说过),所以不加区分很容易造成整个应用的GC。所以就产生了新生代,那么对应的这个新生代的特点就是——存活对象数量少。所以新生代的回收通常使用 Copying
算法。它的流程是这样的(一般情况):
- 普通对象都先放到
Eden区
(一开始不会涉及到Survivor区
) - 当下一个对象进来时,
Eden区
不够分配了,就会触发YGC
YGC
后,Eden区
存活的对象就会进入Survivor区
中的一个。那么此时存活对象的分布就是在Eden区
和Survivor区
了- 再如此反复,总之对象都不会直接进入
Survivor区
,而是先进Eden区
新生代包含了 Eden区
和 Survivor区
两个区域,而 Survivor区
还划分成了两块—— Survivor1
和 Survivor2
,就和前面那张总图一样。
默认情况下, Eden:Survivor = 8:1
(通过 -XX:+PrintFlagsFinal
虚拟机参数搜索 SurvivorRatio
关键字查找到的,初始情况是 Eden:Survivor=8:1
)
新生代大小控制
新生代的内存布局比例会按 -XX:SurvivorRatio=x
划分,这个虚拟机参数是指 Eden区:Survivor区=n ,即Eden区和Survivor区(这里只算了一个Survivor区,实际上要算两个,一个 survivor1
一个 survivor2
)之比为n。现在假设一个 Survivor区
占用 x
的大小,其计算方式如下:
z _x + 2 _ x = 新生代大小
如果z = 6,新生代大小为10240K,那么6 * x + 2 * x = 10240K,解出x = 1280K;那么Eden区的大小就是7680K
理论上的确是7680K,但是当该数值除以1024时,无法得到整数值。所以实际上Eden区的大小为8192K,即8MB。
如果z = 6, 新生代大小为102400K(100MB),那么6 * x + 2 * x = 102400K,解出x = 12800K,
那么Eden区就是76800K
嗯,真的是76800K。
总而言之,解出来的Eden区数值(单位一定是K)如果无法被整除,则向上取一个能被1024整除的数。
老年代
老年代里的对象可以理解为都是老油条了,那些在年轻代反复GC都GC不掉的对象就留在了老年代。当然这里也有一些是特里,通过“后门”直接晋升为老年代的小年轻。老年代的特点就是活着的对象特别多,所以通常使用 Mark Sweep
或 Mark Compact
算法。
永久代(元空间)
方法区
是JVM规范中提到的一种运行时结构,实际在实现过程中,不同的虚拟机有不同的实现。像主流的Hotspot,它是采用永久代(一块物理内存的代号,JDK7是这么叫的)实现的;而后面版本里,它是使用元空间(JDK8这么叫的)实现的。
- 方法区是规范层面的东西,规定了这一个区域要存放哪些东西
- 永久带或者是metaspace是对方法区的不同实现,是实现层面的东西。
回收
因为堆内存主要划分成了新生代和老年代,所以不同区域的回收也划分成了两部分:YGC和FGC。前者全名 Young GC
,又称 Minor GC
,当 Eden区
满了就触发 YGC
;后者全名 Full GC
,当 Old区
满了就触发 FGC
。
笔者曾经在 《深入Java虚拟机 第二版》 看到这么一句话,我觉得可能还不够严谨:
而是将内存分为一块较大的Eden区和两块较小的Survivor空间里,每次使用Eden和其中一块Survivor。
上面的句子摘自《深入Java虚拟机 第二版》,这句子可能会给一些读者带来误解:以为Survivor也会作为一块内存空间用来 直接分配 对象。这直接会让我们以为当 其中一块Survivor
和 Eden区
都满了后才会触发Minor GC。其实不然,在HotSpot虚拟机里,所有新生对象都是先分配到 Eden区
中(特殊情况分配到 Old Gen
)。当 Eden区
不够,触发了Minor GC后,会将 Eden区
里的幸存对象复制到 Survivor的From区
里。在第一次Minor GC后,对象才 真真正正的只在Eden区和其中一块Survivor里 。 即发生第一次Minor GC前,只有Eden区存放对象 。
常用JVM参数
-XX:SurvivorRatio=n
,设置年轻代里Eden
和Survivor
的比例,公式为Eden:Survivor=n
(这里的Survivor
只表示一个,实际计算时要×2
)JDK1.8
默认n=8
,即Eden:Survivor1:Survivor2 = 8:1:1
-XX:NewRatio=n
,设置老年代和年轻代的比例,公式为Old:New=n
JDK1.8
默认n=3
,即Old:New = 2:1
我们可以使用 jmap -heap <pid>
来查看内存分代情况,像我这样:
总结
不同的垃圾回收器使用的算法可能不同,新出的 Epsilon、ZGC、Shenandoah
都没有使用分代算法(具体用的是啥不清楚), G1
是逻辑分代(算法上分代,实际内存不划分),再前面的垃圾回收器不仅算法上分代,实际内存也划分(实际内存咋划分的?)。然后后面介绍了分代算法里面各个代的划分比例,最后简要分析了一下内存从出生到死亡的全过程。