目录
3.Parallel Scavenge收集器(并行扫描式,吞吐量优先收集器)
为什么?
1.为什么学习垃圾回收机制:
当需要排查各种内存溢出,内存泄漏问题时,当垃圾回收成为系统高并发达到更高并发量的瓶颈时,我们就要对这种自动的机制进行监控和调节。
Garbage Collection 垃圾收集。这里所谓的垃圾指的是在系统运行过程当中所产生的一些无用的对象,这些对象占据着一定的内存空间,如果长期不被释放,可能导致OOM。
2.内存回收关注的对象:
内存区域中的程序计数器、虚拟机栈、本地方法栈这3个区域随着线程而生,线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作,每个栈帧中分配多少内存基本是在类结构确定下来时就已知的。在这几个区域不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。(故不需要垃圾回收)
而Java堆和方法区则不同,Java虚拟机中内存回收指的是Java堆区和方法区。一个接口中的多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也不一样。只有在程序运行时才直到会创建那些对象,这部分内存是动态分配的。
判断对象是否还被引用:
1.计数算法(老牌垃圾回收算法。无法处理循环引用,没有被Java采纳)
给每个对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;
当引用失效时,计数器就减1;
当任何时刻计数器为0时,这个对象不再被引用。
优点:实现简单,判定效率高。
缺点:很难解决对象之间的循环引用问题。
主流的java虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因是:它很难解决对象之间相互循环引用的问题。
对于最右边的那张图而言:循环引用的计数器都不为0,但是他们对于根对象都已经不可达了,但是无法释放。
2.可达性分析方法:JVM一般会采用一种新的算法
思路:通过一系列“GC roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。当一个对象到CG roots没有任何的引用链相连时,此节点不再被引用。设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的。
可作为CG roots的对象:
- 虚拟机栈中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的方法;
- 本地方法区中JNI(Native方法)引用的对象。
(第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。)
在根搜索算法的基础上,现代虚拟机的实现当中,垃圾搜集的算法主要有三种,分别是标记-清除算法、复制算法、标记-整理算法。这三种算法都扩充了根搜索算法
垃圾回收算法:
1.标记 - 清除算法
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。
- 清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
也就是说,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。
为什么非要停止程序的运行呢?
答:这个其实也不难理解,假设我们的程序与GC线程是一起运行的,各位试想这样一种场景。
假设刚标记完一个对象,暂且记为A,此时在程序当中又new了一个新对象B,且A对象可以到达B对象。但是由于此时A对象已经标记结束,B对象此时的标记位依然是0(默认未标记的情况),因为它错过了标记阶段。因此当接下来轮到清除阶段的时候,新对象B将会被苦逼的清除掉。如此一来,不难想象结果,GC线程将会导致程序无法正常工作。导致刚new了一个对象,结果经过一次GC,忽然变成null了,这还怎么玩?
总结:分为标记+清除 (最基础的收集算法)
缺点:1>效率问题,标记和清除效率都不高;(递归与全堆对象遍历),导致stop the world的时间比较长,尤其对于交互式的应用程序来说简直是无法接受。
2>空间问题,清除后会产生大量的不连续内存碎片(清理出来的空闲内存是不连续的);而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
2.复制算法(新生代的GC)
将内存按容量划分为大小相同的两块,每次只使用其中一块。当这一块使用完时,将还存活的对象复制到另一块上面。然后将已使用过的这一块一次性清除。再交换两个内存的角色,完成垃圾回收。(常使用于朝生暮死的对象,不适用于老年代)
优点:实现简单,运行高效。
缺点:将内存缩小为原来的一半。(浪费空间)
与标记-清除算法相比,复制算法是一种相对高效的回收方法
不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC)
解决空间浪费的办法:
新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。整个过程如下图所示:
上图中,绿色箭头的位置代表的是大对象,大对象直接进入老年代。
3.标记 - 整理算法(老年代的GC)
分为标记+整理(让所有存活的对象都向一端移动,然后清理掉端边界之外的内存) (常用于老年代)
标记-整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记;但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间。
- 标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
- 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
缺点:标记/整理算法唯一的缺点就是效率也不高。
三种回收方式的总结:
三个算法都基于根搜索算法去判断一个对象是否应该被回收,而支撑根搜索算法可以正常工作的理论依据,就是语法中变量作用域的相关内容。因此,要想防止内存泄露,最根本的办法就是掌握好变量作用域,而不应该使用C/C++式内存管理方式。
在GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序(stop the world)。
Stop-The-World概念:
Java中一种全局暂停的现象。
全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
多半情况下是由于GC引起。
少数情况下由其他情况下引起,如:Dump线程、死锁检查、堆Dump。
GC时为什么会有全局停顿?
(1)避免无法彻底清理干净
如果没有全局停顿,会给GC线程造成很大的负担,GC算法的难度也会增加,GC很难去判断哪些是垃圾。
(2)GC的工作必须在一个能确保一致性的快照中进行。
这里的一致性的意思是:在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因。
3、Stop-The-World的危害:
长时间服务停止,没有响应(将用户正常工作的线程全部暂停掉)
遇到HA系统,可能引起主备切换,严重危害生产环境。
备注:HA:High Available, 高可用性集群。
比如上面的这主机和备机:现在是主机在工作,此时如果主机正在GC造成长时间停顿,那么备机就会监测到主机没有工作,于是备机开始工作了;但是主机不工作只是暂时的,当GC结束之后,主机又开始工作了,那么这样的话,主机和备机就同时工作了。主机和备机同时工作其实是非常危险的,很有可能会导致应用程序不一致、不能提供正常的服务等,进而影响生产环境。
它们的区别如下:(>表示前者要优于后者,=表示两者效果一样)
(1)效率:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
(2)内存整齐度:复制算法=标记/整理算法>标记/清除算法。
(3)内存利用率:标记/整理算法=标记/清除算法>复制算法。
注1:可以看到标记/清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。
注2:时间与空间不可兼得。
4.分代收集算法(新生代的GC+老年代的GC)
根据对象的存活周期的不同将内存划分为几块。一般将Java堆分为新生代和老年代。这样就可以根据各个年代的特点采用最适合的收集算法。新生代用复制算法,老年代用标记清理或标记整理。(短命对象归为新生代,长命对象归为老年代。)
- 少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。
- 大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。
注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。
垃圾收集器:
收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。
一般虚拟机会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集齐。
1.Serial收集器(连续式,穿行GC)
单线程的收集器,进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束。为Client模式下的默认新生代收集器。特点:简单而高效(与其他收集器的单线程比较)新生代采用复制算法,老年代采用标记整理法;
2.ParNew收集器(同新式,并行GC)
Serial收集器的多线程版,除使用多线程进行GC收集外,其余相同。Server模式下的虚拟机中首选的新生代收集器。
3.Parallel Scavenge收集器(并行扫描式,吞吐量优先收集器)
新生代收集器,采用复制算法,多行的多线程收集器。特点:关注的目标是达到一个可控制的吞吐量(运行时间/(运行用户代码时间+垃圾收集时间)),主要适合在后台运算而不需要太多交互的任务。
4.Serial Old收集器
Serial收集器的老年代版本,使用标记整理算法。Client端。
5.Parallel Old收集器
Parallel Scavenge的老年版,使用多线程+标记整理算法。在注重吞吐量及CPU敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old的组合。
6.CMS收集器(并发标记清除收集器)
以获取最短回收停顿时间为目标的收集器。常用在B/S服务器端上。分为四步:
- 初始标记:仅仅标记一下GC roots能直接关联到的对象,需要“stop the world”,很快;
- 并发标记:进行GC Roots Tracing 的过程;同步
- 重新标记:需要“stop the world”,修正并发标记期间因用户程序运行而导致标记产生变动的那一些对象的标记记录;时间比初始标记长比并发标记短;
- 并发清除:清除;同步
优点:并发收集,低停顿;
缺点:
- 对cpu十分敏感,因为占用了一部分线程,当cpu不足4个时,对用户程序运行影响较大;
- 无法处理浮动垃圾(由于并发清理阶段用户程序还在运行,随之会产生新的垃圾),可能出现“Concurrent Mode Failure”导致另一次Full GC的产生。因为浮动垃圾的产生所以不能像其他收集器那样等到老年代几乎被完全填满了再进行收集。当预留内存无法满足程序需求时,临时启用Serial Old收集器来进行老年代的垃圾收集。
- 是一款基于“标记清除”算法的收集器,会产生大量的空间碎片。