一、概述
垃圾收集Garbage Collection通常被称为GC,但是GC一般也指Garbage Collecting(垃圾回收这个动作)或Garbage Collector(垃圾回收器),这些都是是JVM知识体系中非常重要的知识,也是程序员必须要掌握的技能,本文将详细讲述Java垃圾回收的概念机制以及核心算法。
二、分析
1. 什么是垃圾
我们所说的垃圾是指没有任何引用的一个对象或者多个对象(这多个对象相互引用,但是没有一个与主对象挂钩,也就是根可达算法(下文会讲)无法找到这其中任何一个对象)。
我们再来来熟悉两个概念:
(1) 内存泄露:内存泄露是指有的内存地址太过碎片化而无法被利用,我们都知道一个对象创建的时候开辟的内存空间是连续的,所以太过碎片化的内存空间就没办法利用。内存泄露多了也会导致内存溢出。
(2) 内存溢出:内存溢出是指内存已经装满了,无法再装下更多的对象了。
C和C++都是需要开发者用代码手动回收内存的:C语言用free关键字来回收内存,C++用的是delete。但是手动回收内存容易出现两种类型问题:忘记回收(容易引发OOM内存泄露)和多次回收。
后来诞生的java、python等都是自带了垃圾回收器的语音,开发者只管创建对象,对象的销毁不需要手动处理,由专门的垃圾回收器进行回收。
2. 如何定位垃圾
常见的方式有两种:
(1) 引用计数(Reference Count):每当一块内存被一个对象引用,那么计数就+1,当没有对象指向时,计数为0,就表示这块内存可以被回收了,如下图:
但是引用计数没办法解决垃圾之间互相引用的情况,当几块内存都没有外部引用,但是这几块内存之间相互引用的时候,这几块内存也应该视为垃圾,但是引用计数却不为0,如下图:
(2) 根可达算法(Root Searching):当程序运行时,将根对象取出,由根对象出发往下查找,最终找不到的对象,都视为无法由根对象找到,也就是说找不到的对象就都视为垃圾。如下图:
那么哪些对象是根对象呢?主要包含:JVM stack,native method stack,run-time constant pool(运行常量池里的对象),static references in method area(方法区里的静态引用),Clazz等。
3. 常见的垃圾回收算法
主要包含以下3种:
(1) 标记清除(mark sweep):就是将找到的垃圾标记出来,然后直接清除掉。但是这种方式有一个严重的毛病,会使得内存变得碎片化,也就是有多个不连续的内存。
(2) 拷贝算法(copying):这种方式的做法就是将内存平分成两块,在使用的过程中只能在其中一块内存里创建对象,当需要垃圾回收时,将有对象的内存全部复制到另一边,并且将当前区域全部清除。这种方式解决了内存碎片化的问题,但是却浪费了空间,因为每次只能利用一半。
(3) 标记压缩(mark compact):这种方式就是在清理垃圾的同时,将同类型的内存空间放置在一起,也就是说在清理的同时进行空间整理,并且多线程时还需要进行线程同步,所以这种方式明显的缺点就是效率偏低。
常用的垃圾回收算法就是这3种或者这3种方式的组合。
4. JVM内存分代模型(用于分代垃圾回收算法)
JVM的内存模型是由垃圾回收器决定的,一般分为分代模型和不分代模型,两种内存模型不一样。分代垃圾回收的内存模型如下图:
分代模型在逻辑上分代,在物理层面也就是内存中也是分成了new(新生代)和old(老年代)两个大区域。新生代区又详细分为eden(伊甸园)、survivor1和survivor2。new(年轻代)的对象有两大特点:大量产生;大量回收(大多数情况下,一次回收90%的对象)。所以根据new年轻代的特点,采用的算法是Copying算法;而Old老年代则是采用标记压缩(mark compact)算法,以此保证内存的连续性 。
值得一提的是,new新生代和old老年代的比例默认是1:2。但是这个比例也是JVM调优中可以调节的参数,所以上图写的1:3。eden和survivor1,survivor2的默认比例是8:1:1,也是可以调整的。
为了方便对于分区的理解,我们由一个对象的创建到回收进行分析,分区内变化如下:
对象分配过程如下:
过程分析:
(1) 当我们new出一个对象,JVM会首先尝试往栈上分配,如果能够分配得下,就分配到栈上分配到栈上的对象有好处就是不需要GC进行管理,什么时候不需要用到此对象了,将对象出栈就可以了。但是分配到栈上的对象是有要求的:第一,对象比较小,因为栈空间本来就不够大;第二,对象比较简答。
(2) 如果栈上分配不下,我们就判断这个对象是不是够大,如果足够大就直接放在老年代区,在老年代区的对象经过一次全量垃圾回收FGC后,才有可能被回收掉。
(3) 如果如果栈上分配不下并且对象不大,就会判断对象能否被存在线程本地分配缓冲区-TLAB(Thread Local Allocation Buffer)。但是不管放不放得下,都是放在新生代区的伊甸区eden。 但是因为堆是共享的,多个线程可以同时创建对象就可能会争夺同一块内存区域,所以为了保证线程安全,Eden区又被分配成一个个线程本地分配缓冲区,这个TLAB是线程私有的,每个线程都有自己的TLAB,避免了多线程环境下使用同步技术带来的性能损耗。
(4) 伊甸区eden的对象在经过一次GC后,如果被回收掉了,那就结束了生命周期。
(5) 伊甸区eden的对象在经过一次GC后,如果没有被回收掉,JVM在整个new新生代区都采用Copying(拷贝算法),将不是垃圾的对象拷贝到幸存者区survivor1,对比上面的堆内存逻辑分区图。幸存者区survivor1中的对象再经过一次GC后如果对象还存活,那么就拷贝到幸存者区survivor2并且清理掉幸存者区survivor1中的所有对象,再有GC就反复这个操作,直到对象的分代年龄达到了移到老年代的界限(一般分代垃圾回收器默认是15,CMS默认是6),就会被移到老年代中,老年代采用标记压缩(mark compact)算法,保证内存的连续性 。
5. 常见的垃圾回收器
jdk从1.0到14.0一共诞生了10种垃圾回收器,如下图:
分类如下:
(1) 分代模型:Serial,Serial Old,Parallel Scavenge,Parallel Old,ParNew,CMS
(2) 不分代模型:G1(虽然物理模型上没分代,但是逻辑层面上是分代的,jdk1.8及以上的版本建议使用G1,响应时间很快,但是1.8默认是PSPO<Parallel Scavenge和Parallel Old>),ZGC(Oracle官方支持),Shenandoah(小红帽公司开发)
(3) 特殊模型:Epsilon(这种垃圾回收器不回收垃圾,只是跟踪垃圾的产生和回收,但是这个回收只是动作,其实没真正回收。Epsilon有两个用途:<1>用于调试;<2>内存很大,程序很小很快就能运行完成。)
6. 常见垃圾回收器组合参数设定
(1) -XX:+UseSerialGC = Serial New (DefNew) + Serial Old
小型程序默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选中收集器。
(2) -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (jdk1.8默认)【PS+Serial Old】
(3) -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
我们可以用命令行-XX:+PrintCommandLineFlags查看我们所使用的是哪种垃圾回收器,如下图:
三、总结
通过本文,我们了解了GC的基础概念、常用的垃圾回收算法、以及JVM内存分代模型和所有的垃圾回收器的特点,下一文我们将着重讲解不同垃圾回收器所采用的底层算法及原理,请期待《我的JVM(二):十种垃圾回收器所采用的底层算法及原理》。
更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!
http://weixin.qq.com/r/xx3v9_7EY7McraqU90jV (二维码自动识别)