Java垃圾回收机制的理解

Java与C++有一个很大的不同点就是使用Java编程时我们不用特意关注内存的使用,尤其是内存的释放。在C++中,用new关键字申请的内存区域得不到delete关键字的清理时就会产生内存泄漏问题,在Java中,jvm有自己的垃圾回收机制,能自动的回收我们不会再使用的对象。

判别垃圾算法

1.引用计数法

在Java中我们new一个对象时会得到一个这个对象类型的引用。

例如 String str = new String("Hello World!");

就创建了一个String类型的对象并返回一个String类型的引用,我们用str去接收了这个引用。

在引用计数法中,创建对象后,每有一个新的引用类型变量引用这个对象,就给对象的引用计数器加一,在jvm进行垃圾回收时,判断每个对象的引用计数器,如果引用数等于0,则说明这个对象没有被引用,那它肯定无法被程序再使用了,因此jvm就回收这个对象的内存。

图中对象1被多次引用,说明程序极有可能还在使用这个对象(在Java中几乎都是使用引用操作对象),所以jvm不会去回收这个对象,但是对象2就很惨了,没用被引用,说明在程序中没有线程在使用它,那他的存在就没有意义了,因此jvm就回收对象2。引用计数法非常好理解,比如我们有1个气球,这个气球上有很多的线衍生出来,只要有一个人抓其中的一根气球线,气球就不会飞走,我们就可以使用这个气球。相反的,如果这个气球上一条线也没有,或者没有人去抓它的线,那这个气球肯定会飞走,我们就无法使用它。jvm就负责清理我们无法使用(不会再去使用)的对象。

对于引用计数器,它有个致命的缺陷,那就循环引用问题

在上图中,对象1和对象2在外界没有被引用,按道理来说他俩是垃圾对象,应该被回收,但是这两个对象中各有一个指向对方的引用,这就导致对方的引用计数器不为0,jvm在检测垃圾的时候无法把他俩判定为可以回收的垃圾,也就没有办法回收他俩占用的内存区域,因此造成了内存泄漏。为了解决这个问题,就诞生了第二中回收策略。

2.可达性分析法

可达性分析法是通过一个GC Root(可以理解为一个根引用)作为起始点往后遍历搜索,把遍历到的对象都做一个标记,直到全部都遍历完毕后停止。这时,只要是被程序有效引用的对象都被做了标记,那没有做标记的就是当前程序不会去使用的对象,这个对象所占内存就会被垃圾回收器回收。

在上图中,把引用作为GC root向下遍历,可以顺着GC链(每一个连接两个对象的引用链)找到对象1,对象2,对象3并给这些对象做一个标记。jvm通过标记判断认为对象1,对象2和对象3都是有用的对象,但是对象4因为没有被做标记,jvm就认为它是个无用的对象,就会回收它。

这个策略也很好理解,就拿之前气球的例子,jvm从每一个拿气球的人开始遍历,然后顺着气球线把每个气球都做上记号,那么没有被做上记号的气球就是没有被人持有的气球,自然也就是无法使用的气球。

可达性分析法完美的解决了引用计数法存在的循环引用的问题,如果出现了循环引用,因为没有可以连接循环对象的GC链,所以这些循环引用的对象没有被做上相应的标记,因此jvm是不会放过他们的。现在大多垃圾回收器都使用可达性分析法,毕竟引用计数法有很大的缺陷,而且jvm还要新开辟内存用来记录引用数,所以无论是可用性还是效率方面可达性分析法都比引用计数法要好。

垃圾清理算法

1.标记-清除

标记清除法,如字面意思一样,就是把找到的垃圾做标记,然后清除清除所有带有垃圾标志的对象。很好理解,

    

图一中每个垃圾对象都被做了标记,接着jvm就会回收做了标记的对象的内存,回收之后就变成了图二这样的情况,因为标记清理算法并不会进行垃圾回收后的内存整理,这样每个被回收后的内存区域就会散落在整个java堆内存的各个角落,产生很多的内存碎片。这样就非常尴尬了,当我们再在程序中申请一块比较大的内存区域时,因为java堆中空闲区域都是零零散散的小区域,不能找出一块完整的,较大的区域,所以就无法满足这个请求了,于是jvm认为内存不够用了,就又进行一次垃圾回收。。。。所以说标记清理算法有一定的缺点,它只能用在特定的区域中。

2.标记-整理

标记整理算法可以说是标记清理算法的改进版本。首先还是对需要清理的垃圾对象做标记,这时我们把java堆内存想象成一个队列,每次找到垃圾对象就放到队尾,找到有用的对象就放到队头,当全部对象遍历完毕之后,所有有用的对象都是从队头向队尾延伸存放的,所有垃圾对象都是从队尾向队头延伸存放的,jvm只需要找到有用对象和垃圾对象的分界线,然后把分界线后边的对象全部清理掉即可。

如图所示,标记整理算法不仅清理了垃圾,而且避免了产生过多的内存碎片,这样在程序下一次请求较大的内存区域时就可以满足需求,间接得减少了jvm垃圾回收的次数,提升了代码的性能。

