现在的位置: 首页 > 综合 > 正文

JVM较深入解析(二)

2018年09月29日 ⁄ 综合 ⁄ 共 3380字 ⁄ 字号 评论关闭

一、垃圾回收算法

垃圾回收算法有哪些?

按回收策略方式分:

引用计数(Reference Counting):每个对象有一个引用计数器,对该对象的引用多1一个,引用计数器加1,反之减1,当应用计数器的值为0时,该对象可以回收。此算法最致命的是无法处理循环引用的问题。

标记-清除(Mark-Sweep):从根结点开始标记所有被引用的对象,然后遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时会产生内存碎片。

复制(Coping):此算法把内存分为两个相等的区域,每次只是用其中的一个。垃圾回收时,遍历正在是用的区域,把正在使用的对象复制到另一个区域,然后把当前区域清除。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

标记-整理(Mark-Compact):先从根节点开始标记所有被引用的对象,然后遍历整个堆,清除未标记的对象并把存活对象压缩到堆中的一块,按顺序存放。这样既避免了内存碎片问题,又避免了“复制”算法的空间问题。

按分区方式分:

增量收集(Increasemental Collecting):实时垃圾回收算法,即在应用进行的同时进行回收。

分代收集(Generational Collecting):基于对象生命周期的回收算法,把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法。

按系统线程分:

串行收集:使用单线程处理所有垃圾回收工作,适合单处理器机器;

并行收集:使用多线程处理垃圾回收工作,速度快,效率高;

并发收集:前两个垃圾回收工作需要暂停整个运行环境,只有垃圾回收程序在运行,堆越大,暂停时间越长。

如何区分垃圾?

垃圾回收从哪里开始?因为栈是真正进行程序执行的地方,所有要获取哪些对象正在被使用,需要从Java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。除此之外,系统运行时的寄存器,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,接着从这些对象找到堆中其他对象的引用,这种引用逐步扩展,最后以null或者基本类型结束。这样就形成了一颗以Java栈中引用所对应的对象为根结点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。因此,垃圾回收的起点是一些根对象:Java栈、静态变量、寄存器等。最简单的Java栈是Java程序执行的main函数,这种回收方式就是标记-清除的回收方式。

如何处理碎片?

由于不同Java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。

如何解决同时存在的对象创建和对象回收问题?

垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整个应用(即:暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决二者矛盾的方式。

但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决。

二、分代垃圾回收

为什么分代?

分代的垃圾回收策略,是基于这样一个事实:不同对象的生命周期是不一样的。在Java程序运行的过程中,会产生大量的对象。有些生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短。分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

如何分代?

虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

年轻代:

所有新生成的对象都是放在年轻代,年轻代的目标是尽可能快速收集那些生命周期短的对象,年轻代分为三个区:一个Eden区,两个Survivor区。大部分对象在Eden区生成,当该区满了时,其中还存活的对象会被复制到一个Survivor区,当这个Survivor区也满了时,其中还存活的对象会被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的还存活的对象会被复制到年老去(Tenured)。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。

年老代:

在年轻代中经历了N次垃圾回收仍然存活的对象会被放到年老代,因此其中存放的都是一些生命周期较长的对象。

持久代:

存放静态文件,如Java类、方法等。它对垃圾回收无显著影响。

什么情况下触发垃圾回收?

GC有两种:Scavenge GC和Full GC。

Scavenge GC:当新对象要生成却无法从Eden获取空间时,就会触发ScavengeGC。对Eden区域进行GC,清除非存活对象,并把存活对象复制到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC:对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:年老代(Tenured)被写满、持久代(Perm)被写满、 System.gc()被显示调用、上一次GC之后Heap的各域分配策略动态变化。

分代垃圾回收:

1.Eden区满了,触发ScavengeGC,将Eden中存活的对象复制到To区,清空Eden。此时,Eden和From为空,To中不为空。

2.Eden区又满了,再次触发ScavengeGC,To区也会被扫描,将Eden和To中存活的对象复制到From区。此时Eden和To为空,From不为空。From中存放着两种对象:1代对象——刚从Eden复制过来的;2代对象——从Eden复制到To然后又复制到From的。

3.Eden区又满了,再次触发ScavengeGC,From区也被扫描。将From区中还存活的2代对象复制到年老区,From区中存活的1代对象复制到To区,Eden中存活的对象复制到To区。此时,Eden和From为空,To和年老区中不为空。年老区中为3代对象——从Eden到To到From再到年老区;To中存放两种对象:1代对象——刚从刚从Eden复制过来的;2代对象——从Eden复制到From然后又复制到To的对象。

【上篇】
【下篇】

抱歉!评论已关闭.