天天看点

第3章 垃圾收集器与内存分配策略

1. 判断对象的生死

判断对象的生死由两种方法:1. 引用计数法 2. 可达性分析算法

1.1 引用计数法
引用计数法就是为对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。 优点:简单高效 缺点:无法解决对象之间相互循环引用的问题。 因为它的缺点,Java虚拟机没有选用引用计数法 1.2 可达性分析算法 这个算法的基本思想就是通过一系列的称为Gc Roots的对象作为起始点,从这些节点开始向下搜索,搜索走过的路程被称为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(即GC Roots不能到达这个对象),则证明此对象是不可用的。
第3章 垃圾收集器与内存分配策略
上图中的object5, object6,object7虽然互相关联, 但是因为GC Roots不可达,所以它们是不可用的,将会被垃圾收集器回收。
Java中,可作为GC Roots的对象包括以下几种:虚拟机栈中的引用的对象, 方法区中类静态字段引用的对象,方法区中常量引用的对象, 本地方法栈中JNI引用的对象。
1.3 Java中引用的分类
Java中的引用有四种:即强引用(Strong),软引用(Soft),弱引用(Weak),虚引用(Phantom)
强引用:强引用是在代码之中最普遍存在的引用。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:软引用用来描述一些有用但是非必需的对象。软引用对象的回收(前提是这个对象没有强引用了), 只有在将要发生内存溢出异常之前,将会回收只有软引用的对象。 如果这次回收后依然没有足够的内存,才会抛出内存溢出异常。
虚引用:若引用也是用来描述非必需对象的,但是它的强度比软引用更弱,如果一个对象只有虚引用,那么它的生命周期只能到下一次垃圾回收之前,也就是说只要发生垃圾回收,只有虚引用的对象就会被回收,无论当前的内存是否足够。
虚引用:它是最弱的一种引用方式,一个对象是否有虚引用的存在,完全对对象的生命周期没有任何的影响,并且无法通过虚引用来取得一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收之前收到一个系统通知。
1.4 关于finalize()方法 finalize()方法是一个很鸡肋的方法,所以还是忘记这个方法的存在吧。 要真正的宣告一个对象死亡,至少需要经历两次标记过程, 并且在被第一次标记的时候判断是否需要执行该对象的finalize()方法。 在这两种情况下:当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过了,虚拟机判断该对象的finalize()方法没有必要执行。 如果这个对象的finalize()方法有必要执行,那么这个对象将会被放置在一个叫做F-Queue的队列中,并且稍候会在一个低优先级的线程中去执行该对象的finalize()方法。但是虚拟机并不会保证该方法会完整的执行完。 1.5 方法区中的垃圾回收 方法区中垃圾收集器主要回收两部分的内容:无用的常量和无用的类。 回收废弃的常量和回收Java堆中无用的对象非常类似。但是判断方法区中无用的类就不一样了,  方法区中判断一个类是无用的类的条件比较苛刻,需要满足一下三个条件:
1. 该类的所有实例都已经被回收。 2. 加载该类的ClassLoader已经被回收 3. 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足了这三个条件以后仅仅只是该类可以被回收了,但并不是一定会被回收,还要根据虚拟机的相关参数的设置来进行最后的判决。
如果在程序中大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁定义ClassLoader的场景都需要虚拟机具备可以对无用类进行回收的功能,以保证方法区不会溢出。

2. 垃圾收集算法

常用的垃圾回收算法有三种:标记-清除算法, 复制算法,标记-整理算法, 分代收集算法。

