什么是垃圾回收(GC)
垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。 注意:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁。但不能说是回收对象,可以理解为一种文字游戏。 引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。 垃圾:无任何对象引用的对象。 回收:清理“垃圾”占用的内存空间而非对象本身。 发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中。 发生时间:程序空闲时间不定时回收
对象的生命周期
- 创建阶段(Created)
在创建阶段系统通过下面的几个步骤来完成对象的创建过程:
- 为对象分配存储空间
- 开始构造对象
- 从超类到子类对static成员进行初始化
- 超累成员变量按顺序初始化,递归调用超累的构造方法
- 子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到应用状态
- 应用阶段(In Use)
对象至少被一个强引用持有。
- 不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。 简单说就是程序的执行已经超出了该对象的作用域了。
- 不可达阶段(Unreachable)
对象处于不可达阶段是指该对象不再被任何强引用所持有。 与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为**”GC root”。**存在着这些GC root会导致对象的内存泄露情况,无法被回收。
- 收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
- 终结阶段(Finalized)
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
- 对象空间重分配阶段(De-allocated)
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。
不要重载finazlie()方法!原因有两点:
- 会影响JVM的对象分配与回收速度
在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。
- 可能造成该对象的再次“复活”
在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。
判断对象是否是垃圾
上面有提到垃圾对象就是:无任何对象引用的对象。而我们GC算法要回收的就是垃圾对象,那么如何判断一个对象是否是垃圾呢?主要有两种算法:引用计数法和根搜索法
引用计数法
堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。 当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
根搜索法
首先了解一个概念:根集(Root Set) 所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。 这种算法的基本思路: (1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。 (2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。 (3)重复(2)。 (4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。 Java和C#中都是采用根搜索算法来判定对象是否存活的。 首先,垃圾回收器将某些特殊的对象定义为GC根对象。所谓的GC根对象包括: (1)虚拟机栈中引用的对象(栈帧中的本地变量表); (2)方法区中的常量引用的对象; (3)方法区中的类静态属性引用的对象; (4)本地方法栈中JNI(Native方法)的引用对象。 (5)活跃线程。 接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。 存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。
GC算法
明确了GC的工作区域(堆区)和要GC的目标(垃圾对象),那么JVM是按照什么规则去回收这些垃圾对象呢? 主要有三种最基础的算法:标记 -清除算法、复制算法、标记-压缩算法 但是我们常用的垃圾回收器一般都采用分代收集算法
标记清除法
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事:
- 第一:标记,标记从树根可达的对象
- 第二:清除(清楚不可达的对象)。
标记清除的时候有停止程序运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。 缺点:
- 递归效率低性能低
- 释放空间不连续容易导致内存碎片;
- 会停止整个程序运行;
复制算法
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象清除掉,变成空闲区。 特点:
- 解决了产生内存碎片的问题
- 损失了一半的可用内存
标记整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 JVM垃圾回收分代收集算法:
- 分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。
- 分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域。