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

3.1 概述
LISP是第一门使用内存动态分配和垃圾收集技术的语言。
CG需要完成的三件事:
1、哪些内存需要回收?
2、什么时候回收?
3、如何回收?
JAVA堆和方法区中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样。我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,CG关注的是这部分内存。

3.2 对象已死吗
CG在进行垃圾回收时,最先要确定的,就是这些对象之中,哪些对象死了(即不能被任何途径使用的对象),哪些还活着。

3.2.1 引用计数算法
很多教科书的办法,是给对象添加一个计数器,每当有个地方引用它,就加一,当引用失效,就减一。任何时刻计数器为0的对象就不可能被使用。客观地说,这个方法实现简单,判定效率高。但是,主流的Java虚拟机里面没有选择引用计数算法来管理内存。最主要的原因是,它很难解决对象之间相互循环引用的问题。

3.2.2 可达性分析算法
在主流的商用程序语言中(Java ,C#,甚至是Lisp),都是通过可达性分析(Reachability Analysis)来判断对象是否存活。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”(Reference Chain)。当一个对象到GC Root没有任何引用链相连(即从GC Root 到该对象不可达)则证明这个对象是不可用的。
在Java语言中,可作为GC Root 的对象包括以下几种。
1—》虚拟机栈(栈帧中的本地变量表)中引用的对象。
2—》方法区中类静态属性引用的对象。
3—》方法区中常量引用的对象。
4—》本地方法栈中JNI(即一般所说的Native方法)引用的对象。

3.2.3 再谈引用
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference ),虚引用(Phantom Reference) 4种。这四种强度逐渐减弱。
强引用—》在代码中普遍存在的,类似“Object obj = new Object() ”这类的引用。只要强引用还
在,垃圾收集器永远不会回收掉被引用的对象。
软引用—》描述一些还有用,但是并非必需的对象。对于软引用关联着的对象,在系统将要发生
内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收
还没有足够的内存,才会抛出内存溢出异常。提供了SoftReference类来实现软引用。
弱引用—》也是用来描述非必需对象的。但是强度低于软引用,被弱引用关联的对象只能生存到
下次垃圾收集发生之前。当垃圾收集器工作室,无论当前内存是否足够,都会回收掉
只被弱引用关联的对象。提供了WeakReference类来实现弱引用。
虚引用—》也称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,不会对其生存时间构
成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一
目的是在这个对象被收集器回收时收到一个系统通知。提供了PhantomReference类
来实现虚引用。虚引用必须和引用队列一起使用。

3.2.4 生存还是死亡
即使在可达性分析算法中不可达的对象,也不是非死不可。他们暂时处于“缓刑阶段”,要真正宣告一个 对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,将会被第一次标记,并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判断 有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后有一个由虚拟机自己建立的,低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待他运行结束,这样做的原因是,如果一个对象在finalize方法中执行缓慢,或者发生了死循环,将可能会导致F-Queue队列中其他对象永久处于等待状态,导致整个内存回收系统崩溃。Finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对想要在Finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。注意!!任何一个对象的Finalize()方法都只会被系统自动调用一次。

3.2.5 回收方法区
很多人认为方法区(永久代)是没有垃圾收集的。Java虚拟机规范中的确说过可以不要求虚拟机在方法区实现垃圾收集,但是永久代中的确有可回收的内容。
内容主要有两部分:
1、废弃常量
判定标准:假如一个字符串“abc”已经进入了常量池,但是当前系统没有任何一个String
对象引用常量池中的“abc”常量。如果这时发生内存回收,而且必要的话,这个“abc”常
量就会被系统清理出常量池。其他类,接口,方法,字段的符号引用也与此类似。
2、无用的类
同时满足以下三个条件:
(1)该类的所有实例都已经被回收。
(2)加载该类的ClassLoader已经被回收。
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射
访问该类的方法。
虚拟机可以对满足上述条件的无用类进行回收,但是并不是和对象一样,不使用了就必
须回收。是否回收由虚拟机决定。

3.3 垃圾收集算法

3.3.1 标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为标记和清除两个阶段。
首先标记所有需要的对象,在标记完成后统一回收所有被标记的对象。
不足:1-效率问题,,标记和清除两个过程的效率都不太高。
2-空间问题,,标记清除之后会产生大量不连续的内存碎片,可能导致以后
分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾
收集动作。

