分配与回收,是 Java 虚拟机自动管理内存的两个部分。
之前提的垃圾内存回收的三种算法与常见的垃圾收集器,这些都是内存的回收,那 JVM 是如何分配内存呢?
Java 虚拟机规范并没有规定对象的创建和存储的细节,每款收集器都有各自的实现。
我本地环境是:
JDK 1.8、64 位、win7、HotSpot 默认 JVM 配置
Parallel Scavenge + Parallel Old 组合,会加参数改收集器
1、新对象,一般会优先被分配到新生代 Eden 区,当 Eden 区没有足够空间虚拟机将发起 Minor GC
代码:
package constxiong.jvm.gc;
/**
* 测试分配对象到 Eden 区
*/
public class AllocateToEden {
public static void main(String[] args) {
}
}
参数:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
GC 日志:
说明:
-Xms20M 初试堆内存大小 20M
-Xmx20M 最大堆内存大小 20M
-Xmn10M 新生内存大小 10M
-XX:SurvivorRatio=8 新生代中 Eden:Survivor:Survivor=8:1:1,Eden 占 80%
-XX:+PrintGCDetails 打印 GC 日志详情
这样配,堆的大小为 20M
新生代:Eden 就 8M = 8192K,两个 Survivor(from、to) 分别为 1M = 1024K
老年代:10M = 10240K
上面代码启动并未分配对象,Eden 消耗了 2314K,元空间 Metaspace 消耗了 3458K
修改代码在 main 方法分配一个 2M 的字节数组
代码:
package constxiong.jvm.gc;
/**
* 测试分配对象到 Eden 区
*/
public class AllocateToEden {
public static void main(String[] args) {
byte[] array1 = new byte[2 * 1024 * 1024];//2M 数组
}
}
GC 日志:
说明:
2M 数组对象,别分配到了新生代的 Eden 区,未分配对象前 Eden 消耗 2314K,加上 2048K,等于 4362K
2、占用连续空间的大对象(超长字符串、数组),可以直接分配到老年代,避免在新生代的两个 Survivor 区来回复制大对象
修改代码,再分配一个大对象 8M 数组:
package constxiong.jvm.gc;
/**
* 测试分配大对象到老年代
*/
public class AllocateToOldGeneration {
public static void main(String[] args) {
byte[] array1 = new byte[2 * 1024 * 1024]; //2M 数组
byte[] array2 = new byte[8 * 1024 * 1024]; //8M 数组
}
}
参数未变:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
GC 日志:
说明:
新生代已经不够分配 8M 的数组对象,所以直接分配到老年代
-XX:PretenureSizeThreshold 可以指定当对象大小超过这个阀值时,会被分配到老年代;但 Parallel Scavenge 并不支持,所以添加参数 -XX:PretenureSizeThreshold=3145728,指定垃圾收集器为 Serial ,同时修改代码,把 8M 的数组改为 3M
代码:
package constxiong.jvm.gc;
/**
* 测试分配对象到老年代
*/
public class AllocateToOldGeneration {
public static void main(String[] args) {
byte[] array1 = new byte[2 * 1024 * 1024]; //2M 数组
byte[] array2 = new byte[3 * 1024 * 1024]; //3M 数组
}
}
参数:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
GC 日志:
3、存活超过一定年龄的对象会被移到老年代
对象熬过一次 Minor GC 年龄就增加,默认配置年龄超过 15 之后,对象会被移到老年代,可以通过 -XX:MaxTenuringThreshold 进行配置该阀值
代码:
package constxiong.jvm.gc;
/**
* 测试移动超过一定年龄的对象到老年代
*/
public class AllocateToOldGeneration {
public static void main(String[] args) {
byte[] array1 = new byte[1024 * 1024 / 10]; //0.1M
byte[] array2 = new byte[4 * 1024 * 1024]; //4M
array2 = null;
array2 = new byte[4 * 1024 * 1024]; //4M
}
}
参数:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=3 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
GC 日志:
说明:
-XX:SurvivorRatio=3,eden=6M、from survivor=2M、to survivor=2M
byte[] array2 = new byte[4 * 1024 * 1024]; 这行代码执行的时候触发第一次 GC,此时 array1 和部分其他启动时创建的对象继续保留在新生代且年龄加 1
最后一行代码,array2 = new byte[4 * 1024 * 1024]; 代码执行,触发第二次 GC,由于参数中配置了 -XX:MaxTenuringThreshold=1,年龄大于等于 1 的对象都移动到老年代,此次 GC 后新生代为 0
修改参数,把 -XX:MaxTenuringThreshold=1 改为 15
代码不变,参数:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=3 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15
GC 日志:
说明:
修改 -XX:MaxTenuringThreshold=15,代码未变,array1 对象仍然保留在新生代,此时新生代不为 0
这条规则需要同时结合下一条:相同年龄所有对象占用内存之和,大于 Survivor 区空间一半时,大于等于该年龄的对象会被移到老年代。如果本例中的参数 -XX:SurvivorRatio=3 仍然等于 8 的话,第二次 GC 之后新生代仍然为 0,因为相同年龄的对象和大于 survivor 区 1M 的一半了
4、相同年龄所有对象占用内存之和,大于 Survivor 区空间一半时,大于等于该年龄的对象会被移到老年代
代码:
package constxiong.jvm.gc;
/**
* 测试相同年龄所有对象占用内存之和,大于 Survivor 区空间一半时,大于等于该年龄的对象会被移到老年代
*/
public class AllocateToOldGeneration {
public static void main(String[] args) {
byte[] array1 = new byte[1024 * 1024 / 4]; //0.25M
byte[] array2 = new byte[4 * 1024 * 1024]; //4M
byte[] array3 = new byte[4 * 1024 * 1024]; //4M,触发第一次 GC
array3 = null;
array3 = new byte[4 * 1024 * 1024]; //4M,触发第二次 GC,把所有年轻代的对象移动到老年代
}
}
参数:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15
GC 日志:
说明:
byte[] array3 = new byte[4 * 1024 * 1024]; 这行代码执行的时候触发第一次 GC,此时 array2 被移到老年代,array1 和部分其他启动时创建的对象继续保留在新生代且年龄加 1
最后一行代码,array3 = new byte[4 * 1024 * 1024]; 代码执行,触发第二次 GC,参数中配置了 XX:MaxTenuringThreshold=15,但相同年龄的对象占用内存大于 Survivor 区的一半,所有都移动到老年代,此次 GC 后新生代为 0
ConstXiong 备案号:苏ICP备16009629号-3