3.复制-清理算法

复制清理算法与上述两个算法最大的区别就是它的内存模型不同,复制清理算法是基于分为两大块的内存工作的。把整个内存分为两块,在一段时间内只用其中的一块,另一块标记为等待状态,当使用的这一块内存区域没有可用内存时,开始复制清理的过程。首先还是找出垃圾对象,然后把不是垃圾的对象复制到另一块等待状态的内存中并把这块内存标记为使用状态,程序接下来的一段时间使用这块新的内存,之前的内存块则是全盘清理并标记为等待状态,等待下一次复制。

 

复制清理算法也可以避免产生内存碎片的问题,要注意的是jvm在用复制清理算法做垃圾回收时程序是暂停运行的。

垃圾处理综合策略-分代垃圾回收策略

上面说了很多关于判断垃圾,清理垃圾的算法,但是在真正的jvm中并不会去使用某一种算法,而是充分利用每一个垃圾判别/处理算法的特性,让程序整体的性能有更大的提升。怎么决定什么时候使用哪一种算法呢,这就要从内存划分说起了。

在java堆内存中,主要分为新生代,老年代,持久代三块内存。

新生代是对象产生的地方,每次用new关键字产生的对象都是存放在新生代中,因此,新生代也是垃圾回收器的主要工作场地,因为在程序中很多的局部变量在方法执行结束之后就是可以被回收状态的,当垃圾回收器工作室,在新生代上的局部变量就可以很快的被回收。在新生代中,又分为Eden区和两个Servivor区,它们按照8:1:1比例划分,为什么要划分成这样呢,因为在新生代中内存回收策略是基于复制清理算法的,这就需要我们至少要有两块内存去供jvm使用。新生代垃圾回收具体方法是新创建的对象放在Eden中,当Eden区的内存满了以后,就把Eden区有用的对象复制到一个Servivior0区中,然后清理Eden区,第二次Eden区内存满了以后,继续把有用的对象复制到Servivor0区,当Servivor0区的内存满了以后,再把Servivor0区中的有用的对象复制到Servivor1区中,然后清理Servivor0区,下次Servivor1区满了再整理到Servivor中,周而复始。

老年代和新生代不同,在老年代中存放的对象基本都是存活时间很长的对象,而且老年代的内存是新生代的2倍。什么情况下对象会进入老年代呢,当Servivor区中的对象满了以后,就会把对象存放到老年代,这时Servivior区中的对象几乎都是经历了很多次垃圾回收还能保存下来的对象,说明这个对象的寿命真的很长,于是就把他放在老年代中。老年代的对象相比新生代会长很多,所以老年代产生的垃圾对象会很少,这时如果再采用复制清理算法就显得有点麻烦了(需要复制的对象太多了),所以老年代的垃圾回收策略大多使用标记整理或标记清除算法。而且为了提升整体效率,老年代的垃圾回收次数比起新生代也要少很多(毕竟几乎没什么需要清理的)。

持久代是专门存放一些静态变量和class类型的数据的,垃圾回收器也会回收这里的内存空间,但是效果很差,因为这里的数据被判断为垃圾的条件特别严苛。

两种垃圾回收(GC)机制

由于老年代和新生代中存放对象的性质的不同,jvm也有两种不同的垃圾回收机制

1.新生代局部清理(Scavenge GC)

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行垃圾回收,清除垃圾对象,并且把有用的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的垃圾回收机制是对新生代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的垃圾回收会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

2.完全清理(Full GC)

对整个堆进行整理,包括新生代,老年代和持久代。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

1.年老代被写满

2.持久代写满

3.System.gc()被显示调用

4.上一次垃圾回收之后堆的各域分配策略动态变化

各种垃圾收集器

Serial收集器(复制算法)

新生代单线程收集器,标记和清理都是单线程,优点是简单高效。

Serial Old收集器(标记-整理算法)

老年代单线程收集器,Serial收集器的老年代版本。

ParNew收集器(停止-复制算法) 

新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

Parallel Scavenge收集器(停止-复制算法)

并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

Parallel Old收集器(停止-复制算法)

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先

CMS(Concurrent Mark Sweep)收集器(标记-清理算法)

高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择

 

新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge

老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

关于jvm垃圾回收的思考

jvm在垃圾回收方面真的是做的相当好,省去了程序员很大一部分的工作量,但是也不能全部都依靠垃圾回收器,在有jvm垃圾回收器的时,如果代码处理不当,内存泄漏依然会发生,在一些容器类就会发生,还有通常使用各种连接没有调用close方法,使用监听器在对象被释放时没有得到删除时,jvm会认为他们还是有用的对象,所以不会回收他们的内存。因此jvm垃圾回收器再完美,对于程序员来说,写出高效且优雅的代码还是第一重要的。

 

本文是笔者自己的理解,有错误的地方还请大家指出。

文章参考了《java编程思想》《深入理解java虚拟机》和http://www.cnblogs.com/andy-zcx/p/5522836.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值