深入理解Java虚拟机(2.4)- 内存分配与回收策略

2020年8月17日17:50:58 评论 48

内存分配与回收策略

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

深入理解Java虚拟机(2.4)- 内存分配与回收策略

对象优先在Eden分配

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。

private static final int_1MB=1024 * 1024/**
         *VM参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails
         -XX:SurvivorRatio=8
         */
        public static void testAllocation () {
            byte[] allocation1,allocation2,allocation3,allocation4;
            allocation1 = new byte[2 * _1MB];
            allocation2 = new byte[2 * _1MB];
            allocation3 = new byte[2 * _1MB];
            allocation4 = new byte[4 * _1MB];//出现一次Minor GC
        }

运行结果:

[GC[DefNew:6651K->148K(9216K),0.0070106 secs]6651K->6292K(19456K),
0.0070426 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4326K[0x029d0000,0x033d0000,0x033d0000)
eden space 8192K,51%used[0x029d0000,0x02de4828,0x031d0000)
from space 1024K,14%used[0x032d0000,0x032f5370,0x033d0000)
to space 1024K,0%used[0x031d0000,0x031d0000,0x032d0000)
tenured generation total 10240K,used 6144K[0x033d0000,0x03dd0000,0x03dd0000)
the space 10240K,60%used[0x033d0000,0x039d0030,0x039d0200,0x03dd0000)
compacting perm gen total 12288K,used 2114K[0x03dd0000,0x049d0000,0x07dd0000)
the space 12288K,17%used[0x03dd0000,0x03fe0998,0x03fe0a00,0x049d0000)
No shared spaces configured.

上方代码的 testAllocation() 方法中,尝试分配 3 个 2MB 大小和 1 个 4MB 大小的对象,在运行时通过-Xms20M-Xmx20M-Xmn10M这 3 个参数限制了 Java 堆大小为 20MB ,不可扩展,其中 10MB 分配给新生代,剩下的 10MB 分配给老年代。-XX:SurvivorRatio=8决定了新生代中 Eden 区与一个 Survivor 区的空间比例是 8:1,从输出的结果也可以清晰地看到 eden space 8192K、from space 1024K、to space 1024K 的信息,新生代总可用空间为 9216KB(Eden区+1个Survivor区的总容量)。

执行 testAllocation() 中分配 allocation4 对象的语句时会发生一次 Minor GC,这次 GC 的结果是新生代 6651KB 变为 148KB ,而总内存占用量则几乎没有减少(因为 allocation1、allocation2、allocation3 三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。

这次 GC 发生的原因是给 allocation4 分配内存的时候,发现 Eden 已经被占用了 6MB,剩余空间已不足以分配 allocation4 所需的 4MB 内存,因此发生 Minor GC。GC 期间虚拟机又发现已有的 3 个 2MB 大小的对象全部无法放入 Survivor 空间(Survivor 空间只有 1MB 大小),所以只好通过分配担保机制提前转移到老年代去。

这次 GC 结束后,4MB 的 allocation4 对象顺利分配在 Eden 中,因此程序执行完的结果是 Eden 占用 4MB(被allocation4占用),Survivor 空闲,老年代被占用 6MB(被allocation1、allocation2、allocation3占用)。通过 GC 日志可以证实这一点。

Minor GC 和 Full GC 有什么不一样吗?

  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢 10 倍以上。

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组( byte[] 数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(特别是短命大对象,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。

private static final int_1MB=1024 * 1024/**
         *VM参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8
         *-XX:PretenureSizeThreshold=3145728
         */
        public static void testPretenureSizeThreshold () {
            byte[] allocation;
            allocation = new byte[4 * _1MB];//直接分配在老年代中
        }

运行结果:

Heap
def new generation total 9216K,used 671K[0x029d0000,0x033d0000,0x033d0000)
eden space 8192K,8%used[0x029d0000,0x02a77e98,0x031d0000)
from space 1024K,0%used[0x031d0000,0x031d0000,0x032d0000)
to space 1024K,0%used[0x032d0000,0x032d0000,0x033d0000)
tenured generation total 10240K,used 4096K[0x033d0000,0x03dd0000,0x03dd0000)
the space 10240K,40%used[0x033d0000,0x037d0010,0x037d0200,0x03dd0000)
compacting perm gen total 12288K,used 2107K[0x03dd0000,0x049d0000,0x07dd0000)
the space 12288K,17%used[0x03dd0000,0x03fdefd0,0x03fdf000,0x049d0000)
No shared spaces configured.

执行以上代码中的 testPretenureSizeThreshold() 方法后,我们看到 Eden 空间几乎没有被使用,而老年代的 10MB 空间被使用了 40%,也就是 4MB 的 allocation 对象直接就分配在老年代中,这是因为 PretenureSizeThreshold 参数被设置为 3MB(就是 3145728,这个参数不能像 -Xmx 之类的参数一样直接写 3MB),因此超过 3MB 的对象都会直接在老年代进行分配。

注意 PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效,Parallel Scavenge 收集器不认识这个参数,Parallel Scavenge 收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑 ParNew 加 CMS 的收集器组合。

长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1 。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

动态对象年龄判定

为了能更好地适应不同程序的内存状况,无须等到 MaxTenuringThreshold 中要求的年龄,同年对象达到 Survivor 空间的一半后,他们以及年龄大于他们的对象都将直接进入老年代。

空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代
所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC ,否则将进行 Full GC 。


垃圾收集相关链接:

深入理解Java虚拟机(2.1)- 垃圾收集机制

深入理解Java虚拟机(2.2)- 垃圾收集算法

深入理解Java虚拟机(2.3)- 垃圾收集器

深入理解Java虚拟机(2.4)- 内存分配与回收策略

回到目录深入理解Java虚拟机(全解析)

(转载自:https://www.cnblogs.com/czwbig/p/11127159.html)

素课网
  • 本文由 发表于 2020年8月17日17:50:58
  • 转载请注明:https://www.suketech.com/9315.html
深入理解Java虚拟机(3.2)- 类加载器 Java虚拟机

深入理解Java虚拟机(3.2)- 类加载器

类加载器 把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。 将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 ja...
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: