Java GC专家之路-浅析GC

1、对象不可达:没有任何其他对象引用该对象
由于Java 自动垃圾回收机制的存在,所以使得Java程序员不必关心代码申请的内存空间需要释放的问题,但是,如果我们代码写的不好还是存在内存泄露的风险,因此熟悉GC能够应该是一个优秀Java程序员必备的技能,同时,选择合适的GC算法对我们开发的系统也很重要。本篇是Java GC专家之路的第一篇,主要对GC做一个简单的介绍,下一篇将介绍分析GC的状态和GC的配置。
     首先需要介绍的就是GC的“stop-the-world”原则,具体说来就是在JVM进行GC的时候,会暂停我们的应用,不管选择哪种垃圾回收算法,只要进行GC,系统将无法提供服务,只是一般这个动作非常快,以至于用户都感觉不到GC的存在,当JVM执行GC时,除了GC线程外,其他线程都会被阻塞,一直到GC完成,所以通常说的GC调整优化主要是缩短GC的时间
     Java编程时,并不需要通过代码来申请和回收内存,而有些程序员希望通过设置对象为null或者调用System.gc()来释放内存,设置对象引用为null倒没什么大不了,但是调用System.gc()是非常耗费系统性能的操作,不应该在程序中出现这种代码,
     GC通过垃圾回收器搜索无用的Java对象并且删除,从而释放内存,那么垃圾回收器的基于如下两个假设(或者叫前提)来创建
     1、内存中大部分对象都是不可达的(即不存在依赖)
     2、之前存在的对象(生存周期更长)只对一小部分新创建的对象存在依赖
         上面两个假设在Java中被成为“ 弱代假说”,为了基于这种假设来实现GC, HotSpot VM(sun公司垃圾回收器)物理上将内存内存分为新生代(young generation)和老年代(old generation)
年轻代:刚刚被创建的对象保存在此,由于绝大部分对象都会很快不可达,因此在新生代进行GC(minor GC)时将会被回收
老年代:经过新生代GC之后,对象还是可达的,将会被拷贝到该区域,老年代一般比年轻代大很多,每次GC浪费的时间比较长,因此其GC的频率相对年轻代而言也比较低,该区域的对象在进行GC时(major GC or full GC)被回收
下面这张图就展示了Java中内存的区域划分

(Java内存结构)


上面的永久代(permanent generation)也叫做方法区,主要用来存储类文件或者内部字节码,从老年代存活到该区域的对象也会被垃圾回收,这里发生的GC也被记做一次major GC。
或许有人会问,如果老年代一个对象引用了新生代对象,GC应该怎么处理呢??

为了解决这个问题,Java采用了一种叫做标记表(card table)的结构,其长度为512个字节,任何时候,只要有老年代的对象引用了新时代的对象,都会记录在标记表中,当新时代进行GC的时候,只需在该比较表中查询对象是否被老年代引用,而不用搜索整个老年。标记表是由Java写屏障(write barrier)来管理的,Java的写屏障是一个给Java提供了高性能minor GC的组件,由于它的存在使得minor GC的时间大大缩短。


(标记表结构)

新生代
新生代主要由三部分组成
     一个Eden区
     两个Survivor区
在这三个区中,对象的处理流程如下:
1、绝大部分新创建的对象会被放置在Eden区
2、当Eden区经过一次GC之后,存活的对象将会移动到两个Survivor区中的一个
3、当Eden区经过一次GC之后,存活的对象将会被集中到正在使用的Survivor区(两个中的一个)
4、如果正在使用的Survivor区满了,GC之后的对象会被移动到另外一个Survivor区,同时将该Survivor区清空
5、重复多次4之后,依然存活的对象会被移动到老年代
通过上面的步骤可以知道,在任何时候,两个Survivor区肯定有一个是空的,
如果数据存在两个Survivor区或者两个Survivor区都为空,那么系统是非正常状态
在新生代,GC前后的对比图如下:


在HotSpot VM中,采用了bump-the-pointer和TLABs (Thread-Local Allocation Buffers)两种技术来加快内存的分配速度
bump-the-pointer:只检查Eden区顶部上次非配的内存大小是否满足本次内存需求,但是在多线程环境下由于线程安全的要求
锁会导致性能下降。
TLABs:每个线程都有自己的Eden区空间,并且只能访问自己的Eden区,这样在多线程环境下,不存在锁的问题,能够快速非配内存

以上只是对新生代的简单介绍,关键点:对象刚刚创建之后,会分配到Eden区,长期存活在新生代的对象会被转移到老年代

老年代GC算法

当老年代空间不够时,会触发一次老年代GC,在JDK7中,主要有5中垃圾回收器
1、Serial GC
2、Parallel GC
3、Parallel Old GC (Parallel Compacting GC)
4、Concurrent Mark & Sweep GC  (or "CMS")
5、Garbage First (G1) GC

在上述垃圾收集器中,Serial GC不能用于服务器,只能用于单CPU的PC,使用这种GC会造成很大的性能损失

下面依次介绍一下各种垃圾收集器
Serial GC (-XX:+UseSerialGC)
上面已经介绍了该GC收集器在新生代的操作流程,在老年代,它主要采用了标记-清理-压缩算法(mark-sweep-compact)

该收集器主要步骤:

1、标记老年代中的对象

2、从堆的开始到结束对对象进行检查,只有清除不存在引用的对象(清理)

3、最后一步,将本次GC存活的对象顺序存储(清理内存碎片),然后将堆空间分为两个部分:一个包含对象;一个不包含对象(后面的对象已经压缩到第一个空间)

这种垃圾收集器比较适合小内存和单CPU的场景

Parallel GC (-XX:+UseParallelGC)


从上图可以很明显查看串行垃圾收集器和并行垃圾收集器的差异:串行Serial)垃圾收集器只有一个GC进程,而并行(Parallel)垃圾收集器使用多个GC进程进而提升了GC的速度,在多处理器的场景下,GC效果比较明显,该种GC也被称为“吞吐量GC”

Parallel Old GC(-XX:+UseParallelOldGC)

JDK5中加入了Parallel Old GC,这种垃圾回收器和Parallel GC唯一的不同在于它需要三步:标记-汇总-压缩,

汇总将各个区域的需要GC的对象先提取出来,这里跟“标记-清理-压缩”算法相比,略微有点复杂

CMS GC (-XX:+UseConcMarkSweepGC)


串行垃圾回收器与CMS 垃圾收集器

从上图可以看出,并行的标记-清除垃圾回收器比上面其他类型的垃圾回收器都要复杂,在初始化标记阶段,仅仅标记跟GC 类加载器直接关联的对象,因而停顿时间非常短,在并发标记阶段,标记所有已知对象,这个阶段可以与用户的线程同时工作,即此时不需要“stop-the-world"。在重标记阶段,主要用来标记并发标记阶段,用户新产生或者失去引用的对象,并发清除阶段开始进行垃圾回收,此时用户的线程可以并行执行,由于采用了并发执行,GC的暂停时间非常短,因此该收集器也被叫做”低停顿收集器“,通常用于响应时间要求比较苛刻的应用。

然而这种回收器也并不是完美的,主要存在以下问题

1、对内存和CPU的占用比其他垃圾回收器大

2、默认没有提供压缩,磁盘空间碎片较大



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值