本文共 7208 字,大约阅读时间需要 24 分钟。
在堆中存放着所有对象实例,那虚拟机是如何判断该对象已死,是需要进行GC回收的呢?这里两种算法:
引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用
可达性分析
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。
Java虚拟机中就是采用第二种方法:可达性分析
即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这是它们暂时进入“缓刑”阶段,要真正宣告对象死亡,必须经历两个标记阶段:
在java中,可以作为GC Roots对象的包括:
- 方法区: 类静态属性引用的对象
- 方法区: 常量引用的对象
- 虚拟机栈(本地变量表)中引用的对象
- 本地方法栈JNI(Native方法)中引用的对象
1、新生代-标记清除算法
分为两个阶段,首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,标记清除之后会产生大量的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
2、新生代-复制算法
为了解决效率问题,出现了复制收集算法。将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
适用于年轻代内存的垃圾回收算法
优点:
- 这样使得每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等情况
- 只要移动堆顶指针,按顺序分配内存即可,实现简单高效
- 特别适合java朝生夕死的对象特点
缺点:
- 内存减少为原来的一般,太浪费
- 对象存活率较高的时候就要执行较多的复制操作算法,效率变低
其实现在的商业虚拟机都采用这种方式回收新生代
IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以不需要按照1:1的方式来分配空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。
两块较小的空间称为FromSpace和ToSpace,每次只使用Eden和FromSpace。
当垃圾回收时,将Eden和FromSpace中还存活着的对象一次性地复制到另外一块ToSpace空间上,最后清理掉Eden和FromSpace空间。
虚拟机默认设置内存空间比例Eden:FromSpace:ToSpace=8:1:1,即只有10%的内存被浪费。当ToSpace要满的时候,就需要老年代担保了。
分配图如下:
3、老年代-标记整理算法
复制算法不适用于对象存活率较高的情况,因为效率会变低,而且更关键的是50%空间的浪费显然不满足所有对象都100%存活的极端情况根据老年代的特点(老年代都是保存从年轻代过来的存活的对象,内存满时会触发full GC),复制算法不适用于老年代,而“标记-整理”算法就比较适合老年代
标记过程和“标记整理算法”中的标记一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:
- 不会损失50%的空间
- 垃圾回收后空间连续,只要移动堆顶指针,按顺序分配内存即可
- 比较适合有大量存活对象的垃圾回
缺点:
- 标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法
4、分代收集算法【最常用】
当前商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活期间的不同将内存划分为几块,一般把堆分为老年代和新生代。
新生代:每次垃圾收集都会发现大批对象死去,只有少量存活,就可以选用复制算法
老年代:因为对象存活率高、没有额外的空间对它进行分配担保,就必须使用标记清理或标记整理算法
如果垃圾收集算法是一种理论,那么垃圾收集器就是具体的实现了。
总共7种收集器,其中G1是JDK1.7 Update 14之后引入的,最前沿的收集器成果。如果两个收集器之间存在连线,就说明它们可以搭配使用。
虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
以上的收集器各有各的特点,适用于不同的场景,没有绝对的好坏。
1、Serial收集器
最基本、历史最悠久的收集器,单线程收集器,采用复制算法。进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(stop the world)。
简单高效,因为Serial收集器没有线程交互的开销,专心做垃圾收集。
一般用于Client模式下的虚拟机
- 虚拟机以Client模式启动会比较快,以Server模式启动时速度较慢,但一旦运行起来时性能提升很大
- 当JVM用于启动GUI界面的交互应用时适合使用client模式,JVM在client模式默认-Xms是1M,-Xmx是64M
- 当JVM用于运行服务器后台程序时建议用Server模式,JVM在Server模式默认-Xms是128M,-Xmx是1024M
- 我们可以通过运行:java -version来查看jvm默认工作在什么模式
2、ParNew收集器
Serial收集器的多线程版,可以配合老年代垃圾收集器CMS工作。但是在单CPU环境中绝对不会有比Serial收集器更好的效果,甚至存在线程交互的开销。
当然,随着CPU的数量增加,它对于GC还有很有用处的。
它默认开启的收集垃圾线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多)的环境下,是可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
3、Parallel Scavenge收集器
新生代收集器,使用复制算法、并行多线程收集
关注的目标是达到一个可控制的吞吐量,而其他收集器关注的是尽可能地缩短垃圾收集时用户线程的停顿时间
- 所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- 虚拟机总共运行了100分钟,其中垃圾收集花掉了1分钟,则吞吐量为99%
4、Serial Old收集器
Serial收集器的老年代版本,同样也是一个单线程收集器,使用标记整理算法,主要用于Client模式下的虚拟机5、Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法,JDK1.6中才开始提供。在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择。
由于老年代Serial Old在服务端应用性能上的拖累,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果。
6、CMS收集器
Concurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。整个过程包括4个步骤:由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以总体来说,CMS收集器的内存回收过程与用户线程一起并发执行的。
CMS是一款优秀的收集器,主要优点是:并发收集、低停顿。
但是其仍有3个明显的缺点:
- 对CPU资源非常敏感。 在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低。为了解决这种情况,虚拟机提供了一种“增量式并发收集器” 的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐)
- CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾”。无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。因为浮动垃圾的存在,需要预留足够的内存空间给用户线程使用。JDK1.5中,CMS收集器当老年代使用了68%空间后会被激活,这是保守设置,若应用中老年代增长不是很快,可以适当调高参数XX:CMSInitiatingOccupancyFraction值。在JDK1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机启动预案:临时启用Serial Old收集器来重新进行老年代的收集,这样停顿时间更长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的太高很容易导致大量的“Concurrent Mode Failure”失败,性能反而降低。
- CMS基于“标记-清除”算法,容易产生内存碎片。内存碎片过多时,往往会出现老年代还有很大剩余空间,但是无法找到足够大的连续空间类分配当前对象,不得不提前出发一次Full GC。为了解决该问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,内存碎片问题没了,但停顿时间不得不变长。还有一个参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC都会进行碎片整理)
7、G1收集器
当今收集器技术发展的最前沿成果之一,JDK1.7中出现,是一款面向服务端应用的垃圾收集器,不需要其他收集器的配合就能独立管理整个GC堆。有如下特点:
- 并行和并发,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍可以通过并发的方式让Java程序运行
- 分代收集,分代概念在G1中仍然沿用,可不需要配合其他收集器便可独立管理GC堆
- 空间整合,G1整体是基于“标记-整理”算法实现收集,G1运行期间不会产生内存空间碎片,在分配大对象时不会因为无法找到连续内存空间而提前出发下一次GC
- 可预测的停顿,G1除了追求低停顿外,还能建立可预测的停顿时间模型
G1收集器将整个内存空间划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但它们不再是物理隔离的了,它们都是一部分Region的集合。也是分为4个步骤:
G1收集器采用化整为零的思想,每一块内存都独立管理和回收,这样互不影响。
| | | | |
---|---|---|---|---|
Serial | 新生代,复制算法 | 单线程 | 进行垃圾收集时,必须暂停所有工作线程,直到完成;(stop the world) | 简单高效,适合内存不大的情况 |
ParNew | 新生代,复制算法 | 并行的多线程收集器 | ParNew垃圾收集器是Serial收集器的多线程版本 | 搭配CMS垃圾回收器的首选 |
Parallel Scavenge | 新生代,复制算法 | 并行的多线程收集器 | 吞吐量优先收集器,类似ParNew,更加关注吞吐量,达到一个可控制的吞吐量 | 本身是Server级别多CPU机器上的默认GC方式,主要适合后台运算不需要太多交互的任务 |
Serial Old | 老年代,标记整理算法 | 单线程 | jdk7/8默认的老生代垃圾回收器 | Client模式下虚拟机使用 |
Parallel Old | 老年代,标记整理算法 | 并行的多线程收集器 | Parallel Scavenge收集器的老年代版本,为了配合Parallel Scavenge的面向吞吐量的特性而开发的对应组合 | 在注重吞吐量以及CPU资源敏感的场合采用 |
CMS | 老年代,标记清除算法 | 并行与并发收集器 | 尽可能的缩短垃圾收集时用户线程停止时间缺点在于:1.内存碎片2.需要更多cpu资源3.浮动垃圾问题,需要更大的堆空间 | 重视服务的响应速度、系统停顿时间和用户体验的互联网网站或者B/S系统。CMS是目前互联网后端主流的垃圾回收器。因为CMS会产生内存碎片且不会等到老年代满了就会进行FullGC,因此会比较耗内存和CPU |
G1 | 跨新生代和老年代,标记整理 + 化整为零 | 并行与并发收集器 | JDK1.7才正式引入,采用分区回收的思维,基本不牺牲吞吐量的前提下完成低停顿的内存回收;可预测的停顿是其最大的优势 | 面向服务端应用的垃圾回收器,目标为取代CMS |
查看虚拟机使用的垃圾收集器命令如下:
#以下两条都可以java -XX:+PrintFlagsFinal -version | grep :java -XX:+PrintCommandLineFlags -version
或者使用java代码:
public static void main(String[] args) { /* -XX:+UseParallelOldGC和-XX:+UseParallelGC结果一样,因为MXBean名字一样,但是实际使用的不一样 */ Listbeans = ManagementFactory.getGarbageCollectorMXBeans(); for (GarbageCollectorMXBean bean : beans) { System.out.println(bean.getName()); }}
-XX:+UseSerialGC,虚拟机运行在Client模式下的默认值,Serial+Serial Old。
-XX:+UseParNewGC,ParNew+Serial Old,在JDK1.8被废弃,在JDK1.7还可以使用。
-XX:+UseConcMarkSweepGC,ParNew+CMS+Serial Old,Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用
-XX:+UseParallelGC,虚拟机运行在Server模式下的默认值,Parallel Scavenge+Serial Old(PS Mark Sweep)。
-XX:+UseParallelOldGC,Parallel Scavenge+Parallel Old。
-XX:+UseG1GC,G1+G1
- 代码中调用System.gc()
- 老年代空间不足
- 永久区空间不足
- 统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 堆中分配很大的对象导致老年代没有连续的内存空间存放
转载地址:http://nzpxi.baihongyu.com/