1、对象已经死亡?
1.1引用计数法:给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用
的。但是它很难解决对象之间相互循环引用的问题。
1.2根搜索算法:主流的商用语言(Java和C#),都是使用根搜索算法判定对象是否存活的。这个算法的基本思路:通过一系列的名为“GC Roots Tracing”的对象作为起点,从
这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)
时,则证明此对象是不可用的。
在Java语言里,可作为GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中的引用对象
2.方法区中的类静态属性引用的对象
3.方法区中的常量引用的对象
4.本地方法栈中(即一般来说的Native方法)的引用的对象
1.3再谈引用
在JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据中存储的数值代表的是另一块儿内存的起始地址,就称这块儿内存代表着一个引用。但是定义太过狭隘,
无法描述这种对象:当内存空间还足够时,则能保留在内存之中;如果内存进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用四种(引用强度主键递减)
强引用:普遍存在,类似于 Object obj = new Object() 这类引用,只要引用还存在,垃圾收集器永远不会回收掉被引用的对象
软引用:描述有用但非必须的对象,内存空间足够,垃圾收集器就不会回收它,如果内存不足则会回收。只要没被回收就可以使用,可以从来实现内存敏感的高速缓存
弱引用:描述可有可无对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。由于垃圾收集器是优先级很低的线程,并不一定能即时发现弱引用并回收。弱引用
和软引用的区别在于,只具有弱引用的对象具有更短暂的生命周期
虚引用:形同虚设,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。设置虚引用的唯一目的是希望能在这个
对象被收集器回收时收到一个系统通知。
1.4不可达对象并非“非死不可”
即使在可达性分析算法中不可达的对象,也并非“非死不可”的,这时它们暂时处于“缓刑阶段”,真正宣告一个对象死亡,至少要经历两次标记过程。可达性算法中不可达对象
第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize(当对象没有覆盖finalize方法,或finalize已经被虚拟机调用过,虚拟机将这两种情况视为没有必要
执行),被判定为需要执行的对象会被放一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
1.5废弃常量无用类的判断
废弃常量判断比较简单,假如常量池中存在的字符串 "abc",如果当前没有任何Spring类型的对象引用该字符串常量的话,就说明 "abc" 是废弃常量,垃圾收集器会回收它。
无用类要同时满足三个条件:1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2.加载该类的ClassLoader已经被回收。3.该类对应的 java.long.Class
对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
2、垃圾收集算法
2.1标记-清除算法
标记清除算法是最基础的收集算法,首先标记出所有需要回收的对象,在标记完成后统一回收掉所有标记的对象。但是有两个问题:一是效率问题,标记和清除的效率都不高。
二是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时,无法找到足够的连续内存,而不得不
提前触发另一次垃圾收集动作。标记-清除算法的执行过程如下图(图片来源:深入理解Java虚拟机)
2.2复制算法
为解决效率问题而出现的复制算法,它将可用内存按容量划分为大小相等的两块儿,每次只使用其中的一块儿。当这一块儿内存用完了,就将还存活的对象复制到另一块儿上
面,然后再把已使用过的内存空间一次清理掉。这样每次的内存回收都是对内存区间的一半进行回收。复制算法执行过程如下(图片来源:深入理解Java虚拟机)
2.3标记-整理算法
根据老年代的特点推出的标记-整理算法,标记过程仍与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理
掉端边界以外的内存。标记-整理算法示意图如下(图片来源:深入理解Java虚拟机)
2.4分代收集算法
当前虚拟机都采用分代收集算法,这种算法并没有什么新的思想,只是根据对象的存活周期不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据
各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现大量对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以
完成收集。而老年代中,因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来进行回收。
3、垃圾收集器
3.1Serial收集器
单线程垃圾收集器,它在进行垃圾收集工作的时候,必须暂停其他所有的工作线程,直到它收集结束。Serial收集器在新生代采用复制算法,在老年代采用标记-整理算法。劣势
是暂停其他线程,优势简单高效(与其他收集器的单线程相比)。serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率,它对于运行在Client模式下的
虚拟机来说是个不错的选择。运行示意图如下(图片来源:深入理解Java虚拟机)
3.2ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行收集垃圾收集之外,其余行为(控制参数、收集算法、回收策略等)和Serial收集器完全一样。ParNew
收集器在新生代采用复制算法,在老年代采用标记-整理算法。它是许多运行在server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS(真正意义上的并发收
集器)收集器配合工作。
垃圾收集并发和并行概念补充:
并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序继续运行,而垃圾收集程序运行在另一个cpu上。
运行示意图如下(图片来源:深入理解Java虚拟机)
3.3Parallel Scavenge收集器
Parallel Scavenge收集器与ParNew收集器类似,也是并行多线程垃圾收集器,新生代复制算法,老年代标记-整理算法。与其他收集器不同的是,它关注的目标是达到一个可控
制的吞吐量,而ParNew收集器关注与垃圾收集时用户线程停顿的时间。所谓的吞吐量就是cpu运行用户代码的时间与cpu总消耗时间的比值
即 吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。
停顿时间短,适用于和用户有交互的程序,吞吐量高则可以高效的利用cpu,尽快的完成运算任务,适用于后台运算不需要太多交互任务。
3.4CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,是基于标记-清除算法来实现的,它的运作过程分为四部:
1.初始标记:仅仅只是标记一下GC Root能直接关联到的对象,速度很快。
2.并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象,但在这个阶段结束,这个闭包结构并不能保证包含当前所有可达对象,因为用户线程可能会
不断更新引用域,所以GC线程无法可达性分析的实时性,所以这个阶段会跟踪记录引用发生更新的地方。
3.重新标记:重新标记阶段就是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记
稍长一些,但远比并发标记时间短。
4.并发清除:开启用户线程,同时GC线程开始对标记区域做清理。
运行示意图如下(图片来源:深入理解Java虚拟机)
CMS收集器优点:并发收集,低停顿
缺点:对cpu资源敏感,无法处理浮动垃圾,基于标记-清除算法收集,结束时会产生大量空间碎片
3.5G1收集器
G1收集器是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大内存容量的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
G1收集器的特点:
1、并发与并行:G1能充分利用cpu多核环境下的硬件优势,使用多个cpu来缩短暂停用户线程(stpo-the-word)的停顿时间。其他收集器原本需要停顿Java线程来执行GC
动作,G1收集器仍可以通过并发的方式让Java程序继续执行。
2、分代收集:虽然G1收集器不需要其他收集器配合独立管理GC堆,但是还是保留的分代的概念。
3、空间整合:G1收集器从整体上来看是基于标记-整理算法来实现的,从局部上来看是基于复制算法来实现的
4、可预测的停顿:降低停顿时间是CMS和G1收集器共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒
时间片段内,消耗垃圾收集的时间不超过n毫秒
G1收集器的运作大致分为 初始标记、并发标记、最终标记、筛选回收等几个步骤,G1收集器把整个Java堆划分成多个大小固定的独立区域,并且跟踪这些区域里的垃圾堆积
程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。区域的划分以及有优先级的回收,保证了G1收集器在有限的时间内可以获得最高的
收集效率
4、内存分配与回收策略
4.1对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
MinorGC和FullGC有什么不同呢:
新生代GC(Minor GC)是指发生在新生代的垃圾收集动作,Minor GC非常频发,回收速度一般也比较快
老年代GC(Major GC/Full GC)是指发生在老年代的垃圾收集动作,出现Full GC至少伴随一次Minor GC(并非绝对),Full GC的速度一般会比Minor GC慢十倍以上
4.2大对象直接进入老年代
所谓大对象是指,需要大量连续内存空间的Java对象(例如很长字符串或数组)。分配在老年代的目的是避免在Eden区和Survivor区之间发生大量的内存拷贝(新生代采用
复制算法收集内存)。
4.3长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden区出生并经历过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将
对象的年龄设为1,对象在Survivor区每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。
4.4动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
4.5空间分配担保
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的对象的平均大小是否大于老年代的剩余空间,如果大于,则改为直接进行一次Full GC,则查看是否允许担保失败,
如果允许,则只进行Minor GC,如果不允许,则也要改为进行一次FullGC。