一、什么是垃圾回收?
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区这部分内存的分配和回收是动态的,要对已经死亡的或者长时间没有使用的对象进行清除和回收相应的垃圾回收,以达到有效使用内存的目的。
二、哪些对象可以回收?
· 对象没有引用
· 作用域发生未捕获异常
· 程序在作用域正常执行完毕
· 程序执行了System.exit()
· 程序发生意外终止(被杀线程等
如何判断?
(当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world 的发生。)
1. 引用计数法
给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
```
publicclass abc_test {
publicstaticvoid main(String[] args) {
MyObject object1=new MyObject();
MyObject object2=new MyObject();
object1.object=object2;
object2.object=object1;
object1=null;
object2=null;
}
}
```
这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。
2. 可达性分析算法
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
Java内存区域中可以作为GC ROOT的对象:
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
三、垃圾回收算法
1. 标记-清除算法(Mark-Sweep)
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片
2. 复制算法(Copying)
复制算法(Copying)是在标记清除算法基础上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况。复制算法暴露了另一个问题,例如硬盘本来有600G,但却只能用300G,代价实在太高。
3 标记-整理算法(Mark-compact)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存(完成标记之后,先不清理,先移动再清理回收对象)
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
4. 分代收集算法Generational Collection
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外(方法区)还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
1). 年轻代(Young Generation)的回收算法 (回收主要以Copying为主)
a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0/from,survivor1/to)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空(保持survivor1为空,为了让eden和survivor0 交换存活对象), 如此往复。当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC
c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
2). 年老代(Old Generation)的回收算法(回收主要以Mark-Compact为主)
a) 在年轻代中经历了N次(16次)垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
3). 持久代(Permanent Generation)(也就是方法区)的回收算法
方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
加载该类的ClassLoader已经被回收;
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
四、垃圾回收器
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+ Serial Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+ Serial Old(老年代)
jdk1.9 默认垃圾收集器G1
1、Serial(-XX:+UseSerialGC)
Serial:串行(-XX:+UseSerialGC)>为单线程环境设计,且使用一个线程回收垃圾,会暂停所有的用户线程,不适合服务器环境。它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。
使用算法:复制算法
2、SerialOld(-XX:+UseSerialGC)
SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器。
使用算法:标记 - 整理算法
3、ParNew(-XX:+UseParNewGC)
ParNew其实就是Serial收集器的多线程版本
使用算法:复制算法
4、ParallelScavenge(-XX:+UseParallelGC)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
使用算法:复制算法
5、ParallelOld(-XX:+UseParallelOldGC)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先
使用算法:标记 - 整理算法
6、CMS (Concurrent Mark Sweep)(-XX:+UseConcMarkSweepGC)
CMS是一个老年代收集器,全称 Concurrent Low Pause Collector,是JDK1.4后期开始引用的新GC收集器,在JDK1.5、1.6中得到了进一步的改进。它是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的情况下,使用CMS非常合适。
CMS的一大特点,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。
使用算法:标记 - 清理
回收过程分为四个步骤,只有初始标记和重新标记需要暂停用户线程:
· 初始标记(STW initial mark)——仅从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。
· 并发标记(Concurrent marking)——进行GC ROOTS Tracing的过程
· 并发预清理(Concurrent precleaning)——这个阶段是并发的,在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。
· 重新标记(STW remark)——重新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。
· 并发清理(Concurrent sweeping)——这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。
· 并发重置(Concurrent reset)——这个阶段是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。
CMS的缺点:
内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。
需要更多的CPU资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。
无法处理浮动垃圾,需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。
7、GarbageFirst(G1)
优先处理那些垃圾多的内存块。G1虽然也把内存分成了这三大类,但是在G1里面这三大类不是泾渭分明的三大块内存,G1把内存划分成很多小块, 每个小块会被标记为E/S/O中的一个,可以前面一个是Eden后面一个就变成Survivor了。
由于把三块内存变成了几百块内存,内存块的粒度变小了,从而可以垃圾回收工作更彻底的并行化。
回收的大致过程是这样的:
在垃圾回收的最开始有一个短暂的时间段(Inital Mark)会停止应用(stop-the-world)
然后应用继续运行,同时G1开始Concurrent Mark
再次停止应用,来一个Final Mark (stop-the-world)
最后根据Garbage First的原则,选择一些内存块进行回收。(stop-the-world)
由于它高度的并行化,因此它在应用停止时间(Stop-the-world)这个指标上比其它的GC算法都要好。
G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。
CMS与G1的区别
使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
STW时间不一样
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型,可配置)
垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
回收过程不一样
CMS收集器 G1收集器
初始标记 1. 初始标记
并发标记 2. 并发标记
重新标记 3. 最终标记
并发清楚 4. 筛选回收
参考链接:
https://www.cnblogs.com/renxiuxing/p/14978626.html
https://www.cnblogs.com/aspirant/p/8663872.html
https://www.cnblogs.com/aspirant/p/8662690.html
https://zhuanlan.zhihu.com/p/25539690
https://www.jianshu.com/p/23f8249886c6