3.3.2 复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了。它将内存分为两块,每次只用一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,再把已使用过的内存空间一次清理掉。在实际操作中,不需要按照1:1分配,只需要分配一块较大的Eden空间和两块较小的Survivor空间,每次用Eden和一块Survivor空间就行。回收时,将Eden和Survivor中存货的对象复制到另一块Survivor空间中,在清理掉Eden和刚才用过的Survivor空间。大小比例是8:1:1.

3.3.3 标记-整理算法
针对老年代的一个适应性改进。标记过程和“标记-清除”算法类似,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理掉边界以外的内存。

3.3.4 分代收集算法
当代商业虚拟机都使用的分代收集算法。根据对象存活周期的不同将内存划分为几块,一般把JAVA堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。新生代一般使用复制算法,老年代一般使用标记整理或者标记清理算法。

3.4 HotSpot的算法实现
3.4.1 枚举根节点
可达性分析中,能够作为GC Roots的节点主要在全局性引用,与执行上下文中。现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
另外,可达性分析对执行时间的敏感还体现在GC停顿上。因为这项分析工作必须在一个能确保一致性的快照中进行,所以导致GC进行时必须停顿所有JAVA执行线程。即使是在号称几乎不会发生停顿的CMS收集器中,枚举根节点也是必须要停顿的。
由于目前主流Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机有办法直接得知哪些地方存放着对象引用。在HotSpot中使用了一组叫做OopMap的数据结构来达到这个目的的。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样,GC在扫描时就可以直接得知这些消息了。

3.4.2 安全点
在OopMap下,HotSpot可以快速准确的完成GC Roots的枚举,但是OopMap内容变化的指令非常多,如果为每条指令都生成对于的OopMap,将会需要大量的额外空间。
所以,只有在安全点(Safepoint)才记录了OopMap,即程序执行时并非在所有地方都能停顿下来开始GC,只有到达安全点时才能暂停。
对于安全点,另一个问题是如何在GC发生时让所有线程(不包括进行JNI调用的线程)都跑到最近的安全点上再停顿下来。这时有两种方案供选择:
1–》抢先式中断(Preemptive Suspension) 不需要线程的执行代码主动去配合,在
GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全
点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机用此方法。
2–》主动式中断(Voluntary Suspension) 当GC需要中断线程时,不直接对线程进行
操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现
中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的。
安全点的隐患:Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint,但是,程序“不执行”的时候呢?所谓程序不执行就是没有分配CPU时间,例如:线程处于Sleep或者Blocked状态,这时线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也不可能等待线程重新被分配CPU时间。此时,就需要安全区域来解决。

3.4.3 安全区域
安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方开始GC都是安全的。我们可以把它当成扩展的安全点。

3.5 垃圾收集器
垃圾收集器是内存回收的具体体现。虚拟机内部会有各种不同的垃圾收集器来作用于不同分代的垃圾。他们也可以搭配使用。至今为止没有一个万能收集器,只能选择对具体应用场景最适合的收集器。

3.5.1 Serial 收集器
Serial收集器是最基本,发展历史最悠久的收集器。曾经是新生代收集的唯一选择。这是一个单线程的收集器,它的单线程不仅仅是说明他只使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:简单而高效
适用场景:运行在Client模式下的虚拟机;收集几十兆或者一两百兆的新生代,可以控制在几十毫秒到一百多毫秒以内,可接受。

3.5.2 ParNew收集器
它是Serial收集器的多线程版本。使用复制算法。
优点:能和CMS收集器配合工作。

3.5.3 Parallel Scavenge 收集器
使用复制算法,并行的多线程收集器。
特点:目的是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。
适合场景:后台运算而不需要太多交互的任务。
有自适应的调节策略,能够提供最合适的停顿时间或者最大的吞吐量。

3.5.4 Serial Old收集器
是Serial收集器的老年代版本。使用“标志-整理”算法。
适用场景:运行在Client模式下的虚拟机。如果是在Server模式下,可作为CMS收集器的后备预案。

3.5.5 Parallel Old收集器
是Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。

