垃圾收集机制是 Java 的招牌能力,极大的提升了开发效率
。现在,垃圾收集几乎成为了现代语言的标配,即便通过了如此长时间的发展,Java 的垃圾收集机制仍然在不断的演进中,不一样大小的设备、不一样特征的应用场景都对垃圾收集提出了新的挑战,也是面试的热门考点
。web
垃圾是指在运行程序中没有任何指针指向的对象
,这个对象就是须要被回收的垃圾。面试
若是不及时对内存中的垃圾进行清理,那么这些垃圾所占的内存空间会一直保留到应用程序结束,被保留的空间没法被其它对象所使用,甚至可能致使内存溢出
。算法
内存早晚都会被消耗完
。JVM将整理出的内存分配给的新的对象
。没有GC就不能保证应用程序的正常进行
,常常形成 STW 的 GC 又跟不上实际的需求,因此才会不短地尝试对 GC 进行优化。自动内存管理,无需开发人员手动参与内存的分配与回收,下降内存泄漏和内存溢出的风险
。数据库
自动内存管理机制,将开发人员从繁重的内存管理中释放出来,能够更专一与业务开发
。segmentfault
坏处数组
可能会弱化开发人员在程序中出现内存溢出时定位问题和解决问题的能力
。GC 发生的区域缓存
垃圾回收器能够对年轻代回收,也能够对老年代回收,甚至是整个堆和方法区的回收。安全
其中 Java堆是垃圾收集器的工做重点
。服务器
从次数上讲:
垃圾标记阶段:对象存活判断
须要区分出内存中哪些是存活对象,哪些是已经死亡的对象
,只有被标记已经死亡的对象,GC 才会执行垃圾回收时,释放掉其所占内存空间,所以这个过程咱们能够称为垃圾标记阶段。引用计数算法
和可达性分析算法
。引用计数算法
引用计数器属性
,用于记录对象被引用的状况。优势
缺点
存储空间的开销
。时间开销
。没法处理循环引用
的状况。这个问题是致命的,因此致使在 Java 的垃圾回收器中没有使用这类算法。相对于引用计数算法而言,可达性分析算法不只一样具有实现简单和执行高效等特色,更重要的是该算法能够有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生
。
这样类型的垃圾收集一般也叫作追踪性垃圾收集(Tracing Garbage Collection)
。
所谓GC Roots
根集合就是一组必须活跃的引用。
基本思路:
搜索被根对象集合所链接的目标对象是否可达
。引用链(Reference Chain)
。能够做为 GC Roots 的对象有哪些?
对象被销毁以前的自定义处理逻辑
。用于在对象被回收时进行资源释放
。一般在这个方法中进行一些资源释放和清理工做,好比关闭文件、数据库链接等。缘由:
因为 finalize()方法的存在,虚拟机中的对象通常处于三种可能的状态
。
若是从全部的根节点都没法访问到某对象,说明对象已经再也不使用了。通常来讲,此对象须要被回收。但事实上,也并不是是"非死不可"的,这时候它们暂时处于"缓刑"阶段。一个没法触及的对象有可能在某一个条件下"复活"本身
,若是这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。
以下:
可触及的:
从根节点开始,能够到达这个对象。可复活的:
对象的全部引用都被释放,可是对象有可能在 finalize()中复活。不可触及的:
对象的 finalize()被调用,而且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,由于finalize()只会被调用一次
。以上三种状态中,是因为 finalize()方法的存在,进行的区分,只有在对象不可触及时才能够被回收。
具体过程
判断一个对象是否能够回收,至少要经历两次标记过程:
① 若是对象没有重写 finalize()方法,或者 finalize()方法以及被虚拟机调用过,则虚拟机视为"没有必要执行",当前对象断定为不可触及的。
② 若是对象重写了 finalize()方法,且还未执行过,那么当前对象会被插入到 F-Queue 队列中,由一个虚拟机自动建立的、低优先级的 Finalizer 线程触发其 finalize()方法执行。
③ finalize()方法是对象逃脱死亡的最后机会
,稍后 GC 会对 F-Queue 队列中的对象进行第二次标记,若是对象在 finalize()方法终于引用链上的任何一个对象创建了联系,那么在第二次标记时,对象会被移出"即将回收"的集合,以后,若是对象再次出现没有引用存在的状况,finalize()方法不会再次被调用,对象会直接变成不可触及的状态,也就是说,一个对象的 finalize()方法只会被调用一次。
代码示例:
/** * 测试Object类中finalize()方法,即对象的finalization机制。 */ public class CanReliveObj { public static CanReliveObj obj;//类变量,属于 GC Root //finalize方法只能被调用一次 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("调用当前类重写的finalize()方法"); obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj创建了联系。 obj是GC Roots,this是当前对象,也就是要被回收的对象。 } public static void main(String[] args) { try { obj = new CanReliveObj(); // 对象第一次成功拯救本身 obj = null; System.gc();//调用垃圾回收器。第一次GC的时候,会执行finalize方法,在finalize方法中,因为obj指向了this,obj变量是一个GC Root,要被回收的对象与引用链上的对象创建了又联系,因此对象被复活了 System.out.println("第1次 gc"); // 由于Finalizer线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); // 这句会输出 } System.out.println("第2次 gc"); // 下面这段代码与上面的彻底相同,可是此次自救却失败了 obj = null; System.gc(); // 第二次调用GC,因为finalize只能被调用一次,因此对象会直接被回收 // 由于Finalizer线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); // 这句会输出 } else { System.out.println("obj is still alive"); } } catch (InterruptedException e) { e.printStackTrace(); } } }
运行结果:
第1次 gc 调用当前类重写的finalize()方法 obj is still alive 第2次 gc obj is dead
概述
当成功区分出内存中存活对象和死亡对象后, GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在 JVM 中比较常见的三种垃圾收集算法是
标记-清除算法(Mark-Sweep)是一种很是基础和常见的垃圾收集算法,该算法被 J.McCarthy 等人在 1960 年提出并并应用于 Lisp 语言。
执行过程
当堆中的有效内存空间(available memory)被耗尽的时候,就会中止整个程序(也被称为 stop the world),而后进行两项工做,第一项则是标记,第二项则是清除。
标记
: Collector 从引用根节点开始遍历,标记全部被引用的对象,通常是在对象的 Header 中记录为可达对象。清除
: Collector 对堆内存从头至尾进行线性的遍历,若是发现某个对象在其 Header 中没有标记为可达对象,则将其回收。什么是清除?
这里所谓的清除并非真的置空,而是把须要清除的对象地址保存在空闲的地址列表里。下次有新对象须要加载时,判断垃圾的位置空间是否够,若是够,就存放覆盖原有的地址。
关于空闲列表:
若是内存规整
若是内存不规整
缺点
背景
为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky 于 1963 年发表了著名的论文,“使用双存储区的 Lisp 语言垃圾收集器 CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky 在该论文中描述的算法被人们称为复制(Copying)算法,它也被 M.L.Minsky 本人成功地引入到了 Lisp 语言的一个实现版本中。
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,以后清除正在使用的内存块中的全部对象,交换两个内存的角色,最后完成垃圾回收。
优势
缺点
注意
若是系统中的存活对象不少,那么复制算法的效率就会大打折扣。理想状态下须要复制的存活对象数量并不会太大,或者说很是低才行。
在新生代,对常规应用的垃圾回收,一次一般能够回收 70% - 99% 的内存空间。回收性价比很高。因此如今的商业虚拟机都是用这种收集算法回收新生代。
背景
复制算法的高效性是创建在存活对象少、垃圾对象多的前提下的。这种状况在新生代常常发生,可是在老年代,更常见的状况是大部分对象都是存活对象。若是依然使用复制算法,因为存活对象较多,复制的成本也将很高。所以,基于老年代垃圾回收的特性,须要使用其余的算法。
标记一清除算法的确能够应用在老年代中,可是该算法不只执行效率低下,并且在执行完内存回收后还会产生内存碎片,因此 JvM 的设计者须要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。
1970 年先后,G.L.Steele、C.J.Chene 和 D.s.Wise 等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
执行过程
第一阶段和标记清除算法同样,从根节点开始标记全部被引用对象。
第二阶段将全部的存活对象压缩到内存的一端,按顺序排放。以后,清理边界外全部的空间。
标记-清除和标记-压缩的区别
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,所以,也能够把它称为标记-清除-压缩(Mark-Sweep-Compact)算法
。
两者的本质差别在于标记-清除算法是一种非移动式
的回收算法,标记-压缩是移动式
的。是否移动回收后的存活对象是一项优缺点并存的风险决策。能够看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当咱们须要给新对象分配内存时,JVM 只须要持有一个内存的起始地址便可,这比维护一个空闲列表显然少了许多开销。
优势
缺点
标记清除 | 标记压缩 | 复制 | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 一般须要活对象的 2 倍空间(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
效率上来讲,复制算法是当之无愧的老大,可是却浪费了太多内存。
而为了尽可能兼顾上面提到的三个指标,标记-整理算法相对来讲更平滑一些,可是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
前面全部这些算法中,并无一种算法能够彻底替代其余算法,它们都具备本身独特的优点和特色。分代收集算法应运而生。
分代收集算法,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率
。通常是把 Java 堆分为新生代和老年代,这样就能够根据各个年代的特色使用不一样的回收算法,以提升垃圾回收的效率。
在 Java 程序运行的过程当中,会产生大量的对象,其中有些对象是与业务信息相关,好比Http请求中的Session对象、线程、Socket链接
,这类对象跟业务直接挂钩,所以生命周期比较长。可是还有一些对象,主要是程序运行过程当中生成的临时变量,这些对象生命周期会比较短,好比:String对象
,因为其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次便可回收。
目前几乎全部的GC都采用分代收集算法执行垃圾回收的。
在 HotSpot 中,基于分代的概念,GC 所使用的内存回收算法必须结合年轻代和老年代各自的特色。
年轻代特色:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种状况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,所以很适用于年轻代的回收。而复制算法内存利用率不高的问题,经过 hotspot 中的两个 survivor 的设计获得缓解。
老年代特色:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种状况存在大量存活率高的对象,复制算法明显变得不合适。通常是由标记-清除或者是标记-清除与标记-整理的混合实现。
以 HotSpot 中的 CMS 回收器为例,CMS 是基于 Mark-Sweep 实现的,对于对象的回收效率很高。而对于碎片问题,CMS 采用基于 Mark-Compact 算法的 Serial old 回收器做为补偿措施:当内存回收不佳(碎片致使的 Concurrent Mode Failure 时),将采用 serial old 执行 FullGC 以达到对老年代内存的整理。
分代的思想被现有的虚拟机普遍使用。几乎全部的垃圾回收器都区分新生代和老年代.
概述
上述现有的算法,在垃圾回收过程当中,应用软件将处于一种stop the world
的状态。在 stop the world 状态下,应用程序全部的线程都会挂起,暂停一切正常的工做,等待垃圾回收的完成。若是垃圾回收时间过长,应用程序会被挂起好久,将严重影响用户体验或者系统的稳定性
。为了解决这个问题,即对实时垃圾收集算法的研究直接致使了增量收集(Incremental Collecting)算法的诞生。
若是一次性将全部的垃圾进行处理,须要形成系统长时间的停顿,那么就可让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来讲,增量收集算法的基础还是传统的标记-清除和复制算法。增量收集算法经过对线程间冲突的妥善处理,容许垃圾收集线程以分阶段的方式完成标记、清理或复制工做
。
缺点
使用这种方式,因为在垃圾回收过程当中,间断性地还执行了应用程序代码,因此能减小系统的停顿时间。可是,由于线程切换和上下文转换的消耗,会使得垃圾回收的整体成本上升,形成系统吞吐量的降低
。
通常来讲,在相同条件下,堆空间越大,一次 Gc 时所须要的时间就越长,有关 GC 产生的停顿也越长。为了更好地控制 GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减小一次 GC 所产生的停顿。
分代算法将按照对象的生命周期长短划分红两个部分,分区算法将整个堆空间划分红连续的不一样小区间。 每个小区间都独立使用,独立回收。这种算法的好处是能够控制一次回收多少个小区间。
在默认状况下,经过 system.gc()或者 Runtime.getRuntime().gc() 的调用,会显式触发FullGC
,同时对老年代和新生代进行回收,尝试
释放被丢弃对象占用的内存。
然而 system.gc() 调用附带一个免责声明,没法保证对垃圾收集器的调用。(不能确保当即生效)
JVM 实现者能够经过 system.gc() 调用来决定 JVM 的 GC 行为。而通常状况下,垃圾回收应该是自动进行的,无须手动触发,不然就太过于麻烦了。在一些特殊状况下,如咱们正在编写一个性能基准,咱们能够在运行之间调用 System.gc()。
代码演示:
public class SystemGCTest { public static void main(String[] args) { new SystemGCTest(); // 提醒JVM进行垃圾回收 System.gc(); //强制调用使用引用的对象的finalize()方法 //System.runFinalization(); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("执行了 finalize()方法"); } }
运行结果不必定会触发销毁的方法,调用 System.runFinalization()会强制调用 失去引用对象的 finalize()。
手动 GC 来理解不可达对象的回收
public class LocalVarGC { /** * 触发Minor GC没有回收对象,而后在触发Full GC将该对象存入old区 */ public void localvarGC1() { byte[] buffer = new byte[10*1024*1024]; System.gc(); } /** * 触发YoungGC的时候,已经被回收了 */ public void localvarGC2() { byte[] buffer = new byte[10*1024*1024]; buffer = null; System.gc(); } /** * 不会被回收,由于它还存放在局部变量表索引为1的槽中 */ public void localvarGC3() { { byte[] buffer = new byte[10*1024*1024]; } System.gc(); } /** * 会被回收,由于它还存放在局部变量表索引为1的槽中,可是后面定义的value把这个槽给替换了 */ public void localvarGC4() { { byte[] buffer = new byte[10*1024*1024]; } int value = 10; System.gc(); } /** * localvarGC5中的数组已经被回收 */ public void localvarGC5() { localvarGC1(); System.gc(); } public static void main(String[] args) { LocalVarGC localVarGC = new LocalVarGC(); localVarGC.localvarGC3(); } }
内存溢出相对于内存泄漏来讲,尽管更容易被理解,可是一样的,内存溢出也是引起程序崩溃的罪魁祸首之一。
因为 GC 一直在发展,全部通常状况下,除非应用程序占用的内存增加速度很是快,形成垃圾回收已经跟不上内存消耗的速度,不然不太容易出现 OOM 的状况。
大多数状况下,GC 会进行各类年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Fu11GC 操做,这时候会回收大量的内存,供应用程序继续使用。
javadoc 中对 outofMemoryError 的解释是,没有空闲内存,而且垃圾收集器也没法提供更多内存
。
首先说没有空闲内存的状况:说明 Java 虚拟机的堆内存不够。缘由以下:
好比:可能存在内存泄漏问题;也颇有可能就是堆的大小不合理,好比咱们要处理比较可观的数据量,可是没有显式指定 JVM 堆大小或者指定数值偏小。咱们能够经过参数-Xms 、-Xmx 来调整。
对于老版本的 oracle JDK,由于永久代的大小是有限的,而且 JVM 对永久代垃圾回收(如,常量池回收、卸载再也不须要的类型)很是不积极,因此当咱们不断添加新类型的时候,永久代出现 OutOfMemoryError 也很是多见,尤为是在运行时存在大量动态类型生成的场合;相似 intern 字符串缓存占用太多空间,也会致使 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space"。
随着元数据区的引入,方法区内存已经再也不那么窘迫,因此相应的 OOM 有所改观,出现 OOM 的异常信息则变成了:“java.lang.OutOfMemoryError:Metaspace"。直接内存不足,也会致使 OOM。
这里面隐含着一层意思是,在抛出 OutofMemoryError 以前,一般垃圾收集器会被触发,尽其所能去清理出空间。
例如:在引用机制分析中,涉及到 JVM 会去尝试回收软引用指向的对象等。在 java.nio.BIts.reserveMemory()方法中,咱们能清楚的看到,System.gc()会被调用,以清理空间。
固然,也不是在任何状况下垃圾收集器都会被触发的。好比,咱们去分配一个超大对象,相似一个超大数组超过堆的最大值,JVM 能够判断出垃圾收集并不能解决这个问题,因此直接抛出 OutOfMemoryError。
严格来讲,只有对象不会再被程序用到了,可是GC又不能回收他们的状况,才叫内存泄漏。
但实际状况不少时候一些不太好的实践(或疏忽)会致使对象的生命周期变得很长甚至致使 00M,也能够叫作宽泛意义上的“内存泄漏”。
尽管内存泄漏并不会马上引发程序崩溃,可是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽全部内存,最终出现 OutOfMemoryError,致使程序崩溃。
举例
单例的生命周期和应用程序是同样长的,因此单例程序中,若是持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会致使内存泄漏的产生。
数据库链接(dataSourse.getConnection() ),网络链接(socket)和 io 链接必须手动 close,不然是不能被回收的。
Stop-The-World,简称 STW,指的是 GC 事件发生过程当中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应
,有点像卡死的感受,这个停顿称为 STW。
可达性分析算法中枚举根节点(GC Roots)会致使全部 Java 执行线程停顿。
被 STW 中断的应用程序线程会在完成 GC 以后恢复,频繁中断会让用户感受像是网速不快形成电影卡带同样,因此咱们须要减小 STW 的发生。
STW 事件和采用哪款 GC 无关,全部的 GC 都有这个事件。
哪怕是 G1 也不能彻底避免 Stop-the-world 状况发生,只能说垃圾回收器愈来愈优秀,回收效率愈来愈高,尽量地缩短了暂停时间。
STW 是 JVM 在后台自动发起和自动完成的
。在用户不可见的状况下,把用户正常的工做线程所有停掉。
开发中不要用 System.gc(); 会致使 Stop-The-World 的发生。
并发
在操做系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
并发不是真正意义上的“同时进行”,只是 CPU 把一个时间段划分红几个时间片断(时间区间),而后在这几个时间区间之间来回切换,因为 CPU 处理的速度很是快,只要时间间隔处理得当,便可让用户感受是多个应用程序同时在进行。
并行
当系统有一个以上 CPU 时,当一个 CPU 执行一个进程时,另外一个 CPU 能够执行另外一个进程,两个进程互不抢占 CPU 资源,能够同时进行,咱们称之为并行(Parallel)。
其实决定并行的因素不是 CPU 的数量,而是 CPU 的核心数量,好比一个 CPU 多个核也能够并行。
适合科学计算,后台处理等弱交互场景。
并发和并行对比
并发
,指的是多个事情,在同一时间段内同时发生了。
并行
,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的。
只有在多 CPU 或者一个 CPU 多核的状况中,才会发生并行。
不然,看似同时发生的事情,其实都是并发执行的。
并发和并行,在谈论垃圾收集器的上下文语境中,它们能够解释以下:
并行(Parallel)
多条垃圾收集线程并行工做
,但此时用户线程仍处于等待状态。如 ParNew、Parallel Scavenge、Parallel old;串行(Serial)
并发(Concurrent)
指用户线程与垃圾收集线程同时执行
(但不必定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。>用户程序在继续运行,而垃圾收集程序线程运行于另外一个 CPU 上;
如:CMS、G1
安全点(Safepoint)
程序执行时并不是在全部地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来开始 GC,这些位置称为“安全点(Safepoint)”。
Safe Point 的选择很重要,若是太少可能致使GC等待的时间太长,若是太频繁可能致使运行时的性能问题
。大部分指令的执行时间都很是短暂,一般会根据“是否具备让程序长时间执行的特征”
为标准。好比:选择一些执行时间较长的指令做为 Safe Point,如方法调用、循环跳转和异常跳转
等。
如何在 cc 发生时,检查全部线程都跑到最近的安全点停顿下来呢?
安全区域(Safe Region)
Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint。可是,程序“不执行”的时候呢?例如线程处于 sleep 状态或 Blocked 状态,这时候线程没法响应 JVM 的中断请求,“走”到安全点去中断挂起,JVM 也不太可能等待线程被唤醒。对于这种状况,就须要安全区域(Safe Region)来解决。
安全区域是指在一段代码片断中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 Gc 都是安全的
。咱们也能够把 Safe Region 看作是被扩展了的 Safepoint。
执行流程:
再谈引用
咱们但愿能描述这样一类对象:当内存空间还足够时,则能保留在内存中;若是内存空间在进行垃圾收集后仍是很紧张,则能够抛弃这些对象。
【既偏门
又很是高频
的面试题】强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么? 在 JDK1.2 版以后,Java 对引用的概念进行了扩充,将引用分为:
这4种引用强度依次逐渐减弱
。除强引用外,其余 3 种引用都可以在 java.lang.ref 包中找到它们的身影。以下图,显示了这 3 种引用类型对应的类,开发人员能够在应用程序中直接使用它们。
1.强引用(StrongReference)
最广泛的一种引用方式,如 String s = "abc",变量 s 就是字符串“abc”的强引用,只要强引用存在,则垃圾回收器就不会回收这个对象。
2.软引用(SoftReference)
用于描述还有用但非必须的对象,若是内存足够,不回收,若是内存不足,则回收。通常用于实现内存敏感的高速缓存,软引用能够和引用队列 ReferenceQueue 联合使用,若是软引用的对象被垃圾回收,JVM 就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(WeakReference)
弱引用和软引用大体相同,弱引用与软引用的区别在于:只具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程当中,一旦发现了只具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。
4.虚引用(PhantomReference)
就是形同虚设,与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,若是发现它还有虚引,就会在回收对象的内存以前,把这个虚引用加入到与之关联的引用队列中。
强引用(Strong Reference)
在 Java 程序中,最多见的引用类型是强引用(普通系统99%以上都是强引用)
,也就是咱们最多见的普通对象引用,也是默认的引用类型
。
当在 Java 语言中使用 new 操做符建立一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
对于一个普通的对象,若是没有其余的引用关系,只要超过了引用的做用域或者显式地将相应(强)引用赋值为 nu11,就是能够当作垃圾被收集了,固然具体回收时机仍是要看垃圾收集策略。
相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在必定条件下,都是能够被回收的。因此,强引用是形成Java内存泄漏的主要缘由之一
。
强引用的案例说明
StringBuffer str = new StringBuffer("hello world");
局部变量 str 指向 stringBuffer 实例所在堆空间,经过 str 能够操做该实例,那么 str 就是 stringBuffer 实例的强引用。
对应内存结构:
若是此时,再运行一个赋值语句
StringBuffer str1 = str;
对应的内存结构为:
那么咱们将 str = null; 则原来堆中的对象也不会被回收,由于还有其它对象指向该区域。
总结
本例中的两个引用,都是强引用,强引用具有如下特色:
软引用(Soft Reference)
不足即回收
软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收
,若是此次回收尚未足够的内存,才会抛出内存溢出异常。
第一次回收是不可达的对象。
软引用一般用来实现内存敏感的缓存。好比:高速缓存
就有用到软引用。若是还有空闲内存,就能够暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。
相似弱引用,只不过 Java 虚拟机会尽可能让软引用的存活时间长一些,无可奈何才清理。
在 JDK1.2 版以后提供了 SoftReference 类来实现软引用
// 声明强引用 Object obj = new Object(); // 建立一个软引用 SoftReference<Object> sf = new SoftReference<>(obj); obj = null; //销毁强引用
弱引用(Weak Reference)
发现即回收
弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止
。在系统 GC 时,只要发现弱引用,无论系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
可是,因为垃圾回收器的线程一般优先级很低,所以,并不必定能很快地发现持有弱引用的对象。在这种状况下,弱引用对象能够存在较长的时间
。
弱引用和软引用同样,在构造弱引用时,也能够指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,经过这个队列能够跟踪对象的回收状况。
软引用、弱引用都很是适合来保存那些无关紧要的缓存数据
。若是这么作,当系统内存不足时,这些缓存数据会被回收,不会致使内存溢出。而当内存资源充足时,这些缓存数据又能够存在至关长的时间,从而起到加速系统的做用。
在 JDK1.2 版以后提供了 WeakReference 类来实现弱引用
// 声明强引用 Object obj = new Object(); // 建立一个弱引用 WeakReference<Object> sf = new WeakReference<>(obj); obj = null; //销毁强引用
弱引用对象与软引用对象的最大不一样就在于,当 GC 在进行回收时,须要经过算法检查是否回收软引用对象,而对于弱引用对象,GC 老是进行回收。弱引用对象更容易、更快被GC回收
。
虚引用(Phantom Reference)
对象回收跟踪
也称为“幽灵引用”或者“幻影引用”,是全部引用类型中最弱的一个。
一个对象是否有虚引用的存在,彻底不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它和没有引用几乎是同样的,随时均可能被垃圾回收器回收。
它不能单独使用,也没法经过虚引用来获取被引用的对象。当试图经过虚引用的 get()方法取得对象时,老是 null。
为一个对象设置虚引用关联的惟一目的在于跟踪垃圾回收过程。好比:能在这个对象被收集器回收时收到一个系统通知
。
虚引用必须和引用队列一块儿使用。虚引用在建立时必须提供一个引用队列做为参数。当垃圾回收器准备回收一个对象时,若是发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收状况。
因为虚引用能够跟踪对象的回收时间,所以,也能够将一些资源释放操做放置在虚引用中执行和记录。
虚引用没法获取到咱们的数据
在 JDK1.2 版以后提供了 PhantomReference 类来实现虚引用。
// 声明强引用 Object obj = new Object(); // 声明引用队列 ReferenceQueue phantomQueue = new ReferenceQueue(); // 声明虚引用(还须要传入引用队列) PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue); obj = null;
咱们使用一个案例,来结合虚引用,引用队列,finalize() 方法进行讲解:
public class PhantomReferenceTest { // 当前类对象的声明 public static PhantomReferenceTest obj; // 引用队列 static ReferenceQueue<PhantomReferenceTest> phantomQueue = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("调用当前类的finalize方法"); obj = this; } public static void main(String[] args) { Thread thread = new Thread(() -> { while (true) { if (phantomQueue != null) { PhantomReference<PhantomReferenceTest> objt = null; try { objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove(); } catch (Exception e) { e.getStackTrace(); } if (objt != null) { System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了"); } } } }, "t1"); thread.setDaemon(true); thread.start(); phantomQueue = new ReferenceQueue<>(); obj = new PhantomReferenceTest(); // 构造了PhantomReferenceTest对象的虚引用,并指定了引用队列 PhantomReference<PhantomReferenceTest> phantomReference = new PhantomReference<>(obj, phantomQueue); try { System.out.println(phantomReference.get()); // 去除强引用 obj = null; // 第一次进行GC,因为对象可复活,GC没法回收该对象 System.out.println("第一次GC操做"); System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } System.out.println("第二次GC操做"); obj = null; System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } } catch (Exception e) { e.printStackTrace(); } finally { } } }
运行结果:
null 第一次GC操做 调用当前类的finalize方法 obj 可用 第二次GC操做 追踪垃圾回收过程:PhantomReferenceTest实例被GC了 obj 是 null
终结器引用
它用于实现对象的 finalize() 方法,也能够称为终结器引用。
无需手动编码,其内部配合引用队列使用。
在 GC 时,终结器引用入队。由 Finalizer 线程经过终结器引用找到被引用对象调用它的 finalize()方法,第二次 GC 时才回收被引用的对象。
垃圾回收器分类
按线程数
分,可用分为串行垃圾回收器和并行垃圾回收器。
串行回收指的是在同一时间段内只容许有一个 CPU 用于执行垃圾回收操做,此时工做线程被暂停,直至垃圾收集工做结束。
串行回收默认被应用在客户端的Client模式下的JVM中
。和串行回收相反,并行收集能够运用多个 CPU 同时执行垃圾回收,所以提高了应用的吞吐量,不过并行回收仍然与串行回收同样,采用独占式,使用了“Stop-The-World”机制。
按照工做模式
分,能够分为并发式垃圾回收器和独占式垃圾回收器。
按碎片处理方式
分,可分为压缩武垃圾回收器和非压缩式垃圾回收器。
按工做的内存区间
分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。三者整体的表现会随着技术进步而愈来愈好。一款优秀的收集器一般最多同时知足其中的两项。
这三项里,暂停时间的重要性日益凸显。由于随着硬件发展,内存占用多些愈来愈能容忍,硬件性能的提高也有助于下降收集器运行时对应用程序的影响,即提升了吞吐量。而内存的扩大,对延迟反而带来负面效果。 简单来讲,主要抓住两点:
吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)。
好比:虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。
这种状况下,应用程序能容忍较高的暂停时间,所以,高吞吐量的应用程序有更长的时间基准,快速响应是没必要考虑的。
吞吐量优先,意味着在单位时间内,STW 的时间最短:0.2+0.2=0.4
“暂停时间”是指一个时间段内应用程序线程暂停,让 GC 线程执行的状态。
例如,GC 期间 100 毫秒的暂停时间意味着在这 100 毫秒期间内没有应用程序线程是活动的。暂停时间优先,意味着尽量让单次 STW 的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5
吞吐量 VS 暂停时间
有时候甚至短暂的200毫秒暂停均可能打断终端用户体验
。所以,具备低的较大暂停时间是很是重要的,特别是对于一个交互式应用程序
。不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾)。
只能频繁地执行内存回收
,但这又引发了年轻代内存的缩减和致使程序吞吐量的降低。在设计(或使用)GC 算法时,咱们必须肯定咱们的目标:一个 GC 算法只可能针对两个目标之一(即只专一于较大吞吐量或最小暂停时间),或尝试找到一个两者的折衷。
如今标准:在最大吞吐量优先的状况下,下降停顿时间
。
7 种经典的垃圾收集器
7 种经典的垃圾收集器与垃圾分代之间的关系
垃圾收集器的组合关系
为何要有不少收集器,一个不够吗?由于 Java 的使用场景不少,移动端,服务器等。因此就须要针对不一样的场景,提供不一样的垃圾收集器,提升垃圾收集的性能。
虽然咱们会对各个收集器进行比较,但并不是为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。因此咱们选择的只是对具体应用最合适的收集器。
-XX:+PrintcommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID
Serial收集器采用复制算法、串行回收和“Stop-The-World”机制的方式执行内存回收
。Serial Old收集器也采用了串行回收和“Stop-The-World”机制,只不过内存回收算法使用的是标记-压缩算法
。Serial Old 在 server 模式下主要由两个用途:
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不只仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工做,更重要的是在它进行垃圾收集时,必须暂停其余全部的工做线程,直到它收集结束(Stop The World)
。
优点:简单而高效(与其余收集器的单线程比),对于限定单个 CPU 的环境来讲,Serial 收集器因为没有线程交互的开销,专心作垃圾收集天然能够得到最高的单线程收集效率。
运行在 client 模式下的虚拟机是个不错的选择。
在用户的桌面应用场景中,可用内存通常不大(几十 MB 至一两百 MB),能够在较短期内完成垃圾收集(几十 ms 至一百多 ms),只要不频繁发生,使用串行回收器是能够接受的。
在 HotSpot 虚拟机中,使用-XX:+UseSerialGC 参数能够指定年轻代和老年代都使用串行收集器。
等价于新生代用 Serial GC,且老年代用 Serial old GC。
总结
这种垃圾收集器你们了解,如今已经不用串行的了。并且在限定单核 cpu 才能够用。如今都不是单核的了。
对于交互较强的应用而言,这种垃圾收集器是不能接受的。通常在 Java web 应用程序中是不会采用串行垃圾收集器的。
若是说 serialGC 是年轻代中的单线程垃圾收集器,那么 ParNew 收集器则是 serial 收集器的多线程版本。
Par 是 Parallel 的缩写,New:只能处理的是新生代。
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew 收集器在年轻代中一样也是采用复制算法、"Stop-The-World"机制
。
ParNew 是不少 JVM 运行在 Server 模式下新生代的默认垃圾收集器。
目前只有 ParNew GC 能与 CMS 收集器配合工做
在程序中,开发人员能够经过选项"-XX:+UseParNewGC"手动指定使用 ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads 限制线程数量,默认开启和 CPU 数据相同的线程数。
HotSpot 的年轻代中除了拥有 ParNew 收集器是基于并行回收的之外,Parallel Scavenge 收集器一样也采用了复制算法、并行回收和"Stop The World"机制
。
那么 Parallel 收集器的出现是否画蛇添足?
可控制的吞吐量(Throughput)
,它也被称为吞吐量优先的垃圾收集器。高吞吐量则能够高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务
。所以,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
Parallel 收集器在 JDK1.6 时提供了用于执行老年代垃圾收集的 Parallel Old 收集器,用来代替老年代的 Serial Old 收集器。
Parallel Old 收集器采用了标记-压缩算法
,但一样也是基于并行回收和"Stop-The-World"机制
。
在程序吞吐量优先的应用场景中,Parallel 收集器和 Parallel Old 收集器的组合,在 server 模式下的内存回收性能很不错。在 Java8 中,默认是此垃圾收集器。
参数配置
-XX:+UseParallelGC
手动指定年轻代使用 Parallel 并行收集器执行内存回收任务。-XX:+UseParallelOldGC
手动指定老年代使用并行回收收集器。
-XX:ParallelGCThreads
设置年轻代并行收集器的线程数。通常最好与 CPU 数量相等,以免过多的线程数影响垃圾收集性能。
-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间(即 STW 的时间)。单位是毫秒。
须要谨慎使用该参数
。-XX:GCTimeRatio
垃圾收集时间占总时间的比例(= 1 / (N + 1))。
-XX:+UseAda[tiveSizePolicy
设置 Parallel Scavenge 收集器具备自适应调节策略
在 JDK1.5 时期,Hotspot 推出了一款在强交互应用
中几乎可认为有划时代意义的垃圾收集器:cMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工做
。
CMS 收集器的关注点是尽量缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提高用户体验。
目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤为重视服务的响应速度,但愿系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就很是符合这类应用的需求。
CMS 的垃圾收集算法采用标记-清除算法
,而且也会"Stop-The-World"。
不幸的是,CMS 做为老年代的收集器,却没法与 JDK1.4.0 中已经存在的新生代收集器 Parallel Scavenge 配合工做,因此在 JDK1.5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个。
在 G1 出现以前,CMS 使用仍是很是普遍的。一直到今天,仍然有不少系统使用 CMS GC。
CMS 整个过程比以前的收集器要复杂,整个过程分为 4 个主要阶段,即初始标记阶段、并发标记阶段、从新标记阶段和并发清除阶段。(涉及 STW 的阶段主要是:初始标记 和 从新标记)
初始标记
(Initial-Mark)阶段:在这个阶段中,程序中全部的工做线程都将会由于"Stop-The-World"机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记GC Roots能直接关联到的对象
。一旦标记完成以后就会恢复以前被暂停的全部应用线程,因为直接关联对象比较小,因此这个阶段速度很是快
。并发标记
(Concurrent-Mark)阶段:从 GC Roots 的直接关联对象开始遍历整个对象图的过程
,这个过程耗时较长
可是不须要停顿用户线程
,能够与垃圾收集线程一块儿并发运行。从新标记
(Remark)阶段:因为在并发标记阶段中,程序的工做线程会和垃圾收集线程同时运行或者交叉运行,所以为了修正并发标记期间,因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录
,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。并发清除
(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间
。因为不须要移动存活对象,因此这个阶段也是能够与用户线程同时并发的。尽管 CMS 收集器采用的是并发回收(非独占式),可是在其初始化标记和再次标记这两个阶段中仍然须要执行“Stop-the-World”机制
暂停程序中的工做线程,不过暂停时间并不会太长,所以能够说明目前全部的垃圾收集器都作不到彻底不须要“Stop-The-World”,只是尽量地缩短暂停时间。
因为最耗费时间的并发标记与并发清除阶段都不须要暂停工做,因此总体的回收是低停顿的。
另外,因为在垃圾收集阶段用户线程没有中断,因此在CMS回收过程当中,还应该确保应用程序用户线程有足够的内存可用
。所以,CMS 收集器不能像其余收集器那样等到老年代几乎彻底被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收
,以确保应用程序在 CMS 工做过程当中依然有足够的空间支持应用程序运行。要是 CMS 运行期间预留的内存没法知足程序须要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来从新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS 收集器的垃圾收集算法采用的是标记清除算法
,这意味着每次执行完内存回收后,因为被执行内存回收的无用对象所占用的内存空间极有多是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么 CMS 在为新对象分配内存空间时,将没法使用指针碰撞(Bump the Pointer)技术,而只可以选择空闲列表(Free List)执行内存分配。
CMS 为何不使用标记压缩算法?
答案其实很简答,由于当并发清除的时候,用 Compact 整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响。Mark Compact 更适合“Stop The World” 这种场景下使用。
优势
缺点
会产生内存碎片
,致使并发清除后,用户线程可用的空间不足。在没法分配大对象的状况下,不得不提早触发 FullGC。CMS收集器对CPU资源很是敏感
。在并发阶段,它虽然不会致使用户停顿,可是会由于占用了一部分线程而致使应用程序变慢,总吞吐量会下降。CMS收集器没法处理浮动垃圾
。可能出现“Concurrent Mode Failure"失败而致使另外一次 Full GC 的产生。在并发标记阶段因为程序的工做线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段若是产生新的垃圾对象,CMS 将没法对这些垃圾对象进行标记,最终会致使这些新产生的垃圾对象没有被及时回收
,从而只能在下一次执行 GC 时释放这些以前未被回收的内存空间。参数配置
-XX:+UseConcMarkSweepGC
手动指定使用 CMS 收集器执行内存回收任务。开启该参数后会自动将-XX:+UseParNewGC 打开。即:ParNew(Young 区用)+CMS(Old 区用)+Serial Old 的组合。-XX:CMSInitiatingoccupanyFraction
设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。JDK5 及之前版本的默认值为 68,即当老年代的空间使用率达到 68%时,会执行一次 CMS 回收。JDK6 及以上版本默认值为 92%
若是内存增加缓慢,则能够设置一个稍大的值,大的阀值能够有效下降 CMS 的触发频率,减小老年代回收的次数能够较为明显地改善应用程序性能。反之,若是应用程序内存使用率增加很快,则应该下降这个阈值,以免频繁触发老年代串行收集器。所以经过该选项即可以有效下降 Full GC 的执行次数
。
-XX:+UseCMSCompactAtFullCollection
用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生。不过因为内存压缩整理过程没法并发执行,所带来的问题就是停顿时间变得更长了。-XX:CMSFullGCsBeforecompaction
设置在执行多少次 Full GC 后对内存空间进行压缩整理。-XX:ParallelCMSThreads
设置 CMS 的线程数量。CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会很是糟糕。
小结
HotSpot 有这么多的垃圾回收器,那么若是有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 有什么不一样呢?
请记住如下口令:
既然咱们已经有了前面几个强大的 GC,为何还要发布 Garbage First(G1)?
缘由就在于应用程序所应对的业务愈来愈庞大、复杂,用户愈来愈多,没有 GC 就不能保证应用程序正常进行,而常常形成 STW 的 GC 又跟不上实际的需求,因此才会不断地尝试对 GC 进行优化。G1(Garbage-First)垃圾回收器是在 Java7 update4 以后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。
与此同时,为了适应如今不断扩大的内存和不断增长的处理器数量
,进一步下降暂停时间(pause time),同时兼顾良好的吞吐量。
官方给 G1 设定的目标是在延迟可控的状况下得到尽量高的吞吐量,因此才担当起“全功能收集器”的重任与指望。
为何名字叫 Garbage First(G1)呢?
由于 G1 是一个并行回收器,它把堆内存分割为不少不相关的区域(Region)(物理上不连续的)。使用不一样的 Region 来表示 Eden、幸存者 0 区,幸存者 1 区,老年代等。
G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的 Region。
因为这种方式的侧重点在于回收垃圾最大量的区间(Region),因此咱们给 G1 一个名字:垃圾优先(Garbage First)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核 CPU 及大容量内存的机器
,以极高几率知足 GC 停顿时间的同时,还兼具高吞吐量的性能特征。
在 JDK1.7 版本正式启用,移除了 Experimental 的标识,是 JDK9 之后的默认垃圾回收器
,取代了 CMS 回收器以及 Parallel+Parallel Old 组合。被 oracle 官方称为“全功能的垃圾收集器
”。
与此同时,CMS 已经在 JDK9 中被标记为废弃(deprecated)。在 jdk8 中还不是默认的垃圾回收器,须要使用-XX:+UseG1GC 来启用。
G1 垃圾收集器的优势
与其余 GC 收集器相比,G1 使用了全新的分区算法,其特色以下所示:
并行与并发
分代收集
G1依然属于分代型垃圾回收器
,它会气氛年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区,但从堆结构上看,它不要求整个 Eden 区、年轻代、或者老年代都是连续的,也再也不坚持固定大小和固定数量。堆空间分为若干个区域(Region)
,这些区域中包含了逻辑上的年轻代和老年代。兼顾年轻代和老年代
。对比其它收集器,或者工做在年轻代,或者工做在老年代。G1 的分代,已经不是下面这样的了
而是以下图这样的一个区域
空间整合
Region之间使用的是复制算法
,但总体上实际可看做是标记-压缩(Mark-Compact)算法
,两种算法均可以免内存碎片,这种特性有利于程序长时间运行,分配大对象时不会由于没法找到连续内存空间而提早触发 GC,尤为是当 Java 堆很是大的时候,G1 的优点更加明显。可预测的停顿时间模型(即:软实时 soft real-time)
这是 G1 相对于 CMS 的另外一大优点,G1 除了追求低停顿外,还能创建可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片断内,消耗在垃圾收集上的时间不得超过 N 毫秒。
每次根据容许的收集时间,优先回收价值最大的 Region
。保证了 G1 收集器在有限的时间内能够获取尽量高的收集效率。G1 垃圾收集器的缺点
相较于 CMS,G1 还不具有全方位、压倒性优点。好比在用户程序运行过程当中,G1 不管是为了垃圾收集产生的内存占用(Footprint)仍是程序运行时的额外执行负载(overload)都要比 CMS 要高。
从经验上来讲,在小内存应用上 CMS 的表现大几率会优于 G1,而 G1 在大内存应用上则发挥其优点。平衡点在 6-8GB 之间。
G1 参数设置
-XX:+UseG1GC
:手动指定使用 G1 垃圾收集器执行内存回收任务。-XX:G1HeapRegionSize
设置每一个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域。默认是堆内存的 1/2000。-XX:MaxGCPauseMillis
设置指望达到的最大 Gc 停顿时间指标(JVM 会尽力实现,但不保证达到)。默认值是 200ms。-XX:+ParallelGcThread
设置 STW 工做线程数的值。最多设置为 8。-XX:ConcGCThreads
设置并发标记的线程数。将 n 设置为并行垃圾回收线程数(ParallelGcThreads)的 1/4 左右。-XX:InitiatingHeapoccupancyPercent
设置触发并发 Gc 周期的 Java 堆占用率阈值。超过此值,就触发 GC。默认值是 45。G1 收集器的常见操做步骤
G1 的设计原则就是简化 JVM 性能调优,开发人员只须要简单的三步便可完成调优:
第一步:开启 G1 垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
G1 中提供了三种垃圾回收模式:Young GC、Mixed GC 和 Full GC,在不一样的条件下被触发。
G1 收集器的适用场景
用来替换掉 JDK1.5 中的 CMS 收集器;在下面的状况时,使用 G1 可能比 CMS 好:
分区 Region:化整为零
使用 G1 收集器时,它将整个 Java 堆划分红约 2048 个大小相同的独立 Region 块,每一个 Region 块大小根据堆空间的实际大小而定,总体被控制在 1MB 到 32MB 之间,且为 2 的 N 次幂,即 1MB,2MB,4MB,8MB,16MB,32MB。能够经过 XX:G1HeapRegionsize 设定。全部的Region大小相同,且在JVM生命周期内不会被改变。
虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分 Region(不须要连续)的集合。经过 Region 的动态分配方式实现逻辑上的连续。
一个 region 有可能属于 Eden,Survivor 或者 old/Tenured 内存区域。可是一个 region 只可能属于一个角色。图中的 E 表示该 region 属于 Eden 内存区域,s 表示属于 survivor 内存区域,o 表示属于 Old 内存区域。图中空白的表示未使用的内存空间。
G1 垃圾收集器还增长了一种新的内存区域,叫作 Humongous 内存区域,如图中的 H 块。主要用于存储大对象,若是超过 1.5 个 region,就放到 H。
设置 H 的缘由
对于堆中的大对象,默认直接会被分配到老年代,可是若是它是个短时间存在的大对象,就会对垃圾收集器形成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放大对象。若是一个 H 区装不下一个大对象,那么 G1 会寻找连续的 H 区来存储。为了能找到连续的H区,有时候不得不启动 Full GC
。G1 的大多数行为都把 H 区做为老年代的一部分来看待。
每一个 Region 都是经过指针碰撞来分配空间,还能够为每一个线程分配 TLAB。
G1 垃圾回收器的回收过程
G1 GC 的垃圾回收过程主要包括以下三个环节:
应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程
;G1 的年轻代收集阶段是一个并行
的独占式
收集器。在年轻代回收期,G1 GC 暂停全部应用程序线程,启动多线程执行年轻代回收。而后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有多是两个区间都会涉及
。
当堆内存使用达到必定值(默认 45%)时,开始老年代并发标记过程。
标记完成立刻开始混合回收过程。对于一个混合回收期,G1 GC 从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不一样,老年代的 G1 回收器和其余 GC 不一样,G1 的老年代回收器不须要整个老年代被回收,一次只须要扫描/回收一小部分老年代的 Region 就能够了
。同时,这个老年代 Region 是和年轻代一块儿被回收的。
Remembered Set(记忆集)
一个对象被不一样区域引用的问题
一个 Region 不多是孤立的,一个 Region 中的对象可能被其余任意 Region 中对象引用,判断对象存活时,是否须要扫描整个 Java 堆才能保证准确?
在其余的分代收集器,也存在这样的问题(而 G1 更突出)回收新生代也不得不一样时扫描老年代?这样的话会下降 MinorGC 的效率;
解决方法:
每一个 Region 都有一个对应的 Remembered Set;
G1 回收过程一:年轻代 GC
JVM 启动时,G1 先准备好 Eden 区,程序在运行过程当中不断建立对象到 Eden 区,当 Eden 空间耗尽时,G1 会启动一次年轻代垃圾回收过程。
年轻代垃圾回收只会回收Eden区和Survivor区。
Young GC 时,首先 G1 中止应用程序的执行(STW),G1 建立回收集(Collection Set),回收集是指须要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代 Eden 区和 Survivor 区全部的内存分段。
而后开始以下回收过程:
根是指 static 变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同 RSet 记录的外部引用做为扫描存活对象的入口。
处理 dirty card queue 中的 card,更新 RSet。此阶段完成后,RSet能够准确的反映老年代对所在的内存分段中对象的引用
。
对于应用程序的引用赋值语句 object.field=object,JVM 会在以前和以后执行特殊的操做以在 dirty card queue 中入队一个保存了对象引用信息的 card。在年轻代回收的时候,G1 会对 dirty card queue 中全部的 card 进行处理,以更新 RSet,保证 RSet 实时准确的反映引用关系。那为何不在引用赋值语句处直接更新 RSet 呢?这是为了性能的须要,RSet 的处理须要线程同步,开销会很大,使用队列性能会好不少。
识别被老年代对象指向的 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象。
此阶段,对象树被遍历,Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段,Survivor 区内存段中存活的对象若是年龄未达阈值,年龄会加 1,达到阀值会被会被复制到 o1d 区中空的内存分段。若是 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间。
处理 Soft,Weak,Phantom,Final,JNI Weak 等引用。最终 Eden 空间的数据为空,GC 中止工做,而目标内存中的对象都是连续存储的,没有碎片,因此复制过程能够达到内存整理的效果,减小碎片。
G1 回收过程二:并发标记过程
标记从根节点直接可达的对象。这个阶段是 STW 的,而且会触发一次年轻代 GC。
G1 GC 扫描 survivor 区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在 Young GC 以前完成。
在整个堆中进行并发标记(和应用程序并发执行),此过程可能被 Young GC 中断。在并发标记阶段,若发现区域对象中的全部对象都是垃圾,那这个区域会被当即回收。
同时,并发标记过程当中,会计算每一个区域的对象活性(区域中存活对象的比例)。
因为应用程序持续进行,须要修正上一次的标记结果。是 STW 的。G1 中采用了比 CMS 更快的初始快照算法:snapshot-at-the-beginning(SATB)。
计算各个区域的存活对象和 GC 回收比例,并进行排序,识别能够混合回收的区域。为下阶段作铺垫。是 STW 的。这个阶段并不会实际上去作垃圾的收集。
识别并清理彻底空闲的区域。
G1 回收过程三: 混合回收
当愈来愈多的对象晋升到老年代 Old Region 时,为了不堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并非一个 Old GC,除了回收整个 Young Region,还会回收一部分的 Old Region。这里须要注意:是一部分老年代,而不是所有老年代
。能够选择哪些 Old Region 进行收集,从而能够对垃圾回收的耗时时间进行控制。也要注意的是 Mixed GC 并非 Full GC。
并发标记结束之后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认状况下,这些老年代的内存分段会分 8 次(能够经过-XX:G1MixedGCCountTarget 设置)被回收
混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden 区内存分段,Survivor 区内存分段。混合回收的算法和年轻代回收的算法彻底同样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
因为老年代中的内存分段默认分 8 次回收,G1 会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收
。而且有一个阈值会决定内存分段是否被回收,
-XX:G1MixedGCLiveThresholdPercent,默认为 65%,意思是垃圾占内存分段比例要达到 65%才会被回收。若是垃圾占比过低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
混合回收并不必定要进行 8 次。有一个阈值-XX:G1HeapWastePercent,默认值为 10%,意思是容许整个堆内存中有 10%的空间被浪费,意味着若是发现能够回收的垃圾占堆内存的比例低于 10%,则再也不进行混合回收。由于 GC 会花费不少的时间可是回收到的内存却不多。
G1 回收过程三: Full GC
G1 的初衷就是要避免 Full GC 的出现。可是若是上述方式不能正常工做,G1 会中止应用程序的执行
(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会很是差,应用程序停顿时间会很长。
要避免 Full GC 的发生,一旦发生须要进行调整。何时会发生 Full GC 呢?好比堆内存过小
,当 G1 在复制存活对象的时候没有空的内存分段可用,则会回退到 Full GC,这种状况能够经过增大内存解决。
致使 G1 Full GC 的缘由可能有两个:
截止 JDK1.8,一共有 7 款不一样的垃圾收集器。每一款的垃圾收集器都有不一样的特色,在具体使用的时候,须要根据具体的状况选用不一样的垃圾收集器。
GC 发展阶段:Seria l=> Parallel(并行)=> CMS(并发)=> G1 => ZGC
不一样厂商、不一样版本的虚拟机实现差距比较大。HotSpot 虚拟机在 JDK7/8 后全部收集器及组合以下图
怎么选择垃圾回收器
Java 垃圾收集器的配置对于 JVM 优化来讲是一个很重要的选择,选择合适的垃圾收集器可让 JVM 的性能有一个很大的提高。怎么选择垃圾收集器?
最后须要明确一个观点:
没有最好的收集器,更没有万能的收集;调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器。
经过阅读 Gc 日志,咱们能够了解 Java 虚拟机内存分配与回收策略。 内存分配与垃圾回收的参数列表
-XX:+PrintGC
输出 GC 日志。相似:-verbose:gc
-XX:+PrintGCDetails
输出 GC 的详细日志-XX:+PrintGCTimestamps
输出 GC 的时间戳(以基准时间的形式)-XX:+PrintGCDatestamps
输出 GC 的时间戳-XX:+PrintHeapAtGC
在进行 GC 的先后打印出堆的信息-Xloggc:../logs/gc.log
日志文件的输出路径-verbose:gc
打开 GC 日志:
-verbose:gc
这个只会显示总的 GC 堆的变化,以下:
参数解析:
PrintGCDetails
打开 GC 日志:
-verbose:gc -XX:+PrintGCDetails
输入信息以下:
参数解析:
Allocation Failure 代表本次引发 GC 的缘由是由于在年轻代中没有足够的空间可以存储新的数据了。
Young GC Detail
Full GC Detail
GC 回收举例
public class GCUseTest { static final Integer _1MB = 1024 * 1024; public static void main(String[] args) { byte [] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 *_1MB]; allocation2 = new byte[2 *_1MB]; allocation3 = new byte[2 *_1MB]; allocation4 = new byte[4 *_1MB]; } }
-Xms20m -Xmx20m -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:UseSerialGC
首先咱们会将 3 个 2M 的数组存放到 Eden 区,而后后面 4M 的数组来了后,将没法存储,由于 Eden 区只剩下 2M 的剩余空间了,那么将会进行一次 Young GC 操做,将原来 Eden 区的内容,存放到 Survivor 区,可是 Survivor 区也存放不下,那么就会直接晋级存入 Old 区。
而后咱们将 4M 对象存入到 Eden 区中。
ZGC 与 shenandoah 目标高度类似,在尽量对吞吐量影响不大的前提下,实如今任意堆内存大小下均可以把垃圾收集的停颇时间限制在十毫秒之内的低延迟。
《深刻理解 Java 虚拟机》一书中这样定义 ZGC:ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法
的,以低延迟为首要目标的一款垃圾收集器。
ZGC 的工做过程能够分为 4 个阶段:并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射
等。
ZGC 几乎在全部地方并发执行的,除了初始标记的是 STW 的。因此停顿时间几乎就耗费在初始标记上,这部分的实际时间是很是少的。
吞吐量对比:
停顿时间对比:
JDK14 以前,ZGC 仅 Linux 才支持。
尽管许多使用 ZGC 的用户都使用类 Linux 的环境,但在 Windows 和 Mac OS 上,人们也须要 ZGC 进行开发部署和测试。许多桌面应用也能够从 ZGC 中受益。所以,ZGC 特性被移植到了 Windows 和 Mac OS 上。
如今 Mac 或 Windows 上也能使用 ZGC 了,示例以下:
-XX:+UnlockExperimentalVMOptions-XX:+UseZGC