2.1 标记-清除算法
该算法分为两个阶段:标记和清除(这不废话么。。),首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这个算法是最基础的算法,后面的两个算法都是基于它的改进算法。 该算法有两个主要的不足:
1. 效率问题,标记和清除两个过程的效率都不高。
2. 空间问题,标记清除之后会产生大量的内存碎片,可能会导致在以后为大对象分配空间时触发一次垃圾回收。
下面是该算法的图示过程:
第3章 垃圾收集器与内存分配策略
2.2 复制算法
该算法的主要思想是将可用内存按照容量分为大小相等的两块, 每次只使用其中的一块。 当使用的这一块的内存用完了,就将还存活的对象复制到另一块上面,当然复制的时候是在内存空间中进行连续复制的,这就使得执行该操作后内存空间变得规整,然后再把已使用的那一块全部清理掉。
优点:执行后使得内存空间变得规整,不必考虑内存碎片等复杂情况。
缺点:可以使用的内存缩小为原来的一半。

下面是该算法的图示过程:

第3章 垃圾收集器与内存分配策略
现代的商业虚拟机都是采用的复制算法来回收新生代的垃圾。因为新生代中的对象98%都是朝生夕死,所以并不需要按照1:1的比例来划分。而是将内存分为一个较大的Eden区域和两个较小的Survivor区域。Eden和Survivor(一块儿)的空间比例虚拟机默认是8:1.,在使用时, 每次都使用Eden和其中的一块Survivor。每次在回收时将Eden和使用的Survivor中的存活对象赋值到另一个未使用的Survivor中。但是这个过程并不能保证另一个未使用的Survivor空间一定足够,需要依赖老年代进行分配担保(这个后面会有)。
2.3 标记-整理算法
标记-整理算法的标记步骤仍然与“标记-清除算法”一样, 但是后续的步骤将不是直接清理,而是让所有存活的对象向一端移动, 然后直接清理掉端边界以外的内存。

下面是该算法的图示过程:

第3章 垃圾收集器与内存分配策略
2.4 分代收集算法
当前商业虚拟机的垃圾收集都采用分代收集算法, 分代就是根据对象存活的生命周期的不同将内存分为不同的几块。Java虚拟机中将Java堆分为新生代和老年代,这样就可以根据各个代的特点选用最适合的垃圾收集算法。在新生代中每次垃圾回收时都会有大批的对象死去,只有少量存活,那么复制算法在此代中适用。而老年代中因为对象存活率高并且没有额外的空间进行分配担保,那么标记清理和标记整理算法在此代比较合适。

3. HotSpot中可达性分析算法的优化

在可达性算法中,可以作为GC Roots的节点主要在全局性的引用和栈帧中的本地变量表,这些引用在大型的引用中将是非常多的,如果要遍历每一个引用,就需要耗费很多的时间,导致GC时停顿的时间很长。为了解决这个问题,HotSpot的实现中,使用了一组称为OopMap(OoP:Ordinary Object Pointer)的数据结构来存放对象引用的具体位置(在类加载的时候将是引用的位置都记录到OopMap中去)。这样就不用去遍历所有的引用位置了。 HotSpot只是在称为 安全点的地方生成了OopMap,并不是给每一个指令都生成。 安全点是程序只有在执行到此点时才可以停顿下来进行GC(并不能在任何节点都能停顿的)。

4. 垃圾收集器

JDK 1.7中 Sun HotSpot虚拟机的垃圾收集器。

第3章 垃圾收集器与内存分配策略

如果两个垃圾收集器直接有连线, 则表明这两个垃圾收集器可以搭配使用

下面分别简介这几个垃圾收集器

4.1 Serial

特点:  历史最悠久的垃圾收集器, Client模式下的默认的新生代垃圾收集器

算法: 复制算法

工作地点: 新生代

工作原理: 单线程收集器,在垃圾收集时, 必须暂停其他所有工作线程, 直到垃圾收集结束

优点: 简单高效, 单线程没有线程交互开销

缺点:垃圾收集时,需要暂停所有的工作线程
示意图:
第3章 垃圾收集器与内存分配策略

  4.2 ParNew 特点:  Serial的多线程版本。Server模式下默认的新生代垃圾收集器

算法: 复制算法 工作地点: 新生代 工作原理:  多线程收集器, 默认开启和CPU数量相同的线程数。 在垃圾收集时, 必须暂停其他所有工作线程, 直到垃圾收集结束 优点:Serial的多线程版本, 充分利用系统的资源。 缺点:存在线程交互的开销。 示意图:

