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

内存分配与回收策略
Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。

1. 综述

对象的内存分配,从概念上讲,应该都是在堆上分配(而实际上也有可能经过即时编译后被拆散为标量类型并间接地在栈上分配)。在经典分代的设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。对象分配的规则并不是固定的,《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪一种垃圾收集器,以及虚拟机中与内存相关的参数的设定。

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

1. Eden区有足够空间的情形

虚拟机参数

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

参数说明

尝试分配三个2MB大小和一个4MB大小的对象,在运行时通过-Xms20M、-Xmx20M、-Xmn10M这三个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:Survivor-Ratio=8决定了新生代中Eden区与一个Survivor区的空间比例是8∶1。

package com.xiao.test.Test;

public class test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] byte1,byte2,byte3;
byte1 = new byte[2 * _1MB];
byte2 = new byte[2 * _1MB];
byte3 = new byte[2 * _1MB];
}
}

2. Eden区没有足够空间的情形

虚拟机参数相同

package com.xiao.test.Test;

public class test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] byte1,byte2,byte3,byte4;
byte1 = new byte[2 * _1MB];
byte2 = new byte[2 * _1MB];
byte3 = new byte[2 * _1MB];
byte4 = new byte[3 * _1MB];
}
}

 

显然进行了Minor GC

(2)、大对象直接进入老年代
1. 什么是大对象?

java培训中大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。

2. Java虚拟机中要避免大对象的原因

在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们。
而当复制对象时,大对象就意味着高额的内存复制开销。
3. 大对象直接进入老年代的好处

避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作(HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配)。

(3)、长期存活的对象将进入老年代
1. 虚拟机是怎样判断对象是否是长期存活?

内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。

2. 对象年龄增加及晋升至老年代过程

对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

3. 长期存活的对象将进入老年代的原因

我们都知道新生代的垃圾收集算法算法是标记-复制算法,如果长期存活的对象仍然存放在新生代的话,那么就会带来复制的开销增大的问题。所以我们将大于某一年龄阈值的对象放入老年代,这样可以减轻新生代垃圾回收时的压力。

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

(5)、空间分配担保
1. 空间分配担保的内容

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

2. “冒险”是冒了什么风险

前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

3. 那我们需要把担保打开吗?

取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下都还是会将-XX:HandlePromotionFailure开关打开,避免Full GC过于频繁。

posted @ 2021-11-18 12:41  Linux运维阿铭  阅读(56)  评论(0编辑  收藏  举报