文章目录
- JVM运行时内存(堆内存)
- 新生代
- 老年代
- Full GC触发机制
- 内存分配策略
- TLAB
- 相关JVM参数
- 空间担保
- 栈上分配与逃逸分析
- 逃逸分析
JVM运行时内存(堆内存)
新生代
新生代:是用来存放新生的对象。 分为Eden 区、 SurvivorFrom、 SurvivorTo 三个区。Minor GC(复制算法) 进行垃圾回收。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
- Eden 区 :Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。Servivor区不会触发minor GC。
- ServivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者
- ServivorTo 区:保留了一次 MinorGC 过程中的幸存者。
- Minor GC过程 会引发STW
- 首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年(默认15)的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区) ,然后,清空 Eden 和 ServicorFrom 中的对象; 最后, ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区
老年代
老年代:主要存放应用程序中生命周期长的内存对象 **Full GC ( 或称为 Major GC )**进行垃圾回收。可能会抛出OOM(Out Of Memory)异常
- Major GC 不会频繁执行,两种情况会对老年代区进行垃圾回收:
- 一是,当Minor GC对新生代区进行垃圾回收后,一部分对象复制到老年代区,导致老年代内存不足
- 二是,无法找到足够大的连续空间分配给新创建的较大对象时。
- Major GC 流程:先扫描老年代区,标记存活对象,然后清除没有被标记的对象。耗时较长。内存碎片化严重
- 方法区:永久代(java8中被移除,替换为元数据区):指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息 。GC 不会在主程序运行期对永久区域进行清理 ,可能会抛出OOM(Out Of Memory)异常
- 元空间(元数据区)与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存 。默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中
Full GC与Major GC的区别:Full GC是整堆垃圾回收,回收的区域包括年轻代、老年代以及方法区。而Major GC表示对老年代的垃圾回收。但是大多数情况下Major GC与Full GC是同时进行的。
Full GC触发机制
触发Full GC执行的情况有如下五种:
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
内存分配策略
如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代
对象晋升老年代的年龄阀值,可以通过选项-XX:MaxTenuringThreshold来设置
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden
- 大对象直接分配到老年代(尽量避免程序中出现过多的大对象)
- 长期存活的对象分配到老年代
- 动态对象年龄判断:如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 空间分配担保: -XX:HandlePromotionFailure
TLAB
新生代的Eden区存在这样一个区域 TLAB(Thread Local Allocation Buffer) 线程私有,是对Eden区域的再划分。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
相关JVM参数
-XX:+PrintFlagsInitial //查看所有的参数的默认初始值
-XX:+PrintFlagsFinal //查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms //初始堆空间内存(默认为物理内存的1/64)
-Xmx //最大堆空间内存(默认为物理内存的1/4)
-Xmn //设置新生代的大小。(初始值及最大值)
-XX:NewRatio //配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio //设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold //设置新生代垃圾的最大年龄
-XX:+PrintGCDetails //输出详细的GC处理日志
//打印gc简要信息:①-Xx:+PrintGC ② - verbose:gc
-XX:HandlePromotionFalilure://是否设置空间分配担保
空间担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
- 如果大于,则此次Minor GC是安全的
- 如果小于,则虚拟机会查看
-XX:HandlePromotionFailure
设置值是否允担保失败。
- 如果
HandlePromotionFailure=true
,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
- 如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
- 如果小于,则改为进行一次Full GC。
- 如果
HandlePromotionFailure=false
,则改为进行一次Full Gc。
在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行FullGC。
栈上分配与逃逸分析
如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配.。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
逃逸分析
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。