第3章 垃圾收集器与内存分配策略

 4.3 Parallel Scavenge 特点:  多线程, 重点关注程序达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))

算法: 复制算法 工作地点: 新生代 工作原理:  新生代Parallel Scavenge收集器与ParNew收集器工作原理类似 ,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。 Parallel Scavenge是吞吐量优先的垃圾收集器,它还提供一个参数:-XX:+UseAdaptiveSizePolicy,这是个开关参数,打开之后就不需要手动指定新生代大小、Eden与Survivor区的比例、新生代晋升年老代对象年龄等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以达到最大吞吐量,这种方式称为GC自适应调节策略,自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。 优点:具有GC自适应调节策略。对吞吐量要求比较高的程序需要使用该GC。 缺点:同ParNew的缺点相同。

4.4 Serial Old 特点:Serial的年老代版本,单线程,Client模式先的默认的年老代垃圾收集器。 算法: 标记-整理算法。 工作地点: 年老代 工作原理:   Client模式下默认的年老代垃圾收集器。   在Server模式下,主要有两个用途:

a.在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
b.作为年老代中使用CMS收集器的后备垃圾收集方案。
新生代Serial与年老代Serial Old搭配垃圾收集过程图:
第3章 垃圾收集器与内存分配策略

新生代Parallel Scavenge收集器与ParNew收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。

新生代Parallel Scavenge/ParNew与年老代Serial Old搭配垃圾收集过程图:

第3章 垃圾收集器与内存分配策略

优点: 简单高效 缺点:需要暂停所有的工作线程

4.5 Parallel Old 特点:  Parallel Scavenge的年老代版本, 多线程, JDK1.6开始提供

算法: 标记-整理算法 工作地点: 年老代 工作原理:  在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量, Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器, 如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。 新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图:

第3章 垃圾收集器与内存分配策略

优缺点:优缺点和Parallel  Scavenge相同

4.6 CMS(Concurrent mark sweep) 特点:  多线程, 主要目标是获取最短垃圾回收停顿时间, 可以为交互比较高的程序提高用户体验。 第一款真正意义上的并发GC,  第一次实现了GC线程和用户线程同时工作 算法:  标记-清除算法 工作地点: 年老代 工作原理: CMS工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下4个阶段:

a.初始标记: 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。 b.并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。 c.重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。 d.并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。 由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
图示:
第3章 垃圾收集器与内存分配策略

优点:并发收集垃圾,低延时。 真正意义上的并发GC, 实现了GC线程和用户线程同时工作

缺点:  1.  CMS收集器对CPU资源非常敏感 ,其默认启动的收集线程数=(CPU数量+3)/4,在用户程序本来CPU负荷已经比较高的情况下,如果还要分出CPU资源用来运行垃圾收集器线程,会使得CPU负载加重。 2.  CMS无法处理浮动垃圾(Floating Garbage) ,可能会导致Concurrent ModeFailure失败而导致另一次Full GC。由于CMS收集器和用户线程并发运行,因此在收集过程中不断有新的垃圾产生,这些垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好等待下一次GC时再将其清理掉,这些垃圾就称为浮动垃圾。 CMS垃圾收集器不能像其他垃圾收集器那样等待年老代机会完全被填满之后再进行收集,需要预留一部分空间供并发清理时的使用,可以通过参数-XX:CMSInitiatingOccupancyFraction来设置年老代空间达到多少的百分比时触发CMS进行垃圾收集,默认是68%。 如果在CMS运行期间,预留的内存无法满足程序需要,就会出现一次ConcurrentMode Failure失败, 此时虚拟机将启动预备方案,使用Serial Old收集器重新进行年老代垃圾回收。(Serial Old为这个收集器的后备方案)。当使用Serial Old时停顿的时间就长了,延迟就会增高。 3. CMS收集器是基于标记-清除算法,因此不可避免会产生大量不连续的内存碎片,如果无法找到一块足够大的连续内存存放对象时,将会触发因此Full GC。CMS提供一个开关参数-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后进行内存整理,内存整理会使得垃圾收集停顿时间变长,CMS提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不压缩的Full GC之后,跟着再来一次内存整理。

