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

1. 垃圾收集器(GC)要完成的3件事:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

2. 哪些内存需要回收

  • 程序计数器、虚拟机栈、本地方法栈这3个区域不需要回收,因为这3个区域是线程私有的,会随着线程而生,随线程而灭;
  • Java堆和方法区这2两个区域需要回收,因为只有在程序处于运行期时才能知道会创建哪些对象,这部分内存的分配和回收是动态的。

3.什么时候回收

  • 当判断对象已死的时候就可以回收对象占有的内存,如何判断对象已死,有如下两种方法:
  • (不推荐使用)引用计数算法:给对象中添加一个引用计数器,每当一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用,说明对象已死。这种方法很难解决对象之间相互循环引用的问题,就是两个对象互相引用,计数器都为1,但是已经不可能被引用了,造成不能回收这两个对象内存。
  • (推荐使用)可达性分析法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。
  1. 可作为GC Roots的对象的包括:(1)虚拟机栈(栈帧中的本地变量表)中引用的对象;(2)方法区中类静态属性引用的对象;(3)方法区中常量引用的对象;(4)本地方法栈JNI引用的对象。
  2. 引用,(狭义定义)如果reference类型的数据中存储的数值代表的是另外一块内存地址的起始地址,就称这块内存代表着一个引用。引用分类:(1)强引用:程序代码中普遍存在的,类似“Object obj = new Object()”,只要强引用还存在,GC就不会回收该对象;(2)软引用(SoftReference):一些还有用但并非必须的对象,在系统将要发生内存溢出异常时,将会把这些对象列入回收范围,之中进行第二次回收,如果内存还是不够,才会抛出内存溢出异常;(3)弱引用(WeakReference):非必须对象,强度比软引用还更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前;(4)虚引用(PhantomReference):为一个对象设置虚引用的唯一目的就是能在这个对象被GC回收是收到一个系统通知。
  • 对象自我拯救的机会:通过finalize()方法将自己与引用链上的任何一个对象建立关联;如果对象在可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(finalize()方法只能执行一次,即对象只能自我拯救一次),虚拟机将这两种情况视为“没有必要执行”。
  • 回收方法区中的对象:废弃常量和无用的类;
  1. 废弃常量回收:与回收Java堆中的对象相似,当前系统没有任何地方引用这个常量;
  2. 无用的类回收:满足3个条件:(1)该类所有的实例都被回收了;(2)加载该类的ClassLoader已经被回收;(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。

4.如何回收——垃圾收集算法

4.1 标记-清除算法(老年代)

  • 分为“标记”和“清除”阶段:首先标记出所有需要回收的对象(可达性分析法),在标记完成后统一回收所有被标记的对象;
  • 不足:(1)效率问题:标记和清除过程效率都不高;(2)空间问题:标记清除后会产生大量不连续的内存碎片,导致大对象无法找到足够大的连续内存,而触发GC。

4.2 复制算法(Minor GC,新生代)

  • 将内存划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活的对象复制到另一块内存上,然后把已使用过的内存空间一次清理掉。

  • 优点:每次只对其中一块进行GC,不用考虑内存碎片的问题,并且实现简单,运行高效;缺点:内存缩小了一半

  • 现在的商业虚拟机都是用这种收集算法回收新生代。内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior.当回收时,将Eden和Survior中还存活的对象一次性拷贝到另外一块Survior空间上,最后清理Eden和刚才用过的Survior空间。

4.3 标记-整理算法(老年代)

  • 标记过程与“标记-清除”算法一样,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4.4 分代收集算法

  • 根据存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,然后根据不同年代的特点选择最适合的算法,比如新生代中的对象朝生夕灭,只有少量存活,比较适合复制算法,而老年代中对象存活率高,没有额外空间提供分配担保,必须使用标记-清除或标记-整理算法进行回收。

5.HotSpot 收集算法实现

5.1 枚举根节点

  • 可达性分析出哪些对象是存活,哪些对象是已死亡这项操作必须在一个能保证一致性的快照中进行——这里的“一致性”指在整个分析期间整个执行系统看起来就像是冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。
  • 发生GC时,枚举根节点操作必须停顿所有Java执行线程(“Stop the World”),枚举根节点时是必须停顿的。
  • 虚拟机通过使用一组称为OopMap的数据结构,在类加载完成的时候,JVM就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是什么引用,这样GC在扫描时就可以直接得知哪些地方存放着对象引用。通过这种方式降低枚举根节点操作的停顿时间。

5.2 安全点

  • GC枚举根节点时,线程要停在安全点:如方法返回前、循环的末尾、抛出异常的位置。
  • 另一个问题是GC如何让线程在安全点上停顿下来,有两种方式可选,一种是抢先式中断,GC主动中断所有线程,发现有线程中断的地方不在安全点上,就恢复线程,让它继续跑到安全点上,一种是主动式中断,GC不主动中断线程,而是设置一个标志,线程执行时主动去轮询这个标志,发现标志为真就自动挂起,轮询标志和安全点是重合的。

5.3 安全区域

  • 安全区域是指一段代码范围,这个范围内任何位置引用关系都不会发生变化,也就是说在这些位置进行GC是安全的,可以把安全区域看成被扩展了的安全点。

6 堆内存中的分代机制

6.1新生代

  • 新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
  • 默认的,Edem : from : to = 8 : 1 : 1 
  • 年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。这么做主要是为了减少内存碎片的产生。

6.2 老年代

  • 年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

6.3 持久代(方法区)

  • 持久代主要存放类定义、字节码和常量等很少会变更的信息

7. 内存分配与回收策略

  •  对象的内存分配,绝大部分是堆上分配(也有一些事栈上分配,不用GC),对象主要分配在新生代中的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。
  • (1)对象优先在Eden分配。当Eden区没有足够空间进行分配时,将进行一次Minor GC
  • (2)大对象直接进入老年代,比如很大的字符串和数组。-XX:PretenureSizeThreshold
  • (3)长期存活的对象将进入老年代。虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden去中经过一次Minor GC后存活在Survivor中,计数器加1,当年龄增加到一定程度(默认为15岁),将会进入老年代中。
  • (4)动态对象年龄判定。如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需年龄等到15岁。
  • (5)空间分配担保机制。在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总和,条件成立,Minor GC是安全的,否则是不安全的了;如果条件不成立,虚拟机会检查HandlePromotionFailure设置值是否允许担保。若允许,将继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小;如果大于,将尝试进行一次Minor GC,尽管是有风险的;如果小于或是HandlePromotionFailure设置不允许冒险,这时将改为进行一次Full GC。

8. 垃圾收集器

8.1 Serial 收集器

  • 单线程的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。 
  • 由于Serial收集器的简单而高效,Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于运行在Client模式下的虚拟机来说是一个很好的选择。因为分给桌面应用的内存小,所以收集垃圾时停顿的时间不会很长,能够接受。
  • Serial Old收集器作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

8.2 ParNew 收集器 

  • Serial 收集器的多线程版本,即收集垃圾时使用多条线程
  • Server 模式下虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除Serial收集器之外,目前只有ParNew它能与CMS收集器配合工作。

8.3  Parallel Scavenge(并行回收)收集器

  • “吞吐量优先”收集器
  • Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器
  • 该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

  • -XX:+UseAdaptiveSizePolicy。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别

8.4 CMS (Concurrent Mark Sweep)收集器 (老年代收集器)

  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
  • CMS收集器是基于“标记-清除”算法实现的。
  • CMS收集器执行过程:(1)初始标记:标记一下GC Roots能直接关联的对象,速度很快;(2)并发标记:GC Roots Tracing过程,就是枚举所有对象,对堆中对象进行可达性分析,找出存活的对象,耗时很长;(3)重新标记:修正并发标记期间因为用户线程继续运作而导致标记变动的那一部分对象的标记记录,速度也很快;(4)并发清除。其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”,但是这两个过程很快,而并发标记和并发清除耗时很长,采用与用户线程并发执行的,减少停顿时间。
  • CMS收集器主要优点:并发收集,低停顿。
  • CMS三个明显的缺点:
  1. CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程是(CPU数量+3)/ 4,当CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大。 CPU越多越好。
  2. CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
  3. CMS是基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。

8.5 G1 收集器

  • G1收集器的优势:(1)并行与并发(2)分代收集(3)空间整理 (整体看是标记整理算法,从局部(两个region)看是复制算法)(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)

  • 使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。

  • G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在真个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的又来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽量可能高的收集效率。

  • 在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。

  • 如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:(1)初始标记(2)并发标记(3)最终标记(4)筛选回收:首选对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值