查看原文
其他

JVM内存分配与回收

2017-10-11 Java知音


作者:黄俊彬,

来源:http://huangjunbin.com


前言

        前面我们介绍了new一个对象需要经过类加载、内存分配、初始化、对象设置、执行init等操作。那么在我们的代码中,一个对象是怎么样就被回收的呢?我们所知道的当一个对象实例没用被引用持有,就会被GC回收,究竟JVM是怎样来判断一个对象是否被持有引用的呢?下面我们将一一来进行分析。


如何判断对象是否被持有引用?


引用计数算法


        定义:引用计数算法(Reference Counting):给对象添加一个引用计数器,每当一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能被再使用的。


        优点:实现简单,判定效率高;微软的COM技术、Python中都使用了Reference Couting算法进行内存管理。


        缺点:由于其很难解决对象之间相互循环引用的问题,主流Java虚拟机里面都没有选用Refrence Couting算法来管理内存。


可达性分析算法

        定义:可达性分析(Reachability Analysis)判断对象存活的基本思路:通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达)时,则证明此对象是不可用的。




Java语言中,可作为GC Roots对象包括:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;

  2. 方法区中类静态属性引用的对象;

  3. 方法区中产量引用的对象;

  4. 本地方法栈中JNI(即一般的Native方法)引用的对象。


没有持有引用就会被GC回收么?


        即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,而真正宣告一个对象死亡,至少要经历两次标记过程;

        如果对象在进行可达性分析后发现没有GC Roots想连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

        如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。

        这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发送了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

        finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只有重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移除出F-Queue;如果对象这时候还没有逃脱,那就会被GC回收。


内存分配与回收策略内存分配

        由于不同的对象的生命周期是不一样的,因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收的效率。因此JVM采用了分代垃圾回收。在JVM的内存空间中把堆空间分为年老代和年轻代。 49 30801 49 15262 0 0 2518 0 0:00:12 0:00:06 0:00:06 3046 49 30801 49 15262 0 0 2161 0 0:00:14 0:00:07 0:00:07 3129 49 30801 49 15262 0 0 1826 0 0:00:16 0:00:08 0:00:08 2953大量创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。

        年轻代中又被分为Eden区、和两个Survivor区。新的对象分配是首先放在Eden区、Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次收集任然存活的,就会被转移到年老区。



        对象的内存分配,主要分配在年轻代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用了哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数配置。


接下来将会介绍几条最普遍的内存分配规则:


对象优先在Eden分配

        大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。


大对象直接进入老年代

        所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来存放。


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

        既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该在年轻代,哪些应该在年老代。

        为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次的GC后任然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每熬过一次GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。


动态对象年龄判断

        为了能更换地适应不同程序的内存情况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升年老代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于改年龄的对象就可以直接进入年老代。


空间分配担保

        在发生MinorGC之前,虚拟机会先检查年老代最大可用的连续空间是否大于年轻代所有对象空间,如果这个条件成立,那么GC可以确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

        如果允许,那么会继续检查年老代最大可用的连续空间是否大于历次晋升到年老代的对象的平均大小。如果大于,将尝试进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFailure不允许冒险,那这时也要改为进行一次FullGC。


回收策略

        由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC和Full GC。


Minor GC

        一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发SMinorGC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。

        这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。


Full GC

        对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

    年老代(Tenured)被写满

    持久代(Perm)被写满

    System.gc()被显示调用


参考

本文主要参考《深入理解Java虚拟机:JVM高级特性与最佳实践》后的学习笔记及总结。


猜你喜欢:

Java面试题汇总

Java开发教程视频整理

100套IT类简历模版下载

精选《六款JavaWeb项目源码下载

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存