1.作用
自动检测对象是否超过作用域而达到自动回收内存的目的,垃圾处理器会对无用的数据进行自动管理回收。
2.引用
2.1 定义
如果reference类型的数据中存储的数值代表的是领一块内存的起始地址,就称这块内存代表一个引用。
2.2 引用分类
强引用(StrongReference)
声明对象是虚拟机生成的引用,如果一个对象具有强引用,那垃圾回收器绝不会回收它。----有引用就不会被回收
软引用(SoftReference)
一般被作为缓存来使用,若果一个对象只具有软引用,如果内存空间足够,就不会被回收。 ----有引用且内存足够就不回收
弱引用(WeakReference)
作为缓存来使用,在垃圾回收线程扫描它所管理的内存区域过程中,一旦发现了只具有弱引用的对象,就直接回收。 ----扫描到就回收
虚引用(Phantom Reachable)
它存在的唯一作用就是当它指向的对象被回收后,虚引用本身也会加入到引用队列中,用作记录它指向的对象已经被回收。 ----标记对象被回收
3.方法
3.1引用计数法
3.1.1 原理
给对象中添加一个计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器值就减1,当计数器为0时,该对象就不能再被使用。
3.1.2 优缺点
实现简单,判断效率高,但是不能解决对象之间的循环引用问题。
循环引用指两个对象相互强引用了对方,从而导致两个对象都无法被释放,引发了内存泄漏现象。
3.2 可达性分析
3.2.1 原理
又称根可达算法,通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,就证明该对象不可用。
3.2.2 GC Roots
1.虚拟机栈中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈JNI(被地方法)引用的对对象
3.2.3 死亡判定
如果对象在进行可达性算法分析后发现没有与GC Roots相链的引用链,那他将会第一次标记并且进行一次筛选,筛选的条件是,此对象是否有必要执行finaliza方法。
当对象没有覆盖finalize方法,或者finalize方法已经执行过了。虚拟机将这两种情况视为“没有必要执行”,将对象判定为死亡进行回收。
4.算法
4.1 标记-清除
4.1.1 过程
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
4.1.2 特点
效率低,空间碎片化严重,可能会导致提前触发垃圾回收。
4.2 标记整理
4.2.1 过程
首先标记出所有需要回收的对象,标记完成让所有存活的对象都向一端移动,完成后统一回收所有被标记的对象。
4.2.2 特点
时间开销变大。适合存活对象少,垃圾多的情况,老年代中会选用这种方法。
4.3 复制算法
4.3.1 过程
将内存分为两个部分,每次只使用一块,当这一块用完了,就将这这块还存活的对象复制到另外一块中,然后集中清理这一块。这样使得每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片问题。只要移动堆顶指针,按顺序分配内存即可,
4.3.2 特点
效率高,以空间换时间的做法,无碎片
4.4 分代收集算法
4.4.1 过程
根据对象存活周期的不同将内存分为几块。一般把java堆分为新生代和年老代。这样就可以根据各个年代的特点采用最合适的垃圾清理算法。
4.4.2 分代
- 新生代:分为Eden区和Survivor区,Eden区存储新创建的对象,Survivor区存储存活的对象,Survivor区有两个。(例如:方法的局部变量引用的对象等)
- 老年代:用来存储survivor满后触发fullGC后任然存活的对象。(例如:缓存对象、单例对象等)
- 永久代(方法区,1.8 改为元空间):用于存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据
- 新生代和老年代都在java堆,永久代在方法区,堆大小=新生代+老年代,新生代与老年代的比例为1:2,新生代细分为一块较大的Eden空间和两块较小的Survivor空间,分别被命名为from和to。
4.4.3 回收机制
堆中回收
新生代(young GC):采用复制算法,新生代对象一般存活率较低,因此可以不使用50%的内存作为空闲,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放。
**老年代(Major GC)**中使用“标记-清除”或者“标记-整理”算法进行垃圾回收,回收次数相对较少,每次回收时间比较长。
Full GC:是对于整个堆来进行回收。
方法区对象回收
永久代指的是虚拟机内存中的方法区,永久代垃圾回收比较少,效率也比较低,但也必须进行垃圾回收,否则永久代内存不够用时仍然会抛出OutOfMemoryError异常。永久代也使用“标记-清除”或者“标记-整理”算法进行垃圾回收。
回收的对象:
无用的常量:没有对象引用的常量
无用的类:该类的所有实例都已经被回收,该类的加载器已经被回收,.class对象没有在任何地方被引用,无法通过反射获得该类的方法。
5.收集器
5.1 Serial 收集器
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。针对新生代;采用复制算法;单线程收集;进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";HotSpot在Client模式下默认的新生代收集器;
HotSpot在Client模式下默认的新生代收集器,串行,单线程,复制算法,STW下执行。
5.2 ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作
5.3 Parallel 收集器
arallel Scavenge 收集器类似 ParNew 收集器,Parallel 收集器更关注系统的吞吐量。
吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值;即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;
5.4 Serial Old
Serial Old是 Serial收集器的老年代版本,针对老年代; 采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact); 单线程收集;主要用于Client模式;
5.5 Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法
5.6 CMS 收集器
是一种以获取最短回收停顿时间为目标的收集器。
- 特点:
针对老年代;
基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
以获取最短回收停顿时间为目标;
并发收集、低停顿;
需要更多的内存 - 运行过程
1.初始标记
仅标记一下GC Roots能直接关联到的对象;速度很快;但需要"Stop The World";
2.并发标记
进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象;应用程序也 在运行; 并不能保证可以标记出所有的存活对象;
3.重新标记
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率;
4.并发清除
回收所有的垃圾对象
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作 - 缺点
对CPU资源非常敏感,无法处理浮动垃圾,可能会出现"Concurrent Mode Failure"失败,会产生大量的内存碎片
并发标记时对漏标的处理
写屏障 + 增量更新
5.7 G1收集器
5.7.1 介绍
G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
G1也是有Eden区和Survivor区的概念的,只不过它们在内存上不是连续的,而是由一小份一小份组成的。这一小份区域的大小是固定的,名字叫做小堆区(Region)。小堆区可以是Eden也可以是Survivor区,还可以是Old区,所以G1的年轻代和老年代的概念都是逻辑上的。每一块Region,大小都是一致的,它的数值在1M-32M字节之间的一个2的幂值数。Humongous Region,大小超过Region 50%的对象,将会在这里分配。Region的大小,可以通过参数进行设置。
5.7.2 目的
解决CMS出现"Concurrent Mode Failure导致的fullGC
Concurrent Mode Failure:执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的
5.7.3 原理
G1把堆切成了很多份,把每一份当作一个小目标,每一份收集时间自然是很好控制的。
5.7.4 回收过程
(1)年轻代回收
是一个STW的过程,它的跨代引用使用RSet数据结构来追溯,会一次性回收掉年轻代的所有Region;发生时机就是Eden区满的时候。
1.扫描根
可以看做是GC Roots,加上RSet记录的其他Region的外部引用。
2.更新RS
处理dirty card queue中的卡页,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用,可以看作是一次补充
3 处理RS
识别被老年代对象指向的Eden中的对象,这些被指定的Eden中的对象被认为是存活的对象
4.复制对象
收集算法依然用的是Copy算法。在这个阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的Region。这个过程和其它垃圾回收算法一样,包括对象的年龄和晋升,无需做过多介绍。
5.处理引用
处理Soft、weak、Phantom、Final、JNI Weak等引用。结束回收
(2)并发标记
老年代的垃圾回收,它是一个并发标记的过程,顺便清理一点点对象。当整个堆内存使用达到一定比例(默认是45%),并发标记阶段就会被启动
1.初始标记(Initial Mark)
这个过程共用了Minor GC的暂停,这是因为它们可以复用root scan操作。虽然是STW的,但是过程很短
2.Root区扫描 (Root Region Scan)
G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成。
3.并发标记(Concurrent Mark)
这个阶段从GC Roots 开始对heap中的对象标记,标记线程与应用线程并行执行,并且收集各个Region的存活对象信息。
4.重新标记 (Remarking)
和CMS类似,也是STW的。标记那些在并发标记发生变化的对象。
5.清理阶段 (Cleanup)
不需要STW。如果发现Region里全是垃圾,这个阶段立马被清理掉。不全是垃圾的Region,并不会被立马处理,它会在Mixed GC阶段,进行收集。
混合回收
真正的清理,它不只清理年轻代,还会将老年代的一部分区域进行清理。
触发条件
通过Concurrent Mariking 阶段,我们已经统计了老年代的垃圾占比。在Minor GC之后,如果判断这个占比达到某一个阀值,下次就会触发Mixed GC。
并发标记时对漏标的处理
写屏障 + SATB(原始快照)
5.8 ZGC收集器
在ZGC中,连逻辑上的年轻代和老年代也去掉了,只分为一块一块的page,每次进行GC时,都会对page进行压缩操作,所以没有碎片问题。ZGC还能感知NUMA架构,提高内存的访问速度。与传统的回收算法相比,ZGC直接在对象的引用指针上做文章,用来表示对象的状态,所以只能再64位机器上。
1、停顿时间不会超过10ms;
2、停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在10ms以下);
3、可支持几百M,甚至几个T的堆大小(最大支持4T)
6.方法区回收
java虚拟机没有对方法区做垃圾回收规定,永久代的垃圾收集效率非常低。
永久代的垃圾收集主要回收两部分:废弃常量和无用的类。
- 无用类
该类所有的实例都已经被回收,java堆中不存在该来的任何实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方呗引用,无法在任何地方通过反射访问到该类。
6. 补充
6.1 concurrent mode failure
CMS垃圾收集器特有的错误,CMS的垃圾清理和引用线程是并行进行的,如果在并行清理的过程中老年代的空间不足以容纳应用产生的垃圾(也就是老年代正在清理,从年轻代晋升了新的对象,或者直接分配大对象年轻代放不下导致直接在老年代生成,这时候老年代也放不下),则会抛出“concurrent mode failure”。
6.2 RSet
记录了region中的card被谁引用了
RSet是一个空间换时间的数据结构,用于记录和维护Region之间的对象引用关系;记录了其他Region中的对象引用本Region中对象的关系属于Points-into(谁引用了我的对象)
RSet其实是一个HashTable,Key是Region的起始地址,Value是Card Table (字节数组),字节数组下标表示Card的空间地址
有了这个数据结构,在回收某个Region的时候,就不必对整个堆内存的对象进行扫描了。它使得部分回收变成可行。
事实上,为了维护RSet,程序运行的过程中,写入某个字段就会产生一个post-write barrier。为了减少这个开销,将内容放入RSet的过程是异步的,而且经过了很多的优化:Write Barrier 把脏卡信息存放到本地缓冲区(local buffer),有专门的的GC线程负责回收,并将相关信息传给Region的RSet。
6.3 CSet
保存一次GC中将执行垃圾回收的区间(Region)。GC过程中在CSet中的所有存活数据(Live Data)都会被转移。
6.4 card table
JVM将Region内存划分成了固定大小的Card。这里可以类比物理内存上page的概念。当该地址空间被引用的时候会被标记为dirty_card。
6.4 并发标记方案
6.4.1 原始快照(SATB,Snapshot At The Beginning)
原始快照(用在G1)是站在减少引用的对象的角度来解决问题。所谓原始快照,简单的讲,就是在赋值操作(这里是置空)执行之前添加一个写屏障,在写屏障中记录被置空的对象引用。比如,用户线程要执行:B.f=null;那么在写屏障中,首先会把B.f记录下来,然后再进行置空操作。记录下来的这个对象就可以称为原始快照。
那么记录下来之后呢?很简单,之后直接把它变为黑色。意思就是默认认为它不是垃圾,不需要将其清理。当然,这样处理有两种情况,一种情况是,F的确不是垃圾,直到清理的那一刻,都仍然有至少一个引用链能访问到它,这没有什么问题;另一种情况就是F又变成了垃圾。在上述的例子中,就是A到F的引用链也断了,或者直接A都成垃圾了,那F对象就成了浮动垃圾。对于浮动垃圾,前面不止一次就提到了,直接不用理会,如果到下一次GC时它仍然是垃圾,自然会被清理掉。
6.4.2 增量更新(Incremental Update)
增量更新(用在 CMS 中)是站在新增引用的对象的角度来解决问题。所谓增量更新,就是在赋值操作之前添加一个写屏障,在写屏障中记录新增的引用。比如,用户线程要执行:A.f = F;那么在写屏障中将新增的这个引用关系记录下来。标准的描述就是,当黑色对象新增一个白色对象的引用时,就通过写屏障将这个引用关系记录下来。然后在重新标记阶段,再以这些引用关系中的黑色对象为根,再扫描一次,以此保证不会漏标。
就是再进行一次分析。
6.5 读屏障和写屏障
这里的屏障和并发编程中的屏障是两码事儿。这里的屏障很简单,可以理解成就是在读写操作前后插入一段代码,用于记录一些信息、保存某些数据等,概念类似于AOP。
6.6 Mixed GC
回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时进行控制。也要注意的是Mixed GC并不是Full GC。
6.7 三色标记算法
6.7.1 定义
对象会根据是否被访问过(也就是是否在可达性分析过程中被检查过)被分为三个颜色:白色、灰色和黑色:
白色:这个对象还没有被访问过,在初始阶段,所有对象都是白色,所有都枚举完仍是白色的对象将会被当做垃圾对象被清理。
灰色:这个对象已经被访问过,但是这个对象所直接引用的对象中,至少还有一个没有被访问到,表示这个对象正在枚举中。
黑色:对象和它所直接引用的所有对象都被访问过。这里只要访问过就行,比如A只引用了B,B引用了C、D,那么只要A和B都被访问过,A就是黑色,即使B所引用的C或D还没有被访问到,此时B就是灰色。
6.7.2 流程
1.首先我们从GC Roots开始枚举,它们所有的直接引用变为灰色,自己变为黑色。可以想象有一个队列用于存储灰色对象,会把这些灰色对象放到这个队列中
2.然后从队列中取出一个灰色对象进行分析:将这个对象所有的直接引用变为灰色,放入队列中,然后这个对象变为黑色;如果取出的这个灰色对象没有直接引用,那么直接变成黑色
3.继续从队列中取出一个灰色对象进行分析,分析步骤和第二步相同,一直重复直到灰色队列为空
4.分析完成后仍然是白色的对象就是不可达的对象,可以作为垃圾被清理
5.最后重置标记状态
6.7.3 带来的问题
如果整个标记过程是STW的,那么没有任何问题,但是并发标记的过程中,用户线程也在运行,那么对象引用关系就可能发生改变,进而导致两个问题出现。
垃圾变为了非垃圾
A引用了B,AB都标记为了黑色,然后A断开了与B的连接,B成了垃圾,但是由于B是黑色,所以不能被当做是垃圾清除。这种垃圾称为浮动垃圾。
非垃圾变为了垃圾
标记的下一步操作是从队列中取出B对象进行分析,但是这个时候GC线程的时间片用完了,操作系统调度用户线程来运行,而用户线程先执行了这个操作:A.f = F;那么引用关系变成了:
接着执行:B.f=null;那么引用关系变成了:
好了,用户线程的事儿干完了,GC线程重新开始运行,按照之前的标记流程继续走:从队列中取出B对象,发现B对象没有直接引用,那么将B对象变为黑色:
接着继续分别从队列中取出E、C、D三个灰色对象,它们都没有直接引用,那么变为黑色对象:
到现在所有灰色对象分析完毕,你肯定已经发现问题了,出现了黑色对象直接引用白色对象的情况,而且虽然F是白色对象,但是它是垃圾吗?显然不是垃圾,如果F被当做垃圾清理掉了,那就GG~
6.8 STW(Stop-The-World)
6.8.1 定义
在执行垃圾收集算法时,Java应用程序的其他所有线程(除了垃圾收集)都被挂起。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。
6.8.2 作用
有时候我们需要全局所有线程进入 SafePoint 这样才能统计出那些内存还可以回收用于 GC,以及回收不再使用的代码清理 CodeCache,以及执行某些 Java instrument 命令或者 JDK 工具,例如 jstack 打印堆栈就需要 Stop the world 获取当前所有线程快照。
6.8.3 SafePoint
6.8.3.1 原理
代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,线程可以暂停;保存了一些当前线程的运行信息,供其他线程读取;所有线程进入 SafePoint等待的情况,就是 Stop the world
6.8.3.2 作用
保存了线程上下文中的任何东西,只有在线程处于 SafePoint 的时候,对这些信息进行修改,线程才能感知到;还有一个重要的 Java 线程特性也是基于 SafePoint 实现的,那就是 Thread.interrupt(),线程只有运行到 SafePoint 才知道是否 interrupted
6.8.3.2 实现
SafePoint 可以插入到代码的某些位置,每个线程运行到 SafePoint 代码时,主动去检查是否需要进入 SafePoint,这个主动检查的过程,被称为 Polling。理论上,可以在每条 Java 编译后的字节码的边界,都放一个检查 Safepoint 的机器命令。线程执行到这里的时候,会执行 Polling 询问 JVM 是否需要进入 SafePoint,这个询问是会有性能损耗的,所以 JIT 会优化尽量减少 SafePoint;经过 JIT 编译优化的代码,会在所有方法的返回之前,以及所有非counted loop的循环(无界循环)回跳之前放置一个 SafePoint,为了防止发生 GC 需要 Stop the world 时,该线程一直不能暂停,但是对于明确有界循环,为了减少 SafePoint,是不会在回跳之前放置一个 SafePoint
6.8.3.2 进入时机
运行字节码
解释器在通过字节码派发表(dispatch table)获取到下一条字节码的时候会主动检查安全点的状态
运行 native 代码
VMThread不会等待线程进入安全点,执行JNI退出后线程需要主动检查安全点状态,如果此时安全点位置被标记了,那么就不能继续执行,需要等待安全点位置被清除后才能继续执行;
运行 JIT 编译好的代码
编译器会在合适的位置(比如循环、方法调用等)插入读取全局Safepoint Polling内存页的指令,如果此时安全点位置被标记了,那么Safepoint Polling内存页会变成不可读,此时线程会因为读取了不可读的内存也而陷入内核,事先注册好的信号处理程序就会处理这个信号并让线程进入安全点。
处于 BLOCK 状态
比如线程在等待锁,那么线程的阻塞状态将不会结束直到安全点标志被清除掉;
处于线程切换状态或者处于 VM 运行状态
切换前会先检查安全点的状态,如果此时要求进入安全点,那么切换将不被允许,需要等待直到安全点状态清除;不被允许,会一直轮询线程状态直到线程处于阻塞状态