4.7 G1 特点:  目前垃圾收集器理论发展的最前沿成果

1.  并行与并发:通过充分利用CPU资源,缩短停顿的时间,部分其他收集器原本需要停顿Java线程的GC动作,G1仍然可以通过并发的方式让Java程序继续执行。 2.  分代收集:分代的概念在G1中依然得到了保留。 3.  空间整合:G1从整体上看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“赋值算法实现的”。这两种算法在垃圾收集后都能提供规整的内存空间。 4.  可预测的停顿:G1建立了可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾回收上的时间不超过N毫秒。 算法: 整体是标记-整理算法, 局部是复制算法。 工作地点: 整个Java堆 工作原理: G1将整个Java堆划分为多个大小相等的独立区域(region),同时也保留了新生代和老年代的概念。这两个代都是一部分Region的集合。 这样G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。 区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。 G1收集器的大致可划分为以下几个步骤: 1. 初始标记:标记一下GC Roots能直接关联到的对象 2. 并发标记: 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。 3. 最终标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,并将变化记录在叫做Rememberd Set Logs里面。在这个阶段的最后将 Rememberd Set Logs中的数据合并到 Rememberd Set中( Rememberd Set会在下面进行解释 )。 4. 筛选回收:首先对各个Region的回收价值和成本进行排序,并根据用户所期望的GC停顿时间来制定回收计划。在筛选回收期间需要停顿所有的工作线程,这样提高垃圾回收的效率。

第3章 垃圾收集器与内存分配策略
G1收集器有一个很大的问题需要解决:G1将Java堆分为了多个Region,但是Region直接不可能是完全孤立的,比如某个Region中的对象引用着另外一个Region中的对象。这样的话如果需要回收某个Region中的垃圾是不是不要扫描整个堆空间?这显示是很不可取的。为了避免这个问题,G1中每个Region都有一个与之对应的Remembered Set, 虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便把相关的引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set便可不用再对全堆进行扫描了,因为所有其他Region引用该Region中对象的信息都记录在 Remembered Set中了。   优点:  a.基于标记-整理和复制算法,不产生内存碎片。 b.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收  

5. 内存分配与回收策略 先说明两个概念 1. Minor GC:指发生在新生代的垃圾回收动作,因为新生代的对象的生命周期很短,所以Minor GC非常频繁,回收速度也快。

2. Full GC:指发生在老年区的GC,发生了Full GC,通常会伴随着至少一次的MinorGC,Full GC通常要比Minor GC慢十倍以上。

5.1 对象优先在Eden上分配
在大对数情况下对象在新生代Eden中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
5.2 长期存活的对象将进入老年代
虚拟机为每一个对象定义了一个对象年龄计数器。如果对象在Eden出生并且经过一次Minor GC后仍然存活并且Survivor区可以容纳该对象(如果容不下直接进入老年代,空间分配担保),就将对象的年龄增加1。对象每熬过一次Minor GC,年龄就会增加一岁。当年龄增加到一定程度(默认为15,该值可调),就会进入到老年代中。
5.3 动态对象年龄判断
虚拟机并不是永远地要求对象必须达到设置的值才能进入到老年代,如果放在Survivor空间中相同的年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。
5.4 空间分配担保
空间分配担保是老年代对新生代Minor GC的空间分配担保.

在新生代的MinorGC发生时,有可能会有一部分的对象提前进入老年代(比如某些对象过大Survivor空间放不下),但是无法保证这个操作一定是安全的,因为老年代的空间的剩余大小位置,所以在发生MinorGC之前,虚拟机会先检查老年代的最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置是否允许担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,虚拟机将尝试MinorGC,如果小于或者 HandlePromotionFailure的设置值为不语许担保失败,就进行一次Full GC。

博客中的图片大部分来自网络, 侵通删!

继续阅读