【Garbage Collection】GC垃圾回收机制(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011669081/article/details/51866838

注:转载请注明出处:http://blog.csdn.net/u011669081/article/details/51866838


关于GC的介绍
GC的全称是Garbage Collection (垃圾收集)
在GC中,垃圾所指的是程序在运行过程中,会产生出一些无用的对象,或者说是已经被弃用的对象,而这些对象会占用着一部分的内存空间,如果长时间不去回收这些内存空间,那么最终会导致OOM(内存泄漏)。
在内存区域中,有三个区域,分别是:程序计数器,虚拟机栈,本地方法栈。这三个区域和线程是紧密相连的。


GC的性能
通常对GC性能的评定,考虑GC的性能优劣,基本上要从三方面着手:吞吐量,延迟,内存。


关于三大简单GC算法

  • 标记清除算法:标记算法的原理是非常简单的,首先我们从根(root)开始对可能被引用的对象用递归的方式进行标记,而未被标记的对象就是我们应当去回收的对象,如下图所示:
    这里写图片描述
    注意:标记清除算法有一个非常严重的问题:内存碎片化。
  • 复制收集算法:复制收集算法是在内存碎片化问题上的一种解决办法,在复制收集算法中,会把从根(Root)开始被引用的对象复制到另外的空间中,最终会有一个空间内存放着所有不需被清除的对象,并且它们的内存是连续的。如下图所示:
    这里写图片描述
    注意:虽然这可以很好的解决内存碎片化,但是却引发另外的内存空间,使得复制收集算法需要高成本的内存,因为它需求的内存大小是原内存的两倍。
  • 引用计数算法:引用计数的算法和标记清除的方式其实有些类似,原理是在每个对象中保存该对象被引用次数,引用数有变化时进行更新,而一个对象的引用数为0,说明该对象是弃用的,应当被回收。如下图所示:
    这里写图片描述
    注意:引用计数算法是无法释放循环引用的对象的,如下图所示,无法正常计数:
    这里写图片描述

关于三大高级GC算法
上面提及的三大简单GC算法是GC的基本算法,而三大高级GC算法是结合三种方式的高级GC算法。这三大高级GC算法是:分代回收,增量回收,并行回收。


关于GC的分代回收
GC将对象数据进行分类。主要是两类:年轻代(Young Generation),老年代(Old Generation)。

年轻代(Young Generation):通常最新被创建的对象会被分配在这里,而大多数对象会很快的变得不可达,因此,很多对象会在被成年轻代之后就消失,而这个过程我们称之为“ minor GC”

老年代(old Generation):对象来自新生代,如上所说部分对象会不可达,而剩下的从年轻代中存活下来,被拷贝至老年代。老年代所占用的空间要比年轻代多。如上,对象在老年代也会消失,而这个过程被称之为“major GC”(或者是 “full GC”).

Permanebt Generation :持久代,或者称为方法区(method area),通常持久代用来保存类常量以及字符串常量。而特别需要注意这个持久代区域不是用来保存从老年代存活下来的对象的。持久代也可以发生GC。同时这个区域的GC会被看待为 major GC.

下面简单聊聊年轻代的结构组成,年轻代含两种结构,伊甸园空间(1个)和幸存者空间(2个)。
年轻代遵循以下规则:

  • 通常刚刚被创建的对象会存放在伊甸园空间。
  • 伊甸园空间执行GC后,存活着的对象会被转移至其中一个幸存者空间中。
  • 此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
  • 若一个幸存者空间已经饱和了,那么存活的对象就会保存至另一个幸存者空间内,并且之后的操作会去清空之前那个已饱和的幸存者空间。
  • 执行GC多次后,依然存活的对象会被转移至老年代。

而老年代方面,对象在年轻代存活的时间足够长,并且没有被清理掉,那么最终会复制到老年代,注意:老年代的存储空间要比年轻代大,可以存放更多的对象,不过老年代执行GC的次数要比年轻代少,当老年代存储空间不够时,那么就会执行GC,这个GC叫做major Gc,也叫 full GC 。

另外的就是永久代(方法区):
永久代主要回收两种:常量池中的常量,无用的类信息。
要知道常量的回收是相对简单的,主要是无用的类回收比较麻烦,要注意以下几点:

  • 类的实例已经全部被回收了
  • ClassLoader已经被回收
  • 类的对象没有被引用

伊甸区的大部分对象都是刚被分配的,而幸存区用来存储的是从伊甸区内幸存下来的对象,伊甸区和幸存区组成了“年轻代


增量回收
要知道GC的执行本身是耗时间占内存的,并且GC本身执行的时间上是具有不确定性的,在GC执行过程中,会中断其他程序的执行。在对实时性要求高的程序中,是非常重视GC的最大中断时间的,所以有必要将GC的执行“分段”,这种回收方式就是增量回收。在增量回收的过程中,GC的执行是渐进的,在回收过程中,程序本身是继续运行的,在这个过程中对象的引用关系可能也会改变。如果已经完成扫描和标记的对象被修改了,对新的对象产生了引用,这个新的对象就不会被标记。在增量回收过程中,为了解决这个问题,我们需要使用写屏障,在标记的对象的引用关系发生变化时,使用写屏障会将被引用的对象作为扫描的起始点记录下来。当然,增量回收也会有遗留问题存在:中断操作需要消耗一定的时间,GC所消耗的总时间也会随之增加。


关于并行回收
这就好比如今的计算机,多核CPU处理方式,这是提升计算机运行速度的一种手段,例如生活中,我们可以边走路,边打电话,而不是打电话的时候停下来不走路,也不是走路的时候挂电话。因为在环境允许的情况下,很多事是可以同时做的,多核CPU就可以并行处理多个任务。并行回收的原理就是所有程序运行的同时执行GC操作。不过,要记住一点,要让GC完全和其他程序并行操作,并且一点都不互相影响执行速度,这基本是做不到的,总的来说,在GC执行到某个特定阶段时,是需要停止其他程序的运行的。


关于GC的特点
需要回收的对象必须要回收,不该回收的对象一定不能回收。回收所消耗的时间必须要尽可能少,因为回收机制本身就是要消耗内部资源的,因此,我们必须要在时间,空间,效率上都要考虑到。要知道在内存碎片问题、可伸展性、可伸缩性方面考虑,我们可以针对性的选择不同的垃圾回收器。


关于GC自身的初始化
GC是一个自动执行的进程,所以Java并不会去要求开发者去主动初始化GC,在Java中有 system.gc()和 Runtime.gc() 可以hook,请求JVM调用GC进程。JVM是具备选择拒绝启动GC的请求的能力的,所以有请求未必就有真正的调用GC。那么JVM是怎么去决定的呢?事实上,JVM会根据内存空间的Eden区(下文会讲解Eden区)的使用情况做出正确的判断。


GC的基本原理
对于GC来说,对象从被创建时,GC就开始了对对象的监控,包括对象的地址,对象的大小,以及对象的使用情况。GC常常是采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式来判断对象的“可达”与“不可达”。当对象变得“不可达”后,GC将会负责回收“不可达”对象的内存空间。注意:不同的JVM要用不同的回收算法。

下面讲讲一些相关的垃圾回收算法:

  • 标记清除算法:标记可以被回收的内存,然后进行回收处理,而标记-清除算法是有两阶段的,一是标记,二是清除。
  • 复制算法:将内存按容量分为两块,例如A、B两块,每次只使用其中的一块,当要进行回收操作时,将A中还存活的对象复制到B块中(假设上次使用A),然后对A中所有对象清空就又构成一个完整的内存块。
  • 标记整理法:标记整理法就是在标记清除方法上进行的优化,主要是在标记完成后将这些存活的对象向一端移动,然后将末尾边界后的所有内存空间清除。
  • 火车回收算法:在火车算法中,内存被分为块,多个块组成一个集合。为了形象化,一节车厢代表一个块,一列火车代表一个集合。注意每个车厢大小相等,但每个火车包含的车厢数不一定相等。垃圾收集以车厢为单位,收集顺序按被创建的先后顺序进行。

关于全局暂停事件
在GC执行过程中,程序的暂停被称为“全局暂停事件”,为了进行垃圾回收,是不得不这么做的。针对算法的不同,收集器的不同,全局暂停事件可能会在不同时间或者不同地方暂停程序。而为了完全停止程序,则需要暂停所有运行中的线程。


阅读更多
换一批

没有更多推荐了,返回首页