JVM之内存分配和回收机制
作者:吃猫的大鱼
前言
本篇主要介绍JVM内存分配和回收策略,内容主要节选自《深入理解java虚拟机》。
一、内存分配策略
1. 堆内存模型
组成:
- 新生代 默认占堆空间的三分之一,由于在新生代对象大多都是朝生夕死,则采用的是复制算法,在复制的期间会有频繁的Minor GC。
- 老年代 默认占堆空间的三分之二,老年代对象大多都是长期存活的,则采用的是标记算法,老年代的Major GC开销非常大。
- Eden 新生代分为2个区 Eden和Survivor区 其中Eden区默认占新生代十分之八的空间,对象优先进入Eden区。
- Survivor 当Eden区内存满了之后会进行一次Minor GC存活对象会进入Survivor区,默认占新生代十分之二空间, 其中它又均分为From区和To区,在Survivor区的对象每熬过一次从From区到TO区则年龄+1。
大多情况下,对象在新生代Eden区中分配。当Eden没有足够的空间分配对象时虚拟机会发起一次Minor GC。
2.2 大对象直接到老年代
大对象即需要大量连续内存空间的对象(例如很长的字符串及数组)。虚拟机提供了一个-XX:PretenureSizeThreshoId参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个区之间发生大量的内存复制。注意PretenureSizeThreshoId参数只对Serial和ParNew两款收集器有效。
2.3 动态年龄判断
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进人老年代,无须等到MaxTenuringThreshoId中要求的年龄。
2.4 内存担保机制
在发生Minor GC之前,虚拟机会先检查Survivor空间是否够用,如果够用则直接进行Minor GC。否则进行检查老年代最大连续可用空间是否大于新生代的总和,假如大于,那么这个时候发生Minor GC是安全的。假如不大于,那么需要判断HandlePromotionFailure设置是否允许担保失败。假如允许,则继续判定老年代最大可用的连续空间是否大于平均晋升到老年代对象的平均值,如果大于,这个时候可以发生Minor GC ,如果小于或者设置HandlePromotionFailure不允许担保失败,则需要做一次Full GC。通常会把HandlePromotionFailure开关打开,以减少Full GC。
2.5 长期存活对象
虚拟机给每个对象定义了一个对象年龄(Age)计数器(存在于对象头中)。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survwor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshoId设置。
二、对象存活
判断对象存活一般有两种方式: 引用计数算法和可达性分析算法。
1.引用计数算法
原理:
- 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:
- 实现简单,垃圾对象便于辨识;
- 判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次复制都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
2.可达性分析算法
定义:相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
原理:
- 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC ROOT 对象:
- 虚拟机栈中引用的对象;
比如:各个线程被调用的方法中使用到的参数、局部变量等。 - 本地方法栈内 JNI(通常说的本地方法)引用的对象;
- 方法区中类静态属性引用的对象;
比如:Java类的引用类型静态变量 - 方法区中常量引用的对象;
比如:字符串常量池(string Table)里的引用 - 所有被同步锁 synchronized 持有的对象;
- Java虚拟机内部的引用。
基本数据类型对应的 Class 对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
3.再谈引用
- 强引用:强引用在代码中普遍存在,例如Object obj=new Object() 这类的引用。只要强引用存在,垃圾回收器永远不会回收掉被引用的对象。
- 软引用:软引用来描述一些还有用但并非必须的对象,在系统要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收,如果第二次回收还没有足够的内存,才会抛出内存溢出异常。
- 弱引用:弱引用也是用来描述必须对象的,但是它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论内存是否足够,都回收掉只被弱引用关联的对象。
- 虚引用:它是最弱的一种引用关系。它无法通过虚引用来取得一个对象实例。唯一目的就是能在这个对象被回收之前会收到一个系统通知。
三、内存回收
1.堆内存回收
是JVM所管理内存最大的一块,也是gc回收的主要区域。
1.1 哪些对象能回收?
堆内存中对象存活是使用可达性分析算法来判断,其中非存活对象由GC回收掉。这个就是虚拟机需要回收堆的对象。
1.2 如何回收?
- Minor GC:新生代收集,目标只是新生代的垃圾收集;
- Major GC:老年代收集,目标是老年代的垃圾收集(具体说只有CMS会有单独收集老年代的行为);
- Full GC:收集整个java堆和方法区的垃圾收集。这里补充说明一下虽然网上很多说什么Full GC就是Major GC,在这里我要重申一下并不是,具体看书上描述如下:
Mixed GC:收集整个新生代以及部分老年代的垃圾收集,仅G1支持。(类似于Full GC)
1.3 什么时候回收?
Minor GC触发条件:
- Eden区域满了,会触发Minor GC;
- 新生对象需要分配到新生代的Eden,当Eden区的内存不够时需要进行MinorGC。
Major GC触发条件:
- 老年代区域设置的阈值空间满了,会触发Major GC;
- 新对象需要分配到老年代,此时老年代设置阈值可用空间不足时触发Major GC。
Full GC触发条件:
- 内存担保机制 ,Survivor空间不足时,判断是否允许担保失败,如果不允许则进行Full GC。如果允许,并且每次晋升到老年代的对象平均大小>老年代最大可用连续内存空间,也会进行Full GC;
- MinorGC后存活的对象超过了老年代剩余空间;
- 方法区内存不足时;
- 程序中调用了System.gc()方法,可用通过-XX:+ DisableExplicitGC来禁止调用System.gc;
- CMS GC异常,CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,会触发Full GC。
2.方法区回收
方法区主要回收废弃的常量池和不再使用类型,但这个2类对象存活的判断还不一样。
2.1 常量池
同堆的对象存活类似-可达性分析法,具体请参考之前的可达性分析法。
2.2 类型数据
- 该类索引的实例都已经被回收;
- 加载该类的类加载器已经被回收;
- 该类对应的java.lang.class对象没有任何地方被引用。
以上都是我简单总结,以下是书上关于方法区回收描述的内容:
其实从书上就可以看出来,关于方法区OOM问题大都是在程序中是有大量使用反射、动态代理、CGLIB等框架,如果在实际开发中遇到关于可以从以上几个维度来定位问题。
总结
本篇所有理论知识都是摘抄于《深入理解java虚拟机》,有部分是自己简单总结,JVM内存分配和回收是我们在分析JVM调优和相关问题的基石,建议看完我本篇的去多看几遍《深入理解java虚拟机》。