i3geek.com
闫庚哲的个人博客

详解JVM中的垃圾回收机制(GC)


垃圾回收(Garbage Collection,GC),很多人都会联想到java虚拟机中的垃圾回收机制。在C/C++中,内存是需要程序员去管理的,程序员在使用的时候 需要先new一个新的对象,在使用完成后,通过delete等关键字进行释放资源。但是在java中,对于内存的分配和回收则是不需要成员关心的,一切都交给了虚拟机处理,所以我们在此了解下虚拟机的工作原理,如何清除垃圾。

1、如何确定“垃圾”

既然是垃圾回收机制,第一步肯定是要确定垃圾,知道了垃圾便可以进行回收。但是如何确定垃圾呢?什么是垃圾呢?

什么是“垃圾”

首先要明白什么是“垃圾”,垃圾回收机制是回收堆内存中的对象(具体的内存划分可以看:),对于栈中的对象是不需要回收机制去考虑的。在Java中堆内存中的对象是通过和栈内存中的引用相互关联,才被利用的。既然是对堆内存的回收,并且堆内存中存储的都是引用对象的实体,所以回收的就是没有被任何一个引用所关联的实体对象。

因此,“垃圾”实质上指的是java虚拟机中堆内存里没有被引用到的,永远也不会访问到的实体对象。

引用计数法

既然明白了什么是“垃圾”,那么该如何找垃圾呢?很显然,通过定义可知,没有引用的对象就是垃圾,所以可以通过查看对象的当前被引用的数量来判断是否应该回收。

比如当一个引用关联了实体对象后,就在对象的引用数加1,若取消引用则减1.当引用数为0时即被系统回收。看似简单的方法,却十分高效,但是却存在一个弊端——循环引用。假如两个对象互相引用时,各自的引用数都为1,可是对象是永远都访问不到的,如下代码:

public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();

object1.object = object2;
object2.object = object1;

object1 = null;
object2 = null;
}
}

class MyObject{
public Object object = null;
}

虽然两个对象最后都被赋为null,但是由于计数不为0,始终不会被回收。所以在JAVA中并没有采用这种方法。(Python采用的是引用计数法)

可达性分析法

通过一系列被称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链项链时,则证明此对象是不可用的。

在Java中GC Roots的对象包括:虚拟机栈中引用的对象,方法区类静态属性引用的对象,常量引用的对象,本地方法中引用的对象。

当然在回收的过程中,不是被判断为不可达的对象后就成为可回收对象,一般情况下至少要被标记两次以上的对象 才可能会被成为可回收的对象。

常见的可将对象判定为回收对象的情况

(1)显示的将某对象引用赋值为空(null)

Object obj = new Object();
obj = null;

(2)显示的将某对象的引用指向新的对象

Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;

(3)生命周期结束的对象

for(int i=0;i<10;i++)
{
int Object obj = new Object();
}

(4)只有弱引用与其关联的对象

WeakReference<String> wr = new WeakReference<String>(new String("world"));

2、典型的垃圾收集算法

在第一步骤确定了什么是“垃圾”之后,下一步显然是对垃圾的回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

1.Mark-Sweep(标记-清除)算法

这个是最简单的算法,也是最基础最容易实现的算法。标记清楚算法分为两个阶段:标记阶段和清除阶段。标记阶段是找出所有需要被回收的对象,并作出标记;清除阶段是回收被标记的对象所占用的空间。

所上图所示,可以简单的对内存进行回收,但是同样存在一个弊端,就是容易产生内存碎片,大量的内存碎片会无法为大对象分配足够的空间,进而导致内存利用率低。

2.Copying(复制)算法

该方法是针对标记清楚法容易产生碎片的问题,而提出的新的思想。也比较容易理解。

首先将可用内存空间按照大小平均分为两部分,在使用时只使用其中的一部分,另一部分不使用。当那一部分满了之后,触发收集机制,将还存活的对象复制到另一块内存上面,然后把当前内存的空间一次清理掉,这样就不容易出现内存碎片了。

