JVM虚拟机为使用者提供了自动内存管理机制,使的程序员在使用完对象后手动释放占用内存的工做中解脱出来。内存的动态分配和回收彻底使得一切都看起来那么美妙,可是再好的机器也有出问题的时候不是。在项目中须要排查各类内存溢出、内存泄漏问题时,就有必要来了解了解JVM内部对内存回收的那些事了。小白由于要在组内作一次JVM垃圾回收的技术分享,因而又再次研读了《深刻理解Java虚拟机》一书中垃圾收集相关章节。实在是感受每看一遍,都有不一样的收获,本文参考虚拟机神书对GC相关知识加以梳理,同时有的地方谈了一些小白本身的理解,有失偏颇,还望指正。java
何为垃圾?数数JVM运行期的内存结构,也就方法区和堆内存两块内存区域是线程共享的,虚拟机栈、程序计数器、本地方法栈都是线程私有的,私有就意味着这部份内存会随线程的结束而释放,所以垃圾回收是无须关注线程私有的内存的。反却是方法区和堆(主要),因为是线程共享的,每一个线程均可以在这块区域写数据。随着线程的结束,这部份内存就会存在大量无用的数据。这些数据就是咱们常说的垃圾,而这些垃圾占用的内存,就是垃圾回收的目标内存。堆内存中的垃圾即是无用、或者称之为死亡的对象,方法区中的垃圾即是无用的常量和类数据。程序员
空间紧张的内存世界,对于对象而言实在太为残酷,能够说毫无人道主义。只要你没什么用了,那么很差意思,法官便要宣判你的死亡了,而后交由刽子手行刑。可是残酷归残酷,法官是有原则的,就是它须要科学的机制来准确的断定你是否无用,由于只有这种原则才能保证法官所在的世界正常运转。算法
对象是否无用的断定算法有以下两种:缓存
引用技术算法:于对象内部维护一计数器,每有一处运用某个对象,该对象的引用计数器便加一,每有一处的引用失效,该对象的引用计数器减一。计数器为0的对象即是无用的,也就是死亡对象。安全
可达性分析算法:选取特定性质的对象做为根对象(GC Roots),像从树的根节点往下遍历同样,从GC Roots向下遍历其引用链,若存在对象到GC Roots怎么都不可达(无任何一条调用链),那么这些对象即可被回收。性能优化
主流的Java虚拟机采用的基本都是可达性分析算法,主要看重即是其不存在对象互相引用没法回收的问题。该算法中存在一个概念GC Roots,虚拟机会遍历这些对象的调用链来肯定其余对象是否存活。那便有一个前提,能够做为GC Roots的对象必须保证是存活的对象。多线程
断定对象是否无用,其实归根究竟是断定对象的引用是否还存在。引用这个概念是比较java特点的词语,能够类比C、C++中的指针去理解。一个引用类型的变量的值,是另外一块内存的起始地址。更为java特点的是,1.2以后,对引用(Reference)进行了具体化的扩充,也就是常说的强、软、弱、虚四种。架构
强引用:就是咱们平常new对象前声明的引用。好比: Object obj = new Object() 。其中obj就是强引用。并发
软引用(SoftReference):通常用来表示能够存在但非必须的对象。这类对象在内存充足时是能够存在的,可是在内存不足即将溢出时,会被回收掉。可以使用 SoftReference 类实例化,构造参数为要引用的对象。适用场景小白觉的应该是一些非必要的缓存数据,好比图片文件的流对象,内存充足时缓存下来,每次使用直接读流,内存紧张时被回收,下次使用再从原路径读取。分布式
弱引用(WeakReference):也是描述非必须的对象。但这个引用关系比软引用更弱,弱引用引用的对象只要发生垃圾回收,便会被回收,可是在发生垃圾回收以前,仍是能够经过若引用获取到该对象的。适用场景和软引用相似。
虚引用(PhantomReference):准确叫幻影引用吧,也就是引用是假的。虚引用和对象的生存周期毫无关系。没法经过虚引用获取到对应对象。惟一的做用就是使这个对象在被回收时收到一个系统通知。能够被实例化,但必须和一个引用队列关联使用。虚拟机在回收这个对象的时候便会把该引用添加进引用队列,程序即可经过监控引用队列来实如今对象回收前进行一些操做。
虚拟机不会简单地经过一次可达性分析就断定某个对象死亡继而进行回收的。一个对象在肯定要回收时至少已经经历了两次断定标记。这里说的每一次标记能够理解为一次可达性判断。虚拟机标记对象的过程以下图(小白根据本身理解的画的图,欢迎讨论):
虚拟机对对象进行第一次标记的时候,对不可达的对象进行筛选,判断是否有必要执行 finalize() 方法。若对象没有覆盖该方法或已经执行过该方法,JVM会认为该对象没有必要执行 finalize()方法。
而有必要的对象,会被放进一个F-Quene队列,由低优先级的Finalizer线程触发这个队列中对象的finalize()方法。稍后,JVM对该队列中的全部对象进行一次小规模(队列中)标记。若是有对象在finalize()方法中拯救了本身,也就是在这个方法中创建了存活对象到 this (本身)的引用链(具体如何拯救能够百度或去书里看代码),这个对象会在此次小规模标记中标记为可达,不然依旧是不可达。
在第二轮标记开始后,JVM会再次断定对象,将被两次及其以上被标记为不可达的对象内存回收,将拯救了本身的对象移出待回收集合。
对于方法区,并不强制要求虚拟机实现这部分的垃圾回收。主要是由于收集效率低,即耗时长、回收空间少。
方法区主要回收废弃常量和无用类。废弃常量的断定与堆内存中对象的断定相同。类是否须要回收是由开发人员决定的,HotSpot虚拟机提供的配置参数为 -Xnoclassgc 。
类的断定取决于下面三个因素:
垃圾由谁来回收,又是怎样回收呢?虚拟机内部提供了适合不一样场景下的垃圾收集器来进行垃圾回收,程序员能够本身设定。这些垃圾收集器在程序运行时就是虚拟机内部的一个线程,须要注意的一点是这个线程是守护线程,它会伴随着咱们程序(主线程)一块儿结束。GC线程在回收垃圾时,是根据特定的收集算法取进行垃圾内存释放的。
上面简单描述了各类算法的基本思想。小白这里梳理各类算法的优缺点及适应场景以下:
标记-清除算法: 标记和清除两个过程效率都不过高 ,在死亡对象特别多的状况下尤其突出。另外收集完成后会形成 内存碎片化严重 ,回收的空间不连续。这两个特色决定了该算法 适合在对象存活周期特别长的状况下使用 ,由于这种状况下每次收集时死亡对象小,在清理时对特定空间的清理就会变少。
复制算法:很明显的缺点是 浪费一半内存 ,但其 简单高效,且回收后内存连续 的优势也很突出。该算法中回收时是清理使用的内存半区,而后切换复制后的内存半区来使用,相比标记-清理算法确定实现简单,运行高效。可是须要注意的是,在对象存活较多的状况下,对应的复制操做就会越多,效率就会越低。所以,复制算法 适合在对象存活周期较短的状况使用 。
标记-整理算法:很好的弥补了标记-清理算法的缺点,回收后空间连续, 无内存碎片化问题。效率上小白感受大多数状况下是比标记-清理算法略微差一些的,这个没有深刻研究,只是推测,自己多了一个移动的步骤,若是效率也好的话,那标记-清除算法就没有必要存在了。也 适用于对象存活周期特别长的状况 。
分代收集算法:集百家之长,通常是 首选 。 堆内存被分为新生代和老年代 。 新生代对象存活周期短,大都朝生夕死,采用复制算法 。HotSpot虚拟机默认按8:1:1的比例将新生代分为Eden区域和两块同样大的Survivor区域,每次使用Eden和一块S区,回收时将存活的对象复制到另外一块S区,回收完成后再使用这块S区和Eden区。这样每次只会闲置10%的新生代空间,对于得到了高效率的结果来讲这个代价还能够接受。 老年代通常存放存活周期长的对象,每次收集对象存活率高,只能使用标记-清除(整理)算法 。注意:新生代中,若收集时存活对象预留的那块S区放不下时,会依赖老年代存放,具体的机制下面会提到。
上面提到了HotSpot虚拟机对堆内存的划分以及收集算法的选用,这里简单梳理下收集算法在新生代和老年代具体实现,也就是各个区域的垃圾收集器。
新生代收集器:Serial、ParNew、Parallel Scavenge、G1
老年代收集器:Serial Old、CMS、Parallel Old、G1
搭配组合使用于整个堆内存的回收,可搭配的方式如图:
各收集器的工做原理这里不罗列了,感兴趣的朋友看下书就知道了,小白只梳理各自的优缺点及适用场景:
须要注意的一点是,上面提到的并行是指GC和应用程序线程并行,并发则指的是多线程回收。
HotSpot虚拟机中GC线程在开始工做时是须要挂起应用程序的全部线程以保证回收操做的准确性的,准确说是保证选择的GC Roots对象和程序当前上下文的一致性。小白画了流程图以下,来更形象地描述GC如何中止工做线程。
图里引入了两个概念,这里简单说一下。安全点是在程序运行的特定位置,记录了该位置的指令执行时内存中可做为GC Roots的引用的内存地址,方便虚拟机直接去具体位置枚举根节点,而不是在整个内存中查找。设置安全点也是避免虚拟机为每条指令都记录引用信息浪费太多空间。安全域是指该区域内的指令不会致使当前内存中的引用发生变化,也就是说线程在安全域执行不会影响GC的准确性。安全域解决了处于某种状态(好比Sleep或是Blocked)线程没法响应JVM中断要求的问题。
了解了什么是垃圾以及如何回收,接下来就简单聊聊虚拟机何时会进行垃圾回收(不会去详细说明内存如何分配以及各类虚拟机参数)。首先须要明确的是,进行垃圾回收会发生STW问题,没法避免,所谓的并行也只是总体看上去是并行的,那么就意味着频繁的垃圾回收会极为影响应用程序的性能,所以垃圾的回收只能发生在必要的时候,也就是可用内存不足觉得对象分配的时候。
HotSpot虚拟机将 堆内存划分为新生代和老年代 。 新生代 又划 分为三块 ,一块较大的 Eden空间 和 两块 较小 的Survivor 空间,默认比例为 8:1:1 。划分的目的是由于HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减小浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代,大小的判别阈值可配置),当Eden区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC 。GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(做为保留区域)。GC进行时,Eden区中全部存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。而后清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,无论怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,须要依赖老年代进行分配担保,将这些对象存放在老年代中。另外有个特殊状况是,在Minor GC后,若是S区有相同年龄的存活对象,且相同年龄的对象占用空间超过了S区的50%,这些对象也会被提早放入老年代。
当有对象放进老年代而最终内存不足时, 老年代 才会进行 Major GC ,其常常伴随至少一次的Minor GC。老年代的GC通常比新生代的GC慢10倍以上。所以通常来讲要尽可能减小虚拟机进行老年代GC。
HotSpot提供的优化措施是 分配担保机制 ,可经过HandlePromotionFailure参数设置是否容许担保失败。通常在进行Minor GC前,这次GC后存活的对象有多少是没法预知的,最坏的状况就是全部对象都存活,那么一块Survivor区域是绝对放不下,这个时候就须要把存活的对象提早放入老年代。可是老年代也没法保证能放下啊,因此绝对安全的状况就是老年代的最大可用的连续空间(不肯定)大于新生代全部对象总空间。分配担保机制就是在非绝对安全的状况下,检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是大于,担保这次Minor GC安全(有风险),若是小于(担保失败)直接Full GC。另外,若是设置不容许担保失败(其实就是关闭担保机制)就意味着每次新生代空间不足都会Full GC。
注意:Full GC到底是哪里的GC众说纷纭,小白这里认为其并不仅仅指老年代GC,而是一次整个堆内存及永久带的GC。可是在去永久带后,也就只是整个堆内存的GC了。
顺便在此给你们推荐一个Java架构方面的交流学习群:698581634,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,主要针对Java开发人员提高本身,突破瓶颈,相信你来学习,会有提高和收获。在这个群里会有你须要的内容 朋友们请抓紧时间加入进来吧。