GC的对象
可以被垃圾回收的对象:没有引用的对象
- 强引用——普遍的引用类型,只要强引用还在就不会被回收(如:Object obj = new Obje(););
- 软引用——可有可无的对象,在内存溢出之前会被回收;
- 弱引用——gc运行时时就会回收的对象;
- 虚引用——无法通过该引用获取对象的实例,被回收时会收到一个系统通知。
判断对象有没有引用的两种方法:
- 引用计数器:对象每有一个引用,计数器+1,计数器为0时可以回收。无法解决循环引用问题。
- 根搜索算法(可达性分析算法):以一系列的GCRoot开始,向下搜索,不可达的对象即没有引用。(见下节,GC算法的根搜索算法)
回收方法区
方法区主要回收的部分是废弃常量和无用的类。
在常量池中,有字符串abc,当没有任何的String对象的引用常量池中的 abc 常量,也没有其他地方引用这个字面量,则这个常量是废弃常量,无用的类。
- Java堆中不存在该类的任何实例
- 加载该类的ClassLoader被回收了
- 该类对应的java.lang.Class 对象在任何地方没有被引用
GC的触发时机
没有足够的内存可以使用的时候(详细见GC分类中,各类的触发时机)
其他发生GC情况:
- 作用域发生未捕获异常
- 程序在作用域正常执行完毕
- 程序执行了System.exit()
- 程序发生意外终止(被杀进程等)
System.gc();直接调用gc方法不一定会直接触发垃圾回收
GC的流程
- 通过GC算法发现了不可达对象进行第一次标记。
- 其中重写了finalize方法,并且没有被执行的对象进入F-Queue队列
- 队列中的对象,jvm创建一个低优先级线程finalizer来执行对象的finalize方法
- 执行完成后,在finallize方法中重新建立链接的对象移出队列,改为不回收
- 未建立链接的对象和第一次未进入队列的对象标记为第二次标记
- 接着,被第二次标记的对象被回收。
- 执行了finallize方法的对象在下一次GC中不会进入F-Queue队列,直接进行二次标记。
finalize方法:在被回收之前会被调用。经过调用有可能与GCRoot建立链接,不用被回收。
GC算法
根搜索算法
根搜索算法在所有的GC算法中都会使用到,其目的是找到需要被回收的对象。
程序把所有的引用关系看作一张图,从一个节点GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点
可以作为GCRoot的对象:
1,虚拟机栈中引用的对象(本地变量表)
2,方法区中静态属性引用的对象
3,方法区中常量引用的对象
4,本地方法栈中引用的对象(Native Object)
标记/清除算法
标记/清除算法是在根搜索算法的基础上加入了对无用节点的清除达到了GC的目的。标记所有的GCRoot,将所有GCRoot可达的对象标记为存活;将没有标记的对象全部清除。
缺点:效率低,停止程序,清理完的空间不连续。
复制算法
将整个内存划分为两个区间,一个空间使用,另一个空间空闲。GC时同样使用根搜索算法进行标记,标记存活的对象,移动到空闲的空间进行整理。将剩余的对象清除;然后两个空间互换,原本空闲的空间变成使用。
缺点:一半空间是处于空闲状态,利用率低。
优点:解决了内存碎片问题
标记整理算法
标记整理算法在标记清除算法的基础上,对清理完的不连续的空间进行整理,将存活的对象往左端空闲空间移动整理。
缺点:成本更高,资源消耗大
优点:解决了内存碎片的问题
GC分类
作者:RednaxelaFX
链接:https://www.zhihu.com/question/41922036/answer/93079526
来源:知乎 针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
- Partial GC:并不收集整个GC堆的模式
- Young GC:只收集young gen的GC
- Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
- Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
- Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。 最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
- young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
- full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。
jvm为了优化内存的回收,使用分代回收的方式,对新生代内存采用复制算法(minorGC),对老年代的回收采用标记-整理算法(majorGC),Major GC之后通常会触发Full GC(全堆区垃圾回收)。
Minor GC触发机制:
当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。通过复制算法回收垃圾。复制算法不会产生内存碎片。
新生代中的对象每经过一次minorGC,年龄+1,当到达15时(默认)会放到老年代。 例外情况:在创建对象时,进行了一次minorGC,且回收后的内存仍然不足,此时会将对象直接放到永久代中。
Major GC触发机制:
老年代内存空间不足。
Full GC触发机制:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
minorGC会将一部分对象放到老年代,在minorGC执行前,会进行判断,根据之前的数据统计,如果放到老年代的部分对象所占内存大于当前老年代的剩余内存,那么会取消minorGC而转而执行fullGC
垃圾回收器
新生代收集器 | 线程 | 算法(全部为复制算法) | 优点 | 缺点 |
---|---|---|---|---|
Parallel Scavenge | 多线程(并行) | 复制算法 | 吞吐量优先,适用在后台运行不需要太多交互的任务,有GC自适应的调节策略开关 | 无法与CMS收集器配合使用 |
ParNew | 多线程(并行) | 复制算法 | 响应优先,适用在多CPU环境,Server模式一般采用ParNew和CMS组合,多CPU和多Core的环境中高效 | Stop The World |
Serial收集器 | 单线程(串行) | 复制算法 | 响应优先,适用在单CPU环境,Client模式下的默认的新生代收集器无线程交互的开销,简单而高效(与其他收集器的单线程相比) | Stop The World |
老年代收集器 | 线程 | 算法 | 优点 | 缺点 |
---|---|---|---|---|
Serial Old收集器 | 单线程(串行) | “标记-整理”(Mark-Compact)算法 | 响应优先单CPU环境下Client模式,CMS的后备预案。无线程交互的开销,简单而高效(与其他收集器的单线程相比) | Stop The World |
Parallel Old收集器 | 多线程(并行) | 标记-整理 | 响应优先吞吐量优先适用在后台运行不需要太多交互的任务有GC自适应的调节策略开关 | |
CMS收集器 | 多线程(并发) | 标记-清除 | 响应优先集中在互联网站或B/S系统服务、端上的应用。并发收集、低停顿 | 1、对CPU资源非常敏感:收集会占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低 |
G1收集器
参考(https://blog.csdn.net/Shockang/article/details/116954902)
G1收集器:
- 标记整理算法实现,新生代使用复制算法。
- 运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。
- 不会产生空间碎片,可以精确地控制停顿。
- 采用分区的概念,弱化传统的分代回收。将堆区分为:Eden区,survivor区,old区和humongous区(超过区域50%的对象会放到这个区)以及未使用的区。Eden区和survivor区是新生代;old区和humongous区是老年代;未使用区在新生代GC时,会将新生代数据放进去。
- 每个区大小在1-32之间的2的幂指数。共划分2048个左右。
CMS收集器和G1收集器的区别:
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
- G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
- CMS收集器以最小的停顿时间为目标的收集器;
- G1收集器可预测垃圾回收的停顿时间
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
- G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
[
](https://blog.csdn.net/qq_41701956/article/details/100074023)
CMS收集器
初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的”根对象”开始,只扫描到能够和”根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World。
重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从”跟对象”开始向下追溯,并处理对象关联。
并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。
阶段 | 主要工作 | 是否stw |
---|---|---|
初始化标记 | 扫描到GCRoot和其链接的首个可达对象,进行标记 | 是,短暂 |
并发标记 | 多线程并发,延续GCRoot继续往下,标记所有可达对象 | 否 |
并发预处理 | 多线程并发,查找在并发标记阶段进入老年代的对象(因为并发标记阶段并没有stw),这个阶段多线程的查找会解决大部分的新增老年代对象 | 否 |
重新标记 | 由于上一阶段没有stw,所以需要stw后,标记上一阶段新增的老年代对象 | 是,短暂 |
并发清理 | 清理垃圾对象,和java程序并发执行 | 否 |
并发重置 | 重置cms收集器的数据结构 | 否 |
两次暂停:第一次,找到GCRoot;第二次,找到中间未stw的阶段产生的新老年代对象。
多次标记:初始标记和重新标记需要暂停,中间并发标记不暂停。
预处理:多线程并发快速找到绝大多数的新增老年代对象。
CMS通过两次短暂的stw以及并发标记的方法,缩短了回收时间。但是由于并发占用了一部分线程会造成程序变慢,而且cms基础算法是标记清除算法,并不会整理回收后的内存,降低了对空间的利用率。
同时,CMS需要更大的堆空间,因为GC和程序并发执行,要同时保证两边需要留出一部分空间。这也就要求CMS需要在更早的时候被触发,而不是在老年代满的时候才开始。
为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。
使用场景
如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。
Q&A
当JVM无法为新建对象分配内存空间的时候(Eden满了),Minor GC被触发,新生代内存空间不足,GC后仍内存不足,则抛出堆内存溢出异常??
堆内存溢出的原因永远是老年代内存空间不足,发生oom时一定是经过full GC的,full GC发现老年代的空间不足以放下minorGC的数据,就会抛出异常。
流程:
- 创建对象,在新生代分配空间
- 新生代空间不足,触发minorGC
- minorGC,将新生代的数据和幸存者from的数据放到to区,发现to的空间并不够
- 触发fullGC,将新生代和from的数据放到老年代,发现老年代空间也不够
- 抛出oom错误。