JVM基础知识整理(三)垃圾回收与内存分配

编程

       在java语言之前就已经有了垃圾收集GC的技术,理解GC的回收机制、算法无疑是我们进行系统调优排查的重要基础。GC由JVM中的垃圾收集器进行处理,垃圾收集器的设计无外乎关注3个问题(算法):如何识别出对象为”垃圾“?何时进行GC?如何执行GC?以下按这些问题展开阐述。

识别对象已死

        设计一个垃圾回收器第一要点自然就是识别出垃圾。怎样才算一个完善的垃圾识别算法?以下从两个算法讲起:

引用计数算法

       该算法就是JVM给每个实例对象都添加一个计数器,该计数器只记录对象的被引用次数。当对象被引用一次就+1,引用失效一次就-1,如果一个对象的引用计数为零,那么该对象即可以被回收的。该算法明显的优点就是判断效率高,而且具有实时性, 一旦没有引用,内存就直接释放了,而且不⽤像其他机制等到特定时机,这样处理回收内存的时间分摊到了平时。 但是对于JVM来说,除了新增计数器消耗资源外,比较致命的缺点就是无法回收相互循环引用的对象,如以下代码:

public class MyObject {

public Object ref =null;

public static void main(String[] args) {

MyObject myObject1 =newMyObject();

MyObject myObject2 =newMyObject();

myObject1.ref = myObject2;

myObject2.ref = myObject1;

myObject1 =null;

myObject2 =null;

}

}

        从上面的代码可以轻易地发现myObject1与myObject2互为引用,我们知道如果采用引用计数法,myObject1和myObject2将不能被回收,因为他们的引用计数无法为零。当main方法运行完4行后,两个对象的被引用计数均为2。此时将myObject1和myObject2分别置为null,则两个对象的引用计数只减1。若要满足垃圾回收的条件,需要清除myObject2中的ref这个引用,而要清除掉这个引用的前提条件是myObject2引用的对象被回收,可是该对象的引用计数也为1,因为myObject1.ref指向了它。以此类推,也就进入一种死循环的状态。

可达性分析算法

        这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

        该算法的重点自然就是将哪些对象列为GC Roots:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(即一般说的native方法)中引用的对象。

        据此,引用计数法中的循环引用问题则可解决。

垃圾收集算法

        垃圾收集器能识别出垃圾后,接下来便是考虑最优地实现何时、如何收集垃圾。以下分别从这几个算法讲起: 

       1)标记-清除算法。当垃圾收集器开始执行GC时,首先根据可达性分析算法,标记出所有从GC Roots开始的对象,标记完成后,对剩下未被标记的对象进行全部清除回收。该算法实现较为简单基础,但不足有两个:一是效率不高, 扫描了整个空间两次 ,速度慢,二是清除之后容易产生大量不连续的内存碎片(被清除的对象内存地址如正好不连续,清除后将留下内存地址空白间隔),从而导致大对象不够内存空间分配,则被迫触发一次垃圾回收。

       2)复制算法。与标记清除算法不同的是,垃圾收集器标记完对象后,会将这些存活的对象全部复制到另一半新的空白空间,并严格按照内存地址顺序排列,最后将前一半空间进行一次性空间清理。该算法就很好解决了内存空间地址不连续的浪费问题,但仍存在明显不足:一是浪费一半内存空间,二是万一对象存活率较高接近100%,复制操作将显得“大动干戈”,效率低下。

       3)标记-整理算法。该算法不需要像复制算法专门设置新的空间,而是在标记存活对象之后,将所有存活对象往一端移动,移动过程中不留空白空间并按照内存地址顺序排列,最后一次性清理掉存活对象边界以外的内存空间。该算法已解决了前两个算法的不足,唯一的缺点也是存在效率问题:移动过程中需要重新整理对象的空间排序,比起复制操作自然就慢些。

       4)分代收集算法。一般最优算法都是“集大家之所成”,所以当前商用JVM都采用分代收集算法,具体就是按对象存活周期划分不同内存区域,对不同的内存区域使用不同的垃圾回收算法。结合各算法的特点,不难得知,存活时间较短的对象区域,适合使用复制算法,因为存活对象少、复制操作成本低,而存活时间较长的对象区域,适合使用标记清理/整理算法。

内存分配

       按照分代收集算法,需要对内存进行区域划分,以下通过了解JVM的内存分配来认识垃圾回收的整个过程。对象的内存分配,几乎就是在堆上分配,JVM对Java堆进行了分区设计,如图:

       这种分区设计(也叫堆分代)的好处就是做好按需处理对象,新生对象跟老对象默认按1:2比例进行区分存放处理,新生对象又按默认8:1:1进行细分,便于内存空间管理、提高内存空间使用效率。根据该分区的设计,对象从存活到被回收的过程如下:

        一般对象创建后,首先会分配到Young区(新生代)的Eden区中,当这些对象经历过一次Minor GC(新生代GC)后依然存活, 将会被移到Survivor区(Form/To区)。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到Old区(老年代)中。当对象在Old区存活年龄比较大后,最后被JVM进行Major GC/Full GC(老年代GC)。一般来说,Minor GC会非常频繁,因为大多数对象都是朝生夕灭,Major GC的速度一般会比Minor GC慢10倍以上。

 

以上是 JVM基础知识整理(三)垃圾回收与内存分配 的全部内容, 来源链接: utcz.com/z/518744.html

回到顶部