运行时堆内存的划分取决于使用的垃圾回收器,截止到2020年6月份,已知的除 Epsilon、ZGC、Shenandoah 之外的垃圾回收期都使用的逻辑分代,其中G1是逻辑分代但物理堆内存不分代,其余的不仅逻辑分代而且物理堆内存也分代。简单来说:

  • Epsilon、ZGC、Shenandoah ,不分代
  • G1 ,逻辑分代,物理堆内存不分代(指不会划分物理内存)
  • 其余的逻辑+物理堆内存分代

这篇文章将着重分析内存分代算法。

内存分代算法

内存分代算法主要是为了提高速度而出现的,针对不同区域有不同的回收算法。内存分代算法适用于内存分代模型,内存分代模型分为新生代、老年代和永久代(JDK1.8以后,永久代移除),其中新生代、老年代存放在堆区,永久代存放在非堆区。
点击查看【processon】

新生代

通过实践得出绝大多数对象创建的快消亡的也快(忘记哪里有说过),所以不加区分很容易造成整个应用的GC。所以就产生了新生代,那么对应的这个新生代的特点就是——存活对象数量少。所以新生代的回收通常使用 Copying 算法。它的流程是这样的(一般情况):

  1. 普通对象都先放到 Eden区 (一开始不会涉及到 Survivor区
  2. 当下一个对象进来时, Eden区 不够分配了,就会触发 YGC
  3. YGC 后, Eden区 存活的对象就会进入 Survivor区 中的一个。那么此时存活对象的分布就是在 Eden区Survivor区
  4. 再如此反复,总之对象都不会直接进入 Survivor区 ,而是先进 Eden区

新生代包含了 Eden区Survivor区 两个区域,而 Survivor区 还划分成了两块—— Survivor1Survivor2 ,就和前面那张总图一样。
默认情况下, 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 = 新生代大小

  1. 如果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 SweepMark 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也会作为一块内存空间用来 直接分配 对象。这直接会让我们以为当 其中一块SurvivorEden区 都满了后才会触发Minor GC。其实不然,在HotSpot虚拟机里,所有新生对象都是先分配到 Eden区 中(特殊情况分配到 Old Gen )。当 Eden区 不够,触发了Minor GC后,会将 Eden区 里的幸存对象复制到 Survivor的From区 里。在第一次Minor GC后,对象才 真真正正的只在Eden区和其中一块Survivor里即发生第一次Minor GC前,只有Eden区存放对象

常用JVM参数

  • -XX:SurvivorRatio=n ,设置年轻代里 EdenSurvivor 的比例,公式为 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> 来查看内存分代情况,像我这样:
image.png

总结

不同的垃圾回收器使用的算法可能不同,新出的 Epsilon、ZGC、Shenandoah 都没有使用分代算法(具体用的是啥不清楚), G1 是逻辑分代(算法上分代,实际内存不划分),再前面的垃圾回收器不仅算法上分代,实际内存也划分(实际内存咋划分的?)。然后后面介绍了分代算法里面各个代的划分比例,最后简要分析了一下内存从出生到死亡的全过程。