3.5.6 CMS收集器(Concurrent Mark Sweep)
是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿的时间段,以带给用户较好的体验。
从名字上看,他是基于“标记-清除”算法实现的。过程分为4个步骤
1-初始标记(CMS initial mark)
2-并发标记(CMS concurrent mark)
3-重新标记(CMS remark)
4-并发清除(CMS concurrent sweep)
其中,初始标记和重新标记这两个步骤仍然需要“stop the world”。
初始标记–标记GC Roots能直接关联到的对象,速度快。
并发标记–进行GC Roots Tracing 的过程。
重新标记–修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发的时间短。 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以CMS收集器的内存回收过程与用户线程一起并发执行。
CMS的缺点:
1–》CMS收集器对CPU资源非常敏感。为了应对这种情况,虚拟机提供了一种,“增量式并发收集器”的CMS收集器变种,所做的事情和单CPU时代抢占式模拟多任务机制的思想一样,就是在并发标记,清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占时间。但是事实证明,这个效果很一般,所以已经不提倡。
2–》CMS收集器无法处理浮动垃圾。在处理过程中,并发的用户线程产生的垃圾在当次处理中无法处理,必须要在下次才能被处理。
3–》由于这是基于“标记-清除”算法实现的收集器,所以如果没有合理安排空间,当大对象进入java堆,由于无法安排内存,所以不得不提前进行一次FULL GC。

3.5.7 G1收集器
G1收集器是一款面向服务端应用的垃圾收集器。具有以下特点:
1–》并行与并发:G1能充分利用多CPU,多核环境的优势,缩短stop the world 的停顿时间。
2–》分代收集:与其他收集器一样,分代概念也在G1中得到保留
3–》空间整合:与CMS 的“标记-清理”不同,G1从整体看是基于“标记-整理”的,但是从局部(两个Region之间)看是基于复制算法实现的。所以G1运作期间不会产生内存空间碎片。收集后能提供规整的可用内存。
4–》可预测的停顿:这是G1相对于CMS的一大优势。G1除了追求低停顿之外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段里,消耗在垃圾收集上的时间不超过N秒。这几乎已经是实时JAVA(RTSJ)的垃圾收集器的特征了。
使用G1收集器时,JAVA堆的内存布局就与其他收集器有很大的区别,它将整个JAVA堆划分为多个大小相等的独立区域(region),虽然还保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。
在G1虚拟机里,region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用remembered SET 来避免全堆扫描的。G1中每个Region都有一个与之对应的remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的region之中,如果是,就通过CardTable吧相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set 的操作,G1收集器的运作大致可划分为以下几个步骤:
1-初始标记
2-并发标记
3-最终标记
4-筛选回收

3.5.8 理解GC日志
最开始的数字:GC发生时间,从虚拟机启动以来经过的秒数。
GC日志开头的“【GC”和“【FULL GC”说明了这次垃圾收集的停顿类型。如果有FULL,就说明这次停顿是stop the world。
接下来的“DefNew”、“tenured”、“Perm”表示GC发生的区域
后面方括号里面的“3324K->152K(3721K)”表示:“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。方括号之外的“3324K->152K(11904K)”表示 GC之前JAVA堆已使用容量->GC后JAVA堆已使用容量(JAVA堆总容量)
再往后,“0.0025295 secs”表示该内存区域GC所占用的时间,单位是秒。

3.6 内存分配与回收策略
对象的内存分配,往大方向讲,就是在堆上分配,对象主要在新生代的Eden区。如果启动了本地线程分配缓冲,将按照线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中,其细节取决于当前使用的是哪一种垃圾收集器组合。还有虚拟机中与内存相关的参数设置。

3.6.1 对象优先在Eden分配
大多数情况下,多想在新生代Eden区分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。
Minor GC=新生代GC指的是发生在新生代的垃圾收集动作,因为JAVA对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也快。
Major GC/Full GC=老年代GC。一般来说,发生一次Major GC 会伴随着一次Minor GC。Major GC比MInor GC慢10倍以上。

3.6.2大对象直接进入老年代
所谓的大对象,就是指需要大量连续内存空间的对象,例如很长的字符串,数组等。虚拟机有一个设定,让大于这个设定值的大对象直接进入老年代。

3.6.3 长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄(AGE)计数器。如果对象在Eden出生,并且经过第一次Minor GC后仍然存活,将被放置到Survivor空间,并且年龄+1。它在Survivor中每熬过一次Minor GC,年龄就增加一岁,当它的年龄大于一定值(一般默认15),就会被晋升到老年代。

3.6.4 动态对象年龄判断
如果在Survivor中相同年龄所有对象大小大于Survivor空间的一半,年龄大于或等于该年龄的对象就直接进入老年代。

3.6.5 空间分配担保
在发生GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将会尝试一次Minor GC ,,尽管这次GC是有风险的。如果小于,或者HandlePromotionFailure设置不允许冒险,那么改为进行一次FULL GC。
如何触发minor GC和full GC?

Minor GC ,Full GC 触发条件:
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值