一:什么是垃圾回收机制(GC)
在C/C++程序中,程序员在内存中主动开辟一段相应的空间来存值。由于内存是有限的,所以当程序不再需要使用该内存空间时,就需要销毁对象并释放其所占用的内存资源,好重新利用这段空间。在C/C++中,释放无用内存空间的事情需要由程序员自己来处理。就是说当程序员认为空间没用了,就手动地释放其占用的内存。但是这样显然非常繁琐,如果有所遗漏,就可能造成资源浪费甚至内存泄露。当软件系统比较复杂,变量多的时候程序员往往就忘记释放内存或者在不该释放的时候释放内存了。
有了GC,程序员就不需要再手动的去控制内存的释放。当Java虚拟机(VM)或.NETCLR发觉内存资源紧张的时候,就会自动地去清理无用对象(没有被引用到的对象)所占用的内存空间
二:需要GC的内存区域
jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。
三:如何判断一个对象是否存活(死的话就回收)
(1)引用计数:
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
(2)可达性分析(Reachability Analysis):
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
3:java中可作为GC Root的对象有
1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)
四:何时触发GC
- 程序调用System.gc时可以触发
- 系统自身来决定GC触发的时机(Young GC,Old GC,FULL GC ,Mixed GC)
- Young GC:
新生代内存的垃圾收集事件称为 Young GC(又称 Minor GC),当 JVM 无法为新对象分配在新生代内存空间时总会触发 Young GC。比如 Eden 区占满时,新对象分配频率越高,Young GC 的频率就越高。Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代 GC 造成的停顿,几乎可以忽略不计。 - Old GC:只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式
- Full GC:清理整个堆的 GC 事件,包括新生代、老年代、元空间等
- Mixed GC:清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式。
五:GC做了什么事情
主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。
六:GC常用的算法
1:简介
GC常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
目前主流的JVM(HotSpot)采用的是分代收集算法。
2.标记-清除算法:
- a.标记-清除算法:
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。 - 优点:
最大的优点是,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。 - 缺点:
它的缺点就是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。
3:复制算法:
- b.复制算法:
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
注意:
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。 - 优点:
实现简单;不产生内存碎片 - 缺点:
每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。(内存利用率不高)
4:标记整理算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
5:分代收集算法:
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。
在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。
老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。(一般将占用内存较大的对象存放在老年代的内存中)
具体过程:新生代(Young)分为Eden区,From区与To区(也就是survive)
- 当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。
- 这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区。
- 下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
- 经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。
- 老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。
七:垃圾回收器
1:概览
垃圾回收器一般是分为年轻代何老年代的 需要搭配使用
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
2:Serial收集器
串行收集器是最古老,最稳定以及效率高的收集器
可能会产生较长的停顿(stop-the-world应用线程终止 启动GC线程),只使用一个线程去回收
-XX:+UseSerialGC
新生代、老年代使用串行回收
新生代复制算法
老年代标记-压缩
3:ParNew并行收集器
解决Serival单个线程回收效率低下的弊端,从而使用多线程
-XX:+UseParNewGC(new代表新生代,所以适用于新生代)
新生代并行
老年代串行
Serial收集器新生代的并行版本
在新生代回收时使用复制算法
多线程,需要多核支持
-XX:ParallelGCThreads 限制线程数量
4.Parallel收集器
Parallel收集器 和ParNew收集器类似,是一个新生代收集器。使用复制算法的并行多线程收集器。
该垃圾收集器,是JAVA虚拟机在Server模式下的默认值,使用Server模式后,Java虚拟机使用Parallel Scavenge收集器(新生代)+ Serial Old收集器(老年代)的收集器组合进行内存回收。
该收集器还有个特点就是“吞吐量优先”JVM自动调节参数,已达到预设的吞吐量,提升性能。
5:Parallel Old 收集器
老年代收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
6:CMS收集器(老年代收集器)
(1):简介
Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)
使用标记-清除算法
并发阶段会降低吞吐量(停顿时间减少,吞吐量降低)
老年代收集器(新生代使用ParNew)
-XX:+UseConcMarkSweepGC
老年代收集器,它的主要适合场景是对响应时间的重要性需求 大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。在我们的应用中,因为有缓存的存在,并且对于响应时间也有比较高的要求,因此希 望能尝试使用CMS来替代默认的server型JVM使用的并行收集器,以便获得更短的垃圾回收的暂停时间,提高程序的响应性。
(2):CMS的运行过程
- 1.初始标记(会产生全局停顿)
根可以直接关联到的对象
速度快 -
- 并发标记(和用户线程一起)
主要标记过程,标记全部对象
- 并发标记(和用户线程一起)
-
- 重新标记 (会产生全局停顿)
由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
- 重新标记 (会产生全局停顿)
-
- 并发清除(和用户线程一起)
基于标记结果,直接清理对象
这里就能很明显的看出,为什么CMS要使用标记清除而不是标记压缩,如果使用标记压缩,需要多对象的内存位置进行改变,这样程序就很难继续执行。但是标记清除会产生大量内存碎片,不利于内存分配。
- 并发清除(和用户线程一起)
(3):CMS收集器特点:
- a:尽可能降低停顿会影响系统整体吞吐量和性能
比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半 - b:清理不彻底
因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理 - c:CMS也提供了整理碎片的参数
设置进行几次Full GC后,进行一次碎片整理,整理过程是独占的,会引起停顿时间变长 - 总结
CMS的提出是想改善GC的停顿时间,在GC过程中的确做到了减少GC时间,但是同样导致产生大量内存碎片,又需要消耗大量时间去整理碎片,从本质上并没有改善时间。
7:G1收集器
(1):简介
- G1(Garbage-First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器。
- 与CMS收集器相比G1收集器有以下特点:
(1) 空间整合,G1收集器采用标记-整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
(2)可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。 - G1 堆空间划分
- Region
为实现大内存空间的低停顿时间的回收,将划分为多个大小相等的 Region。每个小堆区都可能是 Eden 区,Survivor 区或者 Old 区,但是在同一时刻只能属于某个代
在逻辑上, 所有的 Eden 区和 Survivor 区合起来就是新生代,所有的 Old 区合起来就是老年代,且新生代和老年代各自的内存 Region 区域由 G1 自动控制,不断变动。 - G1 把堆内存划分成一个个 Region 的意义在于:
每次 GC 不必都去处理整个堆空间,而是每次只处理一部分 Region,实现大容量内存的 GC。
通过计算每个 Region 的回收价值,包括回收所需时间、可回收空间,在有限时间内尽可能回收更多的垃圾对象,把垃圾回收造成的停顿时间控制在预期配置的时间范围内,这也是 G1 名称的由来:Garbage-First
(2):G1的几个阶段
初始标记(会有stw)
并发标记
最终标记
筛选回收
(3):相对于CMS的区别在:
- G1在压缩空间方面有优势
- G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
- Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
- G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
- G1也会在Young GC中使用、而CMS只能在Old GC区使用