1 如何判断对象是否已经“死掉”?
1.1 引用计数算法(Reference Counting)
定义:给对象中添加一个引用计算器,每当有一个地方引用它时,计算器就加1;当引用失效时,计数器就减1;任何时候计数器为0的对象就是不可能被再使用的对象。
特点:简单,判断效率高。但是无法解决对象之间相互循环引用的问题(导致了内存泄漏)。
1.2 可达性分析算法(Reachability Analysis)
定义:通过一系列的称为“GC ROOTS”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC ROOTS没有任何引用链相连(即不可达对象)时,则证明此对象是不可用的。
特点:JVM默认使用来判断对象是否不可用的算法。
可作为GC ROOTS的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
2 引用分类
背景:当内存空间足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。(“食之无味,弃之可惜”)
引用:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),强度依次减弱。
2.1 强引用
类似“Object obj=new Object()”的引用,只要强引用还在,垃圾收集器就永远不会回收掉被引用的对象。
2.2 软引用
在系统将要发生内存溢出(OOM)异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会排出OOM异常。(SoftReference类)
2.3 弱引用
强度比软引用弱点,弱引用对象只能生存到下一次垃圾收集发生之前。当垃圾收集工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。(WeakReference类)
2.4 虚引用
最最最弱的引用,无法通过虚引用来取得一个对象实例,其唯一目的就是能在这个对象被收集器回收时能收到一个系统通知。(PhantomReference类)
3 垃圾收集的区域
垃圾收集主要发生在堆和方法区。在堆中(新生代和老年代),尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代(方法区)的垃圾收集效率远低于此。方法区(永久代)的垃圾收集主要是回收两部分:废弃常量和无用的类。
4 Minor GC与Full GC
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC或Full GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次的Minor GC(但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
5 垃圾收集算法
5.1 标记-清除算法(Mark-Sweep)
- 特点:一是效率不高,标记和清除两个过程的效率都不高;另一个是空间问题,清除后会产生大量不连续的内存碎片。
- 对象:老年代
5.2 复制算法(Copying)
- 1:1分配空间:内存空间浪费严重(50%)
- Eden:Survivor:Survivor=8:1:1:内存浪费只有10%,对Eden和其中一个Survivor区进行垃圾收集,最后将存活的对象复制到另一块Survivor区中,使用老年代作为内存担保。
- 特点:实现简单,运行高效,但是需要浪费部分内存空间。由于新生代中的对象98%是“朝生夕死”的(统计),因此使用Eden和Survivor的回收策略会更佳。但是98%并不是确定的,因此当回收后的对象多余10%存活时,即Survivor区并不足够存放,则此时将进行内存担保(使用老年代进行内存担保,即将此次回收后的对象直接进入老年代)。
- 对象:新生代
5.3 标记-整理算法(Mark-Compact)
- 特点:复制算法在对象存活率较高时就需要进行较多的复制操作,效率将会降低。因此老年代更适合采用标记整理算法,将存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 对象:老年代
5.4 分代收集算法(Generational Collection)
- 特点:新生代采用复制算法,老年代采用“标记-清除”或“标记-整理”算法
6 内存分配策略
6.1 对象优先在Eden区分配
对象优先在Eden区分配,当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC。
6.2 大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息(尤其是遇到朝生夕灭的“短命大对象”,写程序时应避免),经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来安置它们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法回收内存)。
6.3 长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
6.4 动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
6.5 空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
7 垃圾收集器
- 新生代:Serial、ParNew、Parallel Scavenge
- 老年代:Serial Old(MSC)、Parallel Old、CMS
7.1 Serial收集器
- 单线程完成垃圾收集工作
- Stop the World:在进行垃圾收集时,必须暂停其他所有用户线程,直到垃圾收集结束
- 虚拟机运行在Client模式下的默认新生代收集器
- 收集算法:复制算法(Copying)
- 简单高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程间交互的开销,专心做垃圾收集,因此获得较高的单线程垃圾收集效率
7.2 ParNew收集器
- Serial的多线程版本
- Stop the World:在进行垃圾收集时,必须暂停其他所有用户线程,直到垃圾收集结束
- 虚拟机运行在Server模式下的默认新生代收集器
- 收集算法:复制算法(Copying)
-
JVM参数:
XX:SurvivorRatio:Eden与Survivor区的比例 -XX:PretenureSizeThreshold:晋升老年代对象年龄 -XX:ParallelGCThread:限制垃圾收集器的线程数
7.3 Parallel Scavenge收集器
- 并行的多线程收集器
- 收集算法:复制算法(Copying)
- CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),即吞吐量优先的收集器
- 短停顿时间可以提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
- GC自适应调节策略(GC Ergonomics):虚拟机会根据当前系统的运行情况手机性能监控信息,动态调整新生代大小(-Xmm)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,以提供最合适的停顿时间或者最大的吞吐量。
-
JVM参数:
-XX:MaxGCPauseMillis:最大垃圾收集停顿时间 -XX:GCTimeRatio:设置吞吐量大小
7.4 Serial Old收集器
- 单线程收集器
- 虚拟机运行在Client模式下的默认垃圾收集器(老年代)
- 收集算法:“标记-整理”算法
7.5 Parallel Old收集
- Parallel Old收集器+Parallel Scavenge收集器的组合达到了名副其实的“吞吐量优先”的应用组合
- 在注重吞吐量及CPU资源敏感的场合,可以优先考虑Parallel Old收集器+Parallel Scavenge收集器的组合
- 收集算法:“标记-整理”算法
7.6 CMS收集器
- 以获得最短回收停顿时间作为目标的收集器(高用户体验)
- 收集算法:“标记-清除”算法
(1)CMS收集器运作过程
- 初始标记(CMS initial mark):只标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程
- 重新标记(CMS remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”
- 并发清除(CMS concurrent sweep) (2)CMS收集器的缺点
- CMS收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生
- 所采用的“标记-清除”算法会产生大量的空间内存碎片
7.7 G1收集器
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
(1)横跨整个堆内存
在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。
(2)建立可预测的时间模型
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
(3)避免全堆扫描——Remembered Set
G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。