在前面的一篇文章深入理解 Java 虚拟机 - 如何利用 VisualVM 进行性能分析中讲到了一些关于 JVM 调优的知识, 但是, 其实, 还是有一些问题没有非常清楚的可以回答的, 这里先给出几个问题, 然后, 我们再展开这篇文章需要讲解的知识.
我们生成的对象最开始在哪分配? Eden?Survivor? 还是老年代呢?
进入到老年代需要满足什么条件呢?
接下来, 我们就带着这两个问题展开全文.
1 对象优先在哪分配
其实, 通过前面几篇文章的讲解, 这个问题其实已经见怪不怪了, 在大多数的情况下, 对象都是在新生代 Eden 区分配的, 在前面的文章我们提到, 在 Eden 区中如果内存不够分配的话, 就会进行一次 Minor GC. 同时, 我们还知道年轻代中默认下 Eden:Survivor0:Survivor2 = 8:1:1, 同时, 还能通过参数 - XX:SurvivorRatio 来设置这个比例 (关于这些参数的分析都可以查看这篇文章: 深入理解 Java 虚拟机 - 常用 vm 参数分析).
下面我们通过一个例子来分析是不是这样的.
1.1 实例
给定 JVM 参数:-Xms40M -Xmx40M -Xmn10M -verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=4
前面三个参数设置 Java 堆的大小为 40M, 新生代为 10M, 紧跟着后面两个是用于输入 GC 信息. 更多参数可以查看这篇文章: 深入理解 Java 虚拟机 - 常用 vm 参数分析.
- /**
- * @ClassName Test_01
- * @Description 参数:-Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8
- * @Author 欧阳思海
- * @Date 2019/12/3 16:00
- * @Version 1.0
- **/
- public class Test_01 {
- private static final int M = 1024 * 1024;
- public static void test() {
- byte[] alloc1, alloc2, alloc3, alloc4;
- alloc1 = new byte[5 * M];
- alloc2 = new byte[5 * M];
- alloc3 = new byte[5 * M];
- alloc4 = new byte[10 * M];
- }
- public static void main(String[] args) {
- test();
- }
- }
输入结果:
分析
eden:from:to=8:1:1, 这个因为前面设置了参数 - XX:SurvivorRatio=8.
新生代分配了 20M 的内存, 所以前面三个 byte 数组可以分配, 但是, 分配第四个的时候, 空间不够, 所以, 需要进行一次 Minor GC,GC 之后, 新生代从 12534K 变为 598K.
前面在新生代分配的内存 Minor GC 之后, 进入到了 Survivor, 但是, Survivor 不够分配, 所以进入到了老年代, 老年代已用内存达到了 50%.
1.2 回答问题
所以, 经过上面的例子我们发现, 对象一般优先在新生代分配的, 如果新生代内存不够, 就进行 Minor GC 回收内存.
点个赞, 看一看, 好习惯! 本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录, 这是我花了 3 个月总结的一线大厂 Java 面试总结, 本人已拿腾讯等大厂 offer.
2 进入到老年代需要满足什么条件
先给出答案, 分为几点.
条件1: 大对象直接进入到老年代
条件2: 长期存活的对象可以进入到老年代
条件3: 如果在 Survivor 空间中相同年龄所有对象的大小的总和大于 Survivor 空间的一半, 年龄大于等于该年龄的对象直接进入到老年代
2.1 分析条件1
哪些属于大对象呢?
一般来说大对象指的是很长的字符串及数组, 或者静态对象.
那么需要满足多大才是大对象呢?
这个虚拟机提供了一个参数 - XX:PretenureSizeThreshold=n, 只需要大于这个参数所设置的值, 就可以直接进入到老年代.
step1: 解决了这两个问题, 首先, 我们不设置上面的参数的例子, 将对象的内存大于 Eden 的大小看看情况.
- /**
- * @ClassName Test_01
- * @Description 参数:-Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
- * @Author 欧阳思海
- * @Date 2019/12/3 16:00
- * @Version 1.0
- **/
- public class Test_01 {
- private static final int M = 1024 * 1024;
- public static void test() {
- byte[] alloc1, alloc2, alloc3, alloc4;
- // alloc1 = new byte[5 * M];
- // alloc2 = new byte[5 * M];
- // alloc3 = new byte[5 * M];
- alloc4 = new byte[22 * M];
- }
- public static void main(String[] args) {
- test();
- }
- }
我们发现分配失败, Java 堆溢出, 因为超过了最大值.
step2: 下面我们看一个例子: 设置 - XX:PretenureSizeThreshold=104,857,600, 这个单位是 B 字节 (Byte/bait), 所以这里是 100M.
- /**
- * @ClassName Test_01
- * @Description 参数:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=104,857,600
- * @Author 欧阳思海
- * @Date 2019/12/3 16:00
- * @Version 1.0
- **/
- public class Test_01 {
- private static final int M = 1024 * 1024;
- public static void test() {
- byte[] alloc1, alloc2, alloc3, alloc4;
- // alloc1 = new byte[5 * M];
- // alloc2 = new byte[5 * M];
- // alloc3 = new byte[5 * M];
- alloc4 = new byte[500 * M];
- }
- public static void main(String[] args) {
- test();
- }
- }
发现新生代没有分配, 直接在老年代分配.
注意: 参数
PretenureSizeThreshold
只对 Serial 和 ParNew 两款收集器有效.
2.2 分析条件2
进入老年代规则: 这里需要知道虚拟机对每个对象有个对象年龄计数器, 如果对象在 Eden 出生经过第一次 Minor GC 后任然存活, 并且能够被 Survivor 容纳, 将被移动到 Survivor 空间中, 并且年龄设置为 1. 接下来, 对象在 Survivor 中每次经过一次 Minor GC, 年龄就增加 1, 默认当年龄达到 15, 就会进入到老年代.
晋升到老年代的年龄阈值, 可以通过参数 - XX:MaxTenuringThreshold 设置.
在下面的实例中, 我们设置 - XX:MaxTenuringThreshold=1.
- /**
- * @ClassName Test_01
- * @Description 参数:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
- * @Author 欧阳思海
- * @Date 2019/12/3 16:00
- * @Version 1.0
- **/
- public class Test_01 {
- private static final int M = 1024 * 1024;
- public static void test() {
- byte[] alloc1, alloc2, alloc3, alloc4;
- alloc1 = new byte[300 * M];
- alloc2 = new byte[300 * M];
- alloc3 = new byte[300 * M];
- alloc4 = new byte[500 * M];
- }
- public static void main(String[] args) {
- test();
- }
- }
从结果可以看出, from 和 to 都没有占用内存, 而老年代则占用了很多内存.
2.3 分析条件3
条件3是: 如果在 Survivor 空间中相同年龄所有对象的大小的总和大于 Survivor 空间的一半, 年龄大于等于该年龄的对象直接进入到老年代, 而不需要等到参数 - XX:MaxTenuringThreshold 设置的年龄.
实例分析
- /**
- * @ClassName Test_01
- * @Description 参数:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
- * @Author 欧阳思海
- * @Date 2019/12/3 16:00
- * @Version 1.0
- **/
- public class Test_01 {
- private static final int M = 1024 * 1024;
- public static void test() {
- byte[] alloc1, alloc2, alloc3, alloc4;
- alloc1 = new byte[100 * M];
- alloc2 = new byte[100 * M];
- // 分配 alloc3 之前, 空间不够, 所以 minor GC, 接着分配 alloc3=900M 大于 Survivor 空间一半, 直接到老年代.
- alloc3 = new byte[900 * M];
- // alloc4 = new byte[500 * M];
- }
- public static void main(String[] args) {
- test();
- }
- }
输入结果:
分配 alloc3 之前, 空间不够, 所以 minor GC, 接着分配 alloc3=900M 大于 Survivor 空间一半, 直接到老年代. 从而发现, survivor 占用 0, 而老年代占用 900M.
3 总结
这篇文章主要讲解了 JVM 内存分配与回收策略的原理, 回答了下面的这两个问题.
我们生成的对象最开始在哪分配? Eden?Survivor? 还是老年代呢?
进入到老年代需要满足什么条件呢?
来源: https://segmentfault.com/a/1190000022503399