JVM-剖析对象内存分配流程

在这里插入图片描述

Pre

JVM-09自动内存管理机制【内存分配和回收策略】


对象分配流程总览

在这里插入图片描述


流程分解

栈上分配对象 (逃逸分析)

众所周知, JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要GC。

如果对象数量较多的时候, GC 压力较大,也间接影响了应用的性能 。

为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问 . 如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,从而减轻GC的压力。

JVM-彻底搞懂 逃逸分析&标量替换


Eden区分配对象

绝大部分情况,新生的对象都是存放在Eden区域。 当Eden区域没有足够的空间来给新生对象进行内存分配的时候,JVM会进行一次Minor GC . (Minor GC 也会STW,只不过速度非常快)

我们常看到的 Minor GC / Young GC 可以理解为是等价的。

Major GC / Full GC 也可以理解为是等价的。

  • Young GC : 回收新生代区域,速度快

  • Full GC : 回收 老年代、新生代 和 方法区的垃圾 ,速度较慢。

新生代:

Eden : Survivor (S1 + S2) = 8:1:1

大部分对象是朝生夕死的,没有死亡的对象被回收存放到S1 区域,所以 8:1:1 合适,其目的就是为了让eden区尽量的大,同时survivor区够用即可。


-XX:+UseAdaptiveSizePolicy 默认开启

JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化 。

禁用: -XX:-UseAdaptiveSizePolicy 自动变化。 不推荐修改。


Eden区域分配对象Demo

不设置具体的Xms Xmx 时 ,JVM会自动根据你电脑的内存,设置一个值。

在这里插入图片描述

可以看到,JVM给新生代自动根据你的电脑配置了47M 左右的内存。

程序运行后,我们分配了new byte[36 * _1M] 36 M的对象 。

total 47616K, used 40960K

eden space 40960K, 100% used

40960K, 100% used ? JVM本身内部的对象也要占用内存空间,不仅仅是你应用分配的对象。

这个时候Eden区域已经被应用的对象占满了。


再分配个 5M

在这里插入图片描述

我们来解释一下为什么会这个样子哈:

  1. 分配allocation1 对象的时候 ,已经把Eden区域占满了, 100%
  2. 当再次给allocation2分配内存空间的时候,需要5M的空间,但eden区间 100%了,没有足够的空间了 ,所以jvm发生了一次 young gc .
  3. GC期间,发下 allocation1 ,无法放入 Survior空间 , 所以提前将这个大对象存入到 老年代
  4. 老年代上的空间足够存放allocation1,所以不会出现Full GC

那在分配对象呢?

在这里插入图片描述

可以知道

Minor GC后,新分配的对象如果eden区足够的话,还是会在eden区分配内存。


大对象直接进入老年代

什么是大对象?

大对象就是需要大量连续内存空间的对象 ,比如一个大的字符串,一个大的数组等等 都可以称之为大对象

如何定义 “大”?

JVM通过 -XX:PretenureSizeThreshold 设置大对象的大小 。

如果对象超过设置大小会直接进入老年代,

比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC ,【 单位是字节】

设置1M以上的对象为大对象

测试一下

-XX:PretenureSizeThreshold=1000000  -XX:+UseSerialGC -XX:+PrintGCDetails

在这里插入图片描述


作用的垃圾收集器

-XX:PretenureSizeThreshold 只在 Serial 和ParNew两个收集器下有效。


好处

这样做的好处是什么呢? ----> 避免为大对象分配内存时的复制操作而降低效率 .


经验之谈

如果你能确保你的应用中有大对象,可以直接通过该参数让它直接接入老年代,避免大量的GC


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

概述

我们知道采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。

对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。


jvm参数 -XX:MaxTenuringThreshold

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


经验之谈

可以设置较短的年龄,尽快让大对象到老年代中去。


对象动态年龄判断

对象动态年龄概述

当前放对象的Survivor区域里(S1 S2中的一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了

例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代

这样做的目的就是希望那些可能是长期存活的对象,尽早进入老年代。

对象动态年龄判断机制一般是在minor gc之后触发。


对象动态年龄引起的频繁Full GC及优化

举个例子哈 关于这个知识点的

在这里插入图片描述

选择方式二优化

在这里插入图片描述

我们来分析一下通过增加新生代的是如何避免FullGC的

每秒60M的对象----> Eden 区域 1.6G ,需要约25秒占满Eden —> 满了就得minor GC -----> 前24秒的对象(24*60M)约1.5G对象,已经是垃圾对象了,肯定可以被回收,只有之后一秒产生的60M对象无法被回收【应用还在跑着,还有引用】 ------> 将60M 尝试放入 S0区域 ----> 60 < 200的50% ------> 可以存放下去,那放入S0 ----->等下个周期 又有60M的无法回收的对象进来 ,放到S1区域, 上个刚才的60M对象已经执行结束了,已经是垃圾对象了,在S0区域可以回收了,这时候连同24秒产生的对象和这个第一次产生的60M对象一起被回收掉。

如此循环往复,老年代根本就不会有对象写入,基本避免了Full GC .


老年代空间分配担保机制

在这里插入图片描述

  1. 年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间。

  2. 如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) ,就会检查 -XX:-HandlePromotionFailure 【jdk1.8默设置】 参数是否设置了。

  3. 如果设置,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。

  4. 如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生OOM

当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生OOM



猜你喜欢

转载自blog.csdn.net/yangshangwei/article/details/106977301