深入理解Java虚拟机-GC(垃圾回收)

 2019-12-22 10:59  阅读(1134)
文章分类:JVM

一,哪些内存需要回收

程序 计数器,虚拟机栈,本地方法栈这几个区域都是随线程生而创建,随线程灭而灭。栈区的数据都是临时性的数据,随着数据第生命周期的结束,相应栈中的数据会自动清除。所以这几个区域的内存就不需过多考虑垃圾回收的问题。

java的方法区和堆区则不一样,一个接口的多个实现类需要的内存都不一样,一个方法中多个分支需要的内存也可能不一样,我们要在运行时才知道要创建哪些对象,这部分内存是动态创建和回收的,内存回收主要关注这一部分的内存。

(1)方法区回收:

在方法区回收垃圾效率比较低,一般主要回收废弃常量和无用的类。

废弃常量:没有对象引用的常量。

无用类:1该类所有对象已经被回收,2,加载该类的classLoader已经被回收,3,该类的java.lang.Class对象没有在任何地方引用,无法通过反射访问该类。

二,如何判断对象已死

所有的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面讲的不可达主要是指应用程序已经没有内存块的引用了, 在Java中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。

判断对象是否存活的算法:

(1)引用计数法

现代编程语言比如Lisp,Python,Ruby等的垃圾收集算法采用的就是引用计数算法。

引用计数算法很简单,它实际上是通过在对象头中分配一个计数器来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就死亡了,它就会被回收。

引用计数算法的

优点:实现简单,判定效率很高,在大多数情况下是一个不错的算法。

缺点:它很难解决对象之间相互循环引用的问题,如果有两个对象相互引用,那么这两个对象就不能被回收,因为它们的引用计数始终为1。

public class ReferenceCountingGC {

public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];

public void testGC() {

ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;

objA = null;
objB = null;
System.gc();
}
}

(2)可达性分析算法

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。

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

2019120001459\_1.png

那么那些点可以作为GC Roots呢?一般来说,如下情况的对象可以作为GC Roots:

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中的常量引用的对象
  4. 本地方法栈中JNI(Native方法)的引用的对象

三,垃圾回收算法:

(1)标记清除法

算法顾名思义,主要就是两个动作,一个是标记,另一个就是清除。

标记就是根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续用。

标记指示回收,那就直接收掉;标记指示对象还能用,那就原地不动留下。
2019120001459\_2.png

缺点

  1. 标记与清除效率低;
  2. 清除之后内存会产生大量碎片;

所以碎片这个问题还得处理,怎么处理,看标记-整理算法。

(2)复制算法

“复制”(Copying)收集算法,为了解决标记-清除算法的效率问题;

算法思路

(A)、把内存划分为大小相等的两块,每次只使用其中一块;

(B)、当一块内存用完了,就将还存活的对象复制到另一块上(而后使用这一块);

(C)、再把已使用过的那块内存空间一次清理掉,而后重复步骤2;

2019120001459\_3.png

—-优点
这使得每次都是只对整个半区进行内存回收;
内存分配时也不用考虑内存碎片等问题(可使用”指针碰撞”的方式分配内存);
实现简单,运行高效;

—缺点

(A)、空间浪费
可用内存缩减为原来的一半,太过浪费(解决:可以改良,不按1:1比例划分);
(B)、效率随对象存活率升高而变低
当对象存活率较高时,需要进行较多复制操作,效率将会变低(解决:后面的标记-整理算法);

(3)标记-整理算法

标记/整理算法与标记/清除算法非常相似,它也是分为两个阶段:标记和整理。下面LZ给各位介绍一下这两个阶段都做了什么。

标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。

整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。

它GC前后的图示与复制算法的图非常相似,只不过没有了活动区间和空闲区间的区别,而过程又与标记/清除算法非常相似,我们来看GC前内存中对象的状态与布局,如下图所示。

2019120001459\_4.png

这张图其实与标记/清楚算法一模一样,只是LZ为了方便表示内存规则的连续排列,加了一个矩形表示内存区域。倘若此时GC线程开始工作,那么紧接着开始的就是标记阶段了。此阶段与标记/清除算法的标记阶段是一样一样的,我们看标记阶段过后对象的状态,如下图。

2019120001459\_5.png

没什么可解释的,接下来,便应该是整理阶段了。我们来看当整理阶段处理完以后,内存的布局是如何的,如下图。

2019120001459\_6.png

可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

不难看出,标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价,可谓是一举两得,一箭双雕,一石两鸟

不过任何算法都会有其缺点,标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

(4)分代收集算法

分代收集算法是目前主流的JVM采用的算法。

JVM根据对象的存活周期的不同而将内存分为3部分:

新生代:刚创建的一些对象(朝生夕灭的对象)

老年代:存活得比较久,但还是要死的对象(例如:缓存对象、单例对象等)。

永久代:对象生成后几乎不灭的对象(例如:加载过的类信息)。

新生代:采用复制算法,新生代对象一般存活率较低,因此可以不使用50%的内存作为空闲,一般的,使用两块10%的内存

作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存

活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放,以此类推,下面还是用一张图来说明:

2019120001459\_7.png

解释下,堆大小=新生代+老年代,新生代与老年代的比例为1:2,新生代细分为一块较大的Eden空间和两块较小的Survivor空间,分别被命名为from和to。

老年代:老年代中使用“标记-清除”或者“标记-整理”算法进行垃圾回收,回收次数相对较少,每次回收时间比较长。

方法区对象回收

永久代指的是虚拟机内存中的方法区,永久代垃圾回收比较少,效率也比较低,但也必须进行垃圾回收,否则永久代内存

不够用时仍然会抛出OutOfMemoryError异常。永久代也使用“标记-清除”或者“标记-整理”算法进行垃圾回收。

结束语:

GC算法基本就是这些了,下一篇我们将一起讨论下有哪些GC算法的实现。

参考:https://blog.csdn.net/cool_summer_moon/article/details/80360600

点赞(1)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解Java虚拟机-GC(垃圾回收)

相关推荐