Java核心技术之内存管理机制---内存分配与回收策略

内存管理其实就只是两个问题:给对象分配内存以及回收分配给对象的内存

 前面的几篇文章已经描述了各种内存回收的问题,所以这一篇着重在对象的内存分配。对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置

对象优先在Eden分配

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

新生代Minor GC

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[0x029d00000x033d00000x033d0000)
eden space 8192K,51%used[0x029d00000x02de48280x031d0000)
from space 1024K,14%used[0x032d00000x032f53700x033d0000)
to space 1024K,0%used[0x031d00000x031d00000x032d0000)
tenured generation total 10240K,used 6144K[0x033d00000x03dd00000x03dd0000)
the space 10240K,60%used[0x033d00000x039d00300x039d02000x03dd0000)
compacting perm gen total 12288K,used 2114K[0x03dd00000x049d00000x07dd0000)
the space 12288K,17%used[0x03dd00000x03fe09980x03fe0a000x049d0000)
No shared spaces configured.

 示例中的testAllocation()方法中,尝试分配3个2MB大小和1个4MB大小的对象,在运行时通过-Xms20M、-Xmx20M、-Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1,从输出的结果也可以清晰地看到“edenspace 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(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对象,例如很长的字符串和数组。
 虚拟机提供了一个 -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[0x029d00000x033d00000x033d0000)
eden space 8192K,8%used[0x029d00000x02a77e980x031d0000)
from space 1024K,0%used[0x031d00000x031d00000x032d0000)
to space 1024K,0%used[0x032d00000x032d00000x033d0000)
tenured generation total 10240K,used 4096K[0x033d00000x03dd00000x03dd0000)
the space 10240K,40%used[0x033d00000x037d00100x037d02000x03dd0000)
compacting perm gen total 12288K,used 2107K[0x03dd00000x049d00000x07dd0000)
the space 12288K,17%used[0x03dd00000x03fdefd00x03fdf0000x049d0000)
No shared spaces configured.

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

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

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

private static final int_1MB=1024*1024/**
*VM参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8-XX:MaxTenuringThreshold=1
*-XX:+PrintTenuringDistribution
*/
@SuppressWarnings"unused"public static void testTenuringThreshold(){
byte[]allocation1,allocation2,allocation3;
allocation1=new byte[_1MB/4]//什么时候进入老年代取决于XX:MaxTenuringThreshold设置
allocation2=new byte[4*_1MB];
allocation3=new byte[4*_1MB];
allocation3=null;
allocation3=new byte[4*_1MB]

以MaxTenuringThreshold=1参数来运行的结果:

[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 1(max 1-age 1414664 bytes,414664 total
:4859K-404K(9216K),0.0065012 secs]4859K-4500K(19456K),0.0065283 secs][Times:user=0.02 sys=0.00,real=0.02 secs]
[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 1(max 1)
:4500K-0K(9216K),0.0009253 secs]8596K-4500K(19456K),0.0009458 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4178K[0x029d00000x033d00000x033d0000)
eden space 8192K,51%used[0x029d00000x02de48280x031d0000)
from space 1024K,0%used[0x031d00000x031d00000x032d0000)
to space 1024K,0%used[0x032d00000x032d00000x033d0000)
tenured generation total 10240K,used 4500K[0x033d00000x03dd00000x03dd0000)
the space 10240K,43%used[0x033d00000x038353480x038354000x03dd0000)
compacting perm gen total 12288K,used 2114K[0x03dd00000x049d00000x07dd0000)
the space 12288K,17%used[0x03dd00000x03fe09980x03fe0a000x049d0000)
No shared spaces configured.
以MaxTenuringThreshold=15参数来运行的结果:
[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 15(max 15-age 1414664 bytes,414664 total
:4859K-404K(9216K),0.0049637 secs]4859K-4500K(19456K),0.0049932 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 15(max 15-age 2414520 bytes,414520 total
:4500K-404K(9216K),0.0008091 secs]8596K-4500K(19456K),0.0008305 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4582K[0x029d00000x033d00000x033d0000)
eden space 8192K,51%used[0x029d00000x02de48280x031d0000)
from space 1024K,39%used[0x031d00000x032353380x032d0000)
to space 1024K,0%used[0x032d00000x032d00000x033d0000)
tenured generation total 10240K,used 4096K[0x033d00000x03dd00000x03dd0000)
the space 10240K,40%used[0x033d00000x037d00100x037d02000x03dd0000)
compacting perm gen total 12288K,used 2114K[0x03dd00000x049d00000x07dd0000)
the space 12288K,17%used[0x03dd00000x03fe09980x03fe0a000x049d0000)
No shared spaces configured.

动态对象年龄判定

 为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

动态对象年龄判定

private static final int_1MB=1024*1024/**
*VM参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8-XX:MaxTenuringThreshold=15
*-XX:+PrintTenuringDistribution
*/
@SuppressWarnings"unused"public static void testTenuringThreshold2(){
byte[]allocation1,allocation2,allocation3,allocation4;
allocation1=new byte[_1MB/4]//allocation1+allocation2大于survivo空间一半
allocation2=new byte[_1MB/4];
allocation3=new byte[4*_1MB];
allocation4=new byte[4*_1MB];
allocation4=null;
allocation4=new byte[4*_1MB]}

运行结果:

[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 1(max 15-age 1676824 bytes,676824 total
:5115K-660K(9216K),0.0050136 secs]5115K-4756K(19456K),0.0050443 secs][Times:user=0.00 sys=0.01,real=0.01 secs]
[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 15(max 15)
:4756K-0K(9216K),0.0010571 secs]8852K-4756K(19456K),0.0011009 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4178K[0x029d00000x033d00000x033d0000)
eden space 8192K,51%used[0x029d00000x02de48280x031d0000)
from space 1024K,0%used[0x031d00000x031d00000x032d0000)
to space 1024K,0%used[0x032d00000x032d00000x033d0000)
tenured generation total 10240K,used 4756K[0x033d00000x03dd00000x03dd0000)
the space 10240K,46%used[0x033d00000x038753e80x038754000x03dd0000)
compacting perm gen total 12288K,used 2114K[0x03dd00000x049d00000x07dd0000)
the space 12288K,17%used[0x03dd00000x03fe09a00x03fe0a000x049d0000)
No shared spaces configured.

 运行结果显示,allocation1、allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来已经到达了512KB,并且它们是同年的,满足同年对象达到Survivor空间的一半规则。我们只要注释掉其中一个对象new操作,就会发现另外一个就不会晋升到老年代中去了。

空间分配担保

 在发生Minor GC之前,虚拟机会 先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会 查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续 检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

空间分配担保

private static final int_1MB=1024*1024/**
*VM参数:-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8-XX:-HandlePromotionFailure
*/
@SuppressWarnings"unused"public static void testHandlePromotion(){
byte[]allocation1,allocation2,allocation3,allocation4,allocation5,allocation6,allocation7;
allocation1=new byte[2*_1MB];
allocation2=new byte[2*_1MB];
allocation3=new byte[2*_1MB];
allocation1=null;
allocation4=new byte[2*_1MB];
allocation5=new byte[2*_1MB];
allocation6=new byte[2*_1MB];
allocation4=null;
allocation5=null;
allocation6=null;
allocation7=new byte[2*_1MB]}

以HandlePromotionFailure=false参数来运行的结果:

[GC[DefNew:6651K-148K(9216K),0.0078936 secs]6651K-4244K(19456K),0.0079192 secs][Times:user=0.00 sys=0.02,real=0.02 secs]
[G C[D e f N e w:6 3 7 8 K-6 3 7 8 K(9 2 1 6 K),0.0 0 0 0 2 0 6 s e c s][T e n u r e d:4096K-4244K(10240K),0.0042901 secs]10474K-4244K(19456K),[Perm:2104K-2104K(12288K)]0.0043613 secs][Times:user=0.00 sys=0.00,real=0.00 secs]

以HandlePromotionFailure=true参数来运行的结果:

[GC[DefNew:6651K-148K(9216K),0.0054913 secs]6651K-4244K(19456K),0.0055327 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
[GC[DefNew:6378K-148K(9216K),0.0006584 secs]10474K-4244K(19456K),0.0006857 secs][Times:user=0.00 sys=0.00,real=0.00 secs]

HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。

总结

 内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为。因此,学习虚拟机内存知识,如果要到实践调优阶段,那么必须了解每个具体收集器的行为、优势和劣势、调节参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作为一名 Java 程序员,了解硬件层面的内存知识是非常重要的。在 NUMA 架构下,内存的访问速度会受到影响,因此了解 NUMA 架构的内存分布和访问方式,可以帮助程序员优化程序的性能。 NUMA 架构是一种多处理器架构,其中每个处理器都有自己的本地内存和一组本地 I/O 设备。这些处理器通过一个快速互联网络连接在一起,共享全局内存和 I/O 设备。 在 NUMA 架构下,内存被分割成多个本地内存区域和全局内存区域。本地内存区域是指与处理器直接相连的内存区域,访问速度最快。全局内存区域是指被所有处理器共享的内存区域,访问速度相对较慢。 程序员需要了解的是,当一个线程在处理器上执行时,它会优先访问本地内存区域,如果需要访问全局内存区域,则会通过互联网络访问,这会导致访问速度变慢。因此,在程序设计时,需要尽可能地减少线程之间的共享内存,避免出现频繁的跨处理器访问全局内存的情况。 另外,程序员还需要了解内存对齐的概念。在 NUMA 架构下,由于不同处理器的内存访问速度不同,如果内存没有对齐,则会导致不同处理器之间的数据传输速度不一致,进而影响程序的性能。因此,在程序设计时,需要注意内存对齐的问题,尽可能地将数据放在相邻的内存位置,避免出现不必要的跨处理器数据传输。 总之,了解 NUMA 架构的内存分布和访问方式是非常重要的,可以帮助程序员优化程序的性能。程序员需要注意线程之间的共享内存内存对齐的问题,尽可能地减少跨处理器访问全局内存的情况,提高程序的执行效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值