垃圾收集(Garbage Collection, 简称GC )
1、判断对象存活算法
引用计数法
占用一些额外的内存空间来进行计数。
在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加一; 当引用失效时, 计数器值就减一; 任何时刻计数器为零的对象就是不可
能再被使用的。
该方法缺陷:对于两个对象相互引用,该算法则无法判断是否引用结束,陷入死循环。Hotspot不采用此算法
可达性算法
通过一系列的垃圾收集器根节点作为其实结点集,从该节点开始,根据引用关系向下搜索,如果没有任何引用链(搜索路径为引用链),则说明该对象不再被使用。
在不同的数据区域,可以采用的垃圾收集器根节点也不同,
·在虚拟机栈(栈帧中的本地变量表) 中引用的对象, 譬如各个线程被调用的方法堆栈中使用到的参数、 局部变量、 临时变量等。
·在方法区中类静态属性引用的对象, 譬如Java类的引用类型静态变量。
·在方法区中常量引用的对象, 譬如字符串常量池(String Table) 里的引用。 在本地方法栈中JNI(即通常所说的Native方法) 引用的对象。
·Java虚拟机内部的引用, 如基本数据类型对应的Class对象, 一些常驻的异常对象(比如NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器。
·所有被同步锁(synchronized关键字) 持有的对象。
·反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等
引用类型
①强引用(Strongly Reference)
new一个对象或数组就是一个强引用
只要强引用关系还存在, 垃圾收集器就永远不会回收掉被引用的对象。
无法被垃圾回收,需要主动断开连接,避免内存泄漏
强引用具有以下特点:
- 当JVM内存不足是,即使抛出OOM,也不会回收被强引用关联的对象
- 被强引用关联的对象,可以直接通过引用进行访问
- 通过将强引用置为
null
,可以断开强引用和对象之间的关联
②软引用(Soft Reference)
SoftReference类来实现软引用。
会被垃圾回收;可以通过get()获取被关联对象,与引用队列一起使用,监听被关联对象是否被回收
-
软引用关联的对象,其生命力比强引用较弱,在内存不足时会被回收。
-
需要通过
SoftReference
创建对象的软引用 -
//强引用 StudentOverride student = new StudentOverride("lucy", 25); // 创建对象的软引用 SoftReference<StudentOverride> softReference = new SoftReference<>(student);
弱引用具有以下特点:
- 被弱引用关联的对象不能直接访问,可以通过get()获取
- 被弱引用关联的对象,无论内存是否充足,都会被回收
- 弱引用同样适用于对内存敏感的缓存
- 弱引用也可以关联引用队列,实现对弱引用的管理和被弱引用关联对象的监听
③弱引用(Weak Reference)
提供了WeakReference类来实现弱引用
不管内存是否充足,被关联的对象在gc时都会被回收;可以通过get()获取被关联对象,与引用队列一起使用,监听被关联对象是否被回收
- 弱引用关联的对象,其生命力比软引用关联的对象更弱,无论内存是否充足,都会被回收
- 弱引用需要使用
WeakReference
进行创建 - 虚引用具有以下特点:
- 被虚引用关联的对象,任何时候都可能被垃圾回收,容易发送内存泄漏
- 虚引用就像幽灵,不影响被关联对象的生命周期,也无法通过虚引用
get()
到被关联的对象 - 虚引用必须和引用队列一起使用,可以监听对象是否被回收,从而进行相应的处理。
④虚引用(Phantom Reference)
提供了PhantomReference类来实现虚引用。
关联的对象在任何时候都可能被回收;不影响被关联对象的生命周期,无法通过get()获取被关联对象;必须与引用队列一起使用,可以跟踪对象是否被垃圾回收
- 虚引用,又叫幻影引用,因为它对被关联对象的生命周期没有任何影响,
- 可以使用
PhantomReference
创建虚引用
虚引用具有以下特点:
- 被虚引用关联的对象,任何时候都可能被垃圾回收,容易发送内存泄漏
- 虚引用就像幽灵,不影响被关联对象的生命周期,也无法通过虚引用
get()
到被关联的对象 - 虚引用必须和引用队列一起使用,可以监听对象是否被回收,从而进行相应的处理。
经过可达性算法,即使被判定为不可达对象,也不能认为是“已死”的对象。还要经历两次标记:
①判断与GC Roots是否有引用链, 若无,进行下一步晒学
②筛选,是否必要执行finalize方法,若该对象没有重写finalize方法,最终判定对象已死。否则,对象还是存活,成功自救(finalize方法已过时,try-finally或者其他方式都可以做得更好 )
方法回收区
回收效率低
方法区的垃圾收集主要回收两部分内容: 废弃的常量和不再使用的类型
不再被使用的类需要同时满足下面三个条件 :
①该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生子类的实例。
②加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景, 如OSGi、 JSP的重加载等, 否则通常是很难达成的。
③该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法
2、垃圾收集算法
分代收集理论(Generational Collection)
该理论是基于实际运行情况的经验法则,建立在两个分带假说之上:
1) 弱分代假说(Weak Generational Hypothesis) : 绝大多数对象都是朝生夕灭的。
2) 强分代假说(Strong Generational Hypothesis) : 熬过越多次垃圾收集过程的对象就越难以消亡。
3) 跨代引用假说( Intergenerational Reference Hypothesis) : 跨代引用相对于同代引用来说仅占极少数 .
根据分带假说,垃圾收集器的一致的设计原则: 收集器应该将Java堆划分出不同的区域, 然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数) 分配到不同的区域之中存储。
部分收集( Partial GC) : 指目标不是完整收集整个Java堆的垃圾收集, 其中又分为:
■新生代收集( Minor GC/Young GC) : 指目标只是新生代的垃圾收集。
■老年代收集( Major GC/Old GC) : 指目标只是老年代的垃圾收集。 目前只有CMS收集器会有单
独收集老年代的行为。 另外请注意“Major GC”这个说法现在有点混淆, 在不同资料上常有不同所指,
读者需按上下文区分到底是指老年代的收集还是整堆收集。
■混合收集( Mixed GC) : 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收
集器会有这种行为。
·整堆收集( Full GC) : 收集整个Java堆和方法区的垃圾收集
标记清除法(Mark-Sweep)
流程:首先标记出所有需要回收的对象, 在标记完成后, 统一回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一回
收所有未被标记的对象
缺点:效率不稳定,执行效率太低;碎片化问题
标记-复制法(Semispace Copying)
它将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。 新生代中的对象有98%熬不过第一轮收集 。
缺点:产生大量内存间复制开销 适用于新生代
标记-整理法(Mark-Compact)
适用于老年代
其中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存,
3、算法实现细节
根节点枚举
所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的, 因此毫无疑问根节点枚举与之前提及的整理内存碎片一样,会面临相似的“Stop The World”的困扰 。
当用户线程停顿下来之后, 其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置, 虚拟机应当是有办法直接得到哪些地方存放着对象引用的。 在HotSpot的解决方案里, 是使用一组称为OopMap的数据结构来完成GC Root枚举 。
安全点
HotSpot也的确没有为每条指令都生成OopMap, 前面已经提到, 只是在“特定的位置”记录了这些信息, 这些位置被称为安全点。
快速跑到安全点的两个方案:
抢先式中断(Preemptive Suspension)
不需要线程的执行代码
主动去配合, 在垃圾收集发生时, 系统首先把所有用户线程全部中断, 如果发现有用户线程中断的地方不在安全点上, 就恢复这条线程执行, 让它一会再重新中断, 直到跑到安全点上 。
主动式中断(Voluntary Suspension,大多数采用此类方案)
当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
安全区域
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化, 因此, 在这个区域中任意地方开始垃圾收集都是安全的。 我们也可以把安全区域看作被扩展拉伸了的安全点。
记忆集和卡表
为解决对象跨代引用所带来的问题, 垃圾收集器在新生代中建立了名为记忆集(Remembered Set) 的数据结构 ,用以避免把整个老年代加进GC Roots扫描范围
可供选择的记录精度:
·字长精度: 每个记录精确到一个机器字长(就是处理器的寻址位数, 如常见的32位或64位, 这个精度决定了机器访问物理内存地址的指针长度) , 该字包含跨代指针。
·对象精度: 每个记录精确到一个对象, 该对象里有字段含有跨代指针。
·卡精度: 每个记录精确到一块内存区域, 该区域内有对象含有跨代指针 。(可以只是一个字节数组 )
写屏障
解决卡表如何变脏,何时维护卡表的问题。
写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形通知, 供程序执行额外的动作, 也就是说赋值的前后都在写屏障的覆盖范畴内。 在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier) , 在赋值后的则叫作写后屏障(Post-Write Barrier) 。
卡表在高并发存在”伪共享“问题(当多线程修改互相独立的变量时, 如果这些变量恰好共享同一个缓存行, 就会彼此影响(写回、 无效化或者同步) 而导致性能降低。)
-XX: +UseCondCardMark, 用来决定是否开启卡表更新的条件判断。
并发的可达性分析
“三色标记”用于解决降低用户线程停顿的辅助推导工具。
白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
会产生“对象消失”的问题, 即原本应该是黑色的对象被误标为白色:
赋值器插入了一条或多条从黑色对象到白色对象的新引用;
赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
解决并发扫描时的对象消失问题, 只需破坏这两个条件的任意一个即可。 由此分别产生了两种解决方案,两种方法均是通过写屏障通过的:
增量更新(Incremental Update)
破坏的是第一个条件 ,黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象。
原始快照(Snapshot At The Beginning,SATB)
破坏的是第二个条件 ,无论引用关系删除与否, 都会按照刚刚开始扫描那一刻的对象图快照来进行搜索 。
4、经典垃圾收集器
Serial收集器
一个单线程工作的收集器,它进行垃圾收集时, 必须暂停其他所有工作线程, 直到它收集结束 。HotSpot虚拟机运行在客户端模式下的默认新生代收集器
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本 ,除多线程控制之外,其余行为与Serial一致。
其效果在单核控制器中未必比Serial效果优秀
并行(Parallel) : 并行描述的是多条垃圾收集器线程之间的关系,
并发(Concurrent) : 并发描述的是垃圾收集器线程与用户线程之间的关系,
Parallel Scavenge收集器
基于标记-复制算法实现,其关注点与其他收集器不同,尽可能地缩短垃圾收集时,用吞吐量表示。
以下两个参数控制吞吐量
-XX: MaxGCPauseMillis参数允许的值是一个大于0的毫秒数, 收集器将尽力保证内存回收花费的时间不超过用户设定值。
-XX: GCTimeRatio参数的值则应当是一个大于0小于100的整数, 也就是垃圾收集时间占总时间的比率, 相当于吞吐量的倒数。
Serial Old收集器
Serial Old是Serial收集器的老年代版本, 它同样是一个单线程收集器, 使用标记-整理算法 ,供客户端模式下的HotSpot虚拟机使用
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集, 基于标记-整理算法实现。
CMS收集器
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。 主要应用于互联网网站或者基于浏览器的B/S系统的服务端 。
该收集器有以下四个步骤:
1) 初始标记(CMS initial mark)
2) 并发标记(CMS concurrent mark)
3) 重新标记(CMS remark)
4) 并发清除(CMS concurrent sweep)
优点 :并发收集、 低停顿
缺点:CMS收集器对处理器资源非常敏感;无法处理“浮动垃圾” ;基于标记清除,会产生碎片;
Garbage First收集器 (G1)
要面向服务端应用的垃圾收集器 ,取代Parallel Scavenge加Parallel Old组合, 成为服务端模式下的默认垃圾收集器(GDK13以后), 充分利用多CPU和多环境硬件优势。
从单一分代收集,转变成面向堆内存收集,但也基于分代理论,整体采用标记-整理法,局部采用复制算法,不产生碎片。
它将独立区域 (Region)作为单次回收的最小单元, 每次收集到的内存空间都是Region大小的整数倍, 这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
实现步骤
1) 初始标记(CMS initial mark)
2) 并发标记(CMS concurrent mark)
3) 最终标记 (Final Marking)
4) 筛选回收(Live Data Counting and Evacuation)
用户可以指定期望的停顿时间是G1收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。
Full gc
1、旧生代空间不足
2、永久代空间满
3、CMS GC出现promotion 和curcurrent mode failure
4、统计得到的Mino GC 晋升到旧生代的平均大小大鱼旧生代的剩余空间
5、System,gc
5、低延时垃圾收集器
Shenandoah收集器
非Oracle开发
同G1相同,也是基于Region地堆内存布局
G1的回收阶段是可以多线程并行的, 但却不能与用户线程并发,
使用连接矩阵”(ConnectionMatrix) 的全局数据结构来记录跨Region的引用关系, 降低了处理跨代指针时的记忆集维护消耗, 也降低了伪共享问题。
分为九个阶段:
初始标记-> 并发标记 ->并发清理 ->并发回收 -> 初始引用更新 -> 并发引用更新-> 最终引用更新 ->并发清理
ZGC收集器 (Z Garbage Collector )
ZGC和Shenandoah的目标是高度相似的, 都希望在尽可能对吞吐量影响不太大的前提下, 实现任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
采用基于Region的堆内存布局 ,有小中大三类容量。
采用的染色指针技术 ,一种直接将少量额外的信息存储在指针上的技术
流程:
并发标记 -> 并发预备重分配-> 并发重分配 -> 并发重映射
6、虚拟机及垃圾收集器日志
HotSpot所有功能的日志都收归到了“-Xlog”参数上
-Xlog[:[selector][:[output][:[decorators][:output-options]]]]
最关键的参数是选择器(Selector) , 它由标签(Tag) 和日志级别(Level) 共同组成 。日志级别从低到高, 共有Trace, Debug, Info, Warning, Error, Off六种级别, 日志级别决定了输
出信息的详细程度
查看GC基本信息 -Xlog: gc:
查看GC详细信息 -X-log: gc*:
查看GC前后的堆、 方法区可用容量变化 -Xlog: gc+heap=debug:
查看GC过程中用户线程并发时间以及停顿的时间 -Xlog:safepoint:
查看收集器Ergonomics机制 -Xlog: gc+ergo*=trace:
查看熬过收集后剩余对象的年龄分布信息 -Xlog: gc+age=trace:
7、内存分配与回收策略
对象优先在Eden分配
大多数情况下, 对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时, 虚拟机将发起一次Minor GC
HotSpot虚拟机提供了-XX: +PrintGCDetails
这个收集器日志参数, 告诉虚拟机在发生垃圾收集行为时打印内存回收日志, 并且在进程退出的时候输出当前的内存各区域分配情况。
-Xms20M、 -Xmx20M、 -Xmn10M这三个参数限制了Java堆大小为20MB, 不可扩展, 其中10MB分配给新生代, 剩下的10MB分配给老年代。 -XX: Survivor-Ratio=8决定了新生代中Eden区与一个Survivor区的空间比例是8∶ 1,
大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象, 最典型的大对象便是那种很长的字符串,元素数量很庞大的数组。
长期存活的对象将进入老年代
对象通常在Eden区里诞生, 如果经过第一次Minor GC后仍然存活, 并且能被Survivor容纳的话, 该对象会被移动到Survivor空间中 ,并设置年龄为1岁,在Survivor区没熬过Minor GC年龄就增加一岁,当它的年龄增加到一定程度(默认为15) , 就会被晋升到老年代中。 对象晋升老年代的年龄阈值, 可以通过参数-XX:MaxTenuringThreshold设置。
动态对象年龄判定
为了能更好地适应不同程序的内存状况, HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX: MaxTenuringThreshold才能晋升老年代, 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 无须等到-XX:MaxTenuringThreshold中要求的年龄。
空间分配担保
在发生Minor GC之前, 虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
如果这个条件成立, 那这一次Minor GC可以确保是安全的。
如果不成立, 则虚拟机会先查看-XX: HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure) ;
如果允许, 那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,
如果大于, 将尝试进行一次Minor GC, 尽管这次Minor GC是有风险的;
如果小于, 或者-XX:HandlePromotionFailure设置不允许冒险, 那这时就要改为进行一次Full GC。