具体流程:如图,(1)上半部分内存使用。(2)用满后,执行算法,存活的复制到下半部分。(3)下半部分内存使用。(4)下半部分用满后,执行算法,存活的复制到上半部分。

虽然该方法简单,且不易产生碎片,但是却付出了高昂的代价——将可使用的内存空间缩减到原来的一半。而且该算法的效率跟存活对象的数目多少有很大的关系,如果存活的对象很多 那么效率就会大大降低。

3.Mark-Compact(标记-整理)算法

为了解决复制算法的缺陷,充分利用存储空间,提出了标记整理算法。该算法结合了标记清除和复制算法,分为两个阶段。第一阶段:同标记清楚算法,先标记出待回收的对象。第二阶段,不是直接清楚可回收对象,而是将存活对象都向一端移动,然后清理掉可回收的内存。

该方法简单易懂,结合以上两种算法的优势,但是相比之下,效率较低,而且会随着存活对象的增加而降低效率。

4.Generational Collection(分代收集)算法

目前大多数JVM的垃圾收集器采用的算法都是分代回收算法。它的核心思想是,根据对象存活的生命周期将内存划分为若干个不同的区域。因为每个对象的生命周期都是不一样的,有些对象是与业务相关的,比如线程、Scoket、Http请求中的Session等,生命周期就比较长;但是还有一些,如局部变量、临时变量等,这些的生命周期就会比较短。如果不根据存活时间进行区分,每次收集都扫描全部的对象的话,会花费较长的时间。而且对于长生命周期的对象而言,多次的这种遍历是没有效果的,他们仍然存在,导致效率低下。

因此,分代垃圾回收机制是采用了分治的思想,进行代的划分,不同的生命周期对象放在不同代上,对于不同代采用不同的最适合它的算法进行垃圾回收。

目前大部分收集器会划分成三代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。

年轻代(新生代)

该区域主要存放生命周期较短的对象,所以每次垃圾回收中都需要回收大部分对象,因此该区域采用复制(Copying)算法,也就是说复制操作少,效率不会太低。

但是在实际中,对于新生代的空间并不是1:1的划分,为了提高空间的利用率,一般将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和一块Servivor空间,当回收时,将Eden和Servivor中存活的对象复制到另一块Servivor空间中,然后清理掉Eden和刚刚用过的Survivor空间。保证始终有一个Servivor空间是空闲的,在触发收集算法时,对于从上一个Survivor区复制来还存活的对象,将被复制到“老年代”。注意,在一块Servivor中可能同时存在从Eden和上一块Servivor中复制来的对象,但是只有从Servivor复制来的对象,可以被复制到老年代。同时,程序可以根据需要,配置多个(多余两个)Survivor区,这样可以增加对象在年轻代中的时间,减少被放到年老代的可能性。

年老代

年老代一般都是生命周期较长的,或者在年轻代经历了N次垃圾及回收后仍然存活的对象,就会被放到年老代中。因此该区的特点是每次回收都只有少数对象被回收,所以一般使用的是标记整理(Mark-Compact)算法。

永久代(持久代)

它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如 Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

3、什么情况下触发垃圾回收

由于根据对象的生命周期进行了分代,所有不同区域的回收时间和方式是不一样的,主要有两种类型:Scavenge GC和Full GC。

Scavenge GC

这个是对新生代的回收方法,一般情况下,当新生代空间Eden申请失败时就会触发Scavenge GC,进行新生代的回收,执行复制算法,将存活的对象复制到Survivor区。

但是不会影响老年代,由于一般Eden区不少很大,所以Eden区的GC会频发进行。

Full GC

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

有如下原因可能导致Full GC:

  • 年老代(Tenured)被写满
  • 持久代(Perm)被写满
  • System.gc()被显示调用
  • 上一次GC之后Heap的各域分配策略动态变化

4、典型的垃圾收集器

垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。所以在此不做重点,有感兴趣的可以自己去了解。

G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

赞(0)
未经允许不得转载:爱上极客 » 详解JVM中的垃圾回收机制(GC)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址