Java虚拟机内存分配与回收策略

Java虚拟机中的内存分配与回收策略就是 Java的自动内存管理,其最核心的部分就是内存中对象的分配与回收。所以在了解虚拟机内存分配与回收策略之前我们有必要了解一下Java堆内存的组成部分。

堆内存示意图

从上图可以得知,堆内存主要分为新生代、老年代、永久代几部分组成,其中新生代又分为一个Eden区和两个Survivor区,其比例为8:1。JDK1.8之后,用元空间(Metaspace)的区域取代了堆中的永久代区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。


  • 对象优先在Eden分配

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

/*虚拟机参数配置如下 : -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails  */
public class MemoryAllocate {

    public static void main(String[] args) {
       
    }
}

首先解释下 虚拟机参数配置 : -Xms20M -Xmx20M -Xmn10M 三个参数限制了Java堆大小为20MB,不可扩展(-Xms设置堆容量的最小值,-Xmx设置堆容量的最大值),其中10M分配给新生代(-Xmn设置对容量新生代的大小),剩下的10M分配给老年代;-XX:SurvivorRatio=8 决定了新生代中Eden区与一个Survivor区的空间比例为8:1;-XX:+PrintGCDetails 打印内存回收日志。接下来看一下运行结果:

Heap
 PSYoungGen      total 9216K, used 2148K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff819270,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 3143K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 342K, capacity 388K, committed 512K, reserved 1048576K

从结果中分析得知新生代(PSYoungGen)总可用空间为Eden区与一个Survivor(from 与to其中)区的总和:(8192+1024)这里说的是新生代的可用空间,而不是总空间,总空间大小为Eden与两个Survivor区的和;老年代(ParOldGen) 的空间大小为10240k;Metaspace(元空间,JDK1.8之后用于取代永久代的空间)。因此虚拟机参数配置已经生效,另外,虽然我们什么都没做,Eden区的空间也已经被使用26%。接下来看看第二段代码:

public class MemoryAllocate {

    public static void main(String[] args) {
        //连续向堆中申请5个1M的空间
        byte[] allocate1 = new byte[1 * 1024 * 1024];
        byte[] allocate2 = new byte[1 * 1024 * 1024];
        byte[] allocate3 = new byte[1 * 1024 * 1024];
        byte[] allocate4 = new byte[1 * 1024 * 1024];
        byte[] allocate5 = new byte[1 * 1024 * 1024];
    }
}

输出结果如下 : 
Heap
 PSYoungGen      total 9216K, used 7598K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 92% used [0x00000000ff600000,0x00000000ffd6bab8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

PSYongGen的使用空间从2148k增加到7598k,而两个Survivor区根本没有使用,所以新增加的5450K空间全部在Eden中,证实了对象优先在Eden区中分配的观点。接下来看第三段代码:

public class MemoryAllocate {

    public static void main(String[] args) {
        //连续向堆中申请5个1M的空间
        byte[] allocate1 = new byte[1 * 1024 * 1024];
        byte[] allocate2 = new byte[1 * 1024 * 1024];
        byte[] allocate3 = new byte[1 * 1024 * 1024];
        byte[] allocate4 = new byte[1 * 1024 * 1024];
        byte[] allocate5 = new byte[1 * 1024 * 1024];
        //再申请1一个1M的空间
        byte[] allocate6 = new byte[1 * 1024 * 1024];
    }
}

输出结果如下:
[GC (Allocation Failure) [PSYoungGen: 7270K->968K(9216K)] 7270K->6096K(19456K), 0.0035282 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 968K->0K(9216K)] [ParOldGen: 5128K->5873K(10240K)] 6096K->5873K(19456K), [Metaspace: 3218K->3218K(1056768K)], 0.0060934 secs] [Times: user=0.08 sys=0.02, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 1353K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 16% used [0x00000000ff600000,0x00000000ff7527c8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 5873K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 57% used [0x00000000fec00000,0x00000000ff1bc6f8,0x00000000ff600000)
 Metaspace       used 3232K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

其中触发了一次GC回收和一次Full GC:

  • [PSYoungGen: 7270K->968K(9216K)] 表示GC前年轻代占用内存7270K,GC后占用内存968K,内>存区域总容量9M;
  • 7270K->6096K(19456K) 表示GC前堆占用内存7270K,GC后占用内存6096K,堆总容量20M;
  • [Full GC (Ergonomics) [PSYoungGen: 968K->0K(9216K)] 表示进行了一次Full GC,后面会说到Full GC与GC的区别。
    接下来看结果:
    从结果中我们可以得知ParOldGen老年代中使用了5873K的内存,而eden区中的内存使用情况反而变小了。因为当给allocate6分配内存的时候,eden区中的的剩余空间已经不足分配allocate6所需的1M内存, 因此发生GC,而GC期间发现已有的5个1M大小的对象无法全部放入Survivor空间,所以只好通过担保分配机制将这五个对象提前转移到老年代中。

注:上面代码运行可能会产生不一样的结果,那就需要读者另行分析了。

  • 大对象直接进入老年代

所谓的大对象主要指,需要大量连续内存空间的Java对象,最典型的例子就是那种很长的字符串以及数组。虚拟机提供了-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的好处是避免了在Eden区和两个Survivor区之间发生大量的内存复制(新生代采用复制算法回收内存)。

/*-XX:PretenureSizeThreshold=3145728 虚拟机参数配置*/
public class MemoryAllocate {
    
    public static void main(String[] args) {
        //申请一个8M的空间
        byte[] allocate = new byte[8 * 1024 * 1024];
    }
}
输出结果 :
Heap
 PSYoungGen      total 9216K, used 2478K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 30% used [0x00000000ff600000,0x00000000ff86b970,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
 Metaspace       used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

很明显的我们可以看到8M的空间全部分配到了老年代之中。这里有一点需要注意的PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。本列中使用的Parallel Scavenge收集器,在所申请的空间大于eden区可使用的空间时,就会直接将大对象直接分配到老年代。

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

如果对象在Eden区出生并且经历过一次Minor GC后仍然存活,并且能够被Servivor容纳,将被移动到Servivor空间中,并且把对象年龄设置成为1。对象在Servivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋级到老年代中。虚拟机提供了-XX:MaxTenuringThreshold参数来设置这个阈值。

  • 动态对象年龄判定

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

  • 空间分配担保

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

上面提到了Minor GC依然会有风险,是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。


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

上一篇:Java虚拟机垃圾收集
下一篇:虚拟机类加载机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容