概述

垃圾回收器(GC)是什么以及为什么我们需要垃圾回收器??

垃圾回收是Java语言区别于其他语言的一种最为重要的特性之一, 通过垃圾回收器(Garbage Collection)来实现对我们Java虚拟机中内存的自动回收,即将一些我们不再需要的对象所在的内存进行回收。
正是这种特性,使得我们的Java程序员在进行开发工作时不再像C/C++程序员一样需要关心对象的释放和内存的回收。这为我们的开发带来了极大的便利。虽然在这种便利下,势必需要付出一些额外的性能消耗。但是这种消耗在多数情况下所带来的收益也是远超出其代价的。
因此,本篇将和大家一起讨论关于垃圾回收器以及内存分配策略的相关知识点。后文也会用GC来代替垃圾回收进行说明。

JVM是如何判断哪些对象需要回收的?

有了解过虚拟机的同学都很清楚,Java的虚拟机内存模型被分为了5个部分:虚拟机栈、堆、方法区、程序计数器以及本地方法栈。其中堆占据着我们虚拟机最大的内存,而我们几乎所有的对象的内存分配都是在堆中进行的,因此,对于虚拟机的内存回收主要是针对堆中哪些不再使用的对象的回收。

那么既然我们需要去回收哪些不再使用的对象实例,我们就必须要知道堆中哪些对象是不会再被使用的,也就是失效对象。下面来介绍虚拟机中常见的几种判断方式。

引用计数法

引用计数法是一种比较直接的算法,通过给每个对象分配一个引用计数器。每当一个地方引用它时,计数器值就会加1.当引用失效时,也就是某个变量不再指向该对象时,计数器值就会减1.任何时刻计数器值为0的对象就是不可能再被使用的。

客观地说,引用计数法简单直接,是一个效率不错的算法。当下也有很多的语言正是使用这个算法进行内存管理。比如Python、微软的COM(Component Object Model)技术等。但是,至少在主流的Java虚拟机中没有选择用引用计数法来实现内存的管理。其中最主要的原因是它很难解决对象之间的相互循环引用的问题。

可达性分析算法

在主流的语言中包括Java、C#都是都是通过可达性分析来判定对象是否存活的。这种算法的思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始往下搜索,所有不在其搜索路径上的对象都被认为是一个不可达对象,即该对象已经失效。如下图

而在Java中,可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象:可以理解为我们线程当前执行的方法中的局部变量所引用的对象
  • 方法区中类静态属性引用的对象:一个类中静态变量所引用的对象,一个类在被卸载之前都会保持其对静态变量的存储。因此只要一个类没卸载,其所引用的对象就不会被回收
  • 方法区中常量引用的对象:一个类中的常量引用类型的变量所引用的对象,比如我们的字符串字面常量在JDK7后都是存储在堆中,在方法区中有一张常量表来引用实际的String对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象:可以参考虚拟机栈

所有被上述对象所引用的对象都属于有效对象,正常情况下一般是不会被GC回收的。而所有不在其引用链上的对象则都会被回收。这种方式可以很好地解决对象的循环引用问题。

强引用、弱引用、软引用与虚引用

除了那些不可用的对象以外,事实上一些有效的对象也会被GC回收。也就是本小结的标题所述的reference类型:强引用、弱引用、软引用与虚引用。
这几个类型的作用就是用于描述某种介于引用与未被引用两中状态中间的状态。换句话来说就是一些食之无味,弃之可惜的对象。当内存空间充足时,这些对象可以保留在内存中;而内存紧张时,则可以抛弃这些对象。这种方式为我们的内存的控制带来了相当的灵活性。

  • 强引用:垃圾回收器永远不会回收的对象
  • 软引用:即将发生内存溢出时会被回收的对象
  • 弱引用:在下一次GC时会被回收
  • 虚引用:可以看作没有被引用,甚至无法通过虚引用来获取一个对象实例。其存在的唯一目的就是在该对象被回收时能收到一个系统通知(一般情况下可以不用纠结这个引用)

上述是关于强引用、弱引用、软引用与虚引用的简单叙述,有需要了解的朋友可以单独去搜索一下。

补充

我们前面一直在谈如何判断哪些对象不可用,会被回收。但是一个对象在被判断不可用后就一定会被回收吗?
事实是不一定。在一般垃圾回收算法中,一个对象在被回收之前会有一轮标记阶段,然后在回收阶段对所有被标记的对象进行回收。而在第一轮标记阶段我们类如果被覆写了finalize()方法,则会被执行。而这个阶段一个对象是可以在方法中完成自救从而不被回收。只要在方法中将自己挂载到某个GC Roots对象的引用链上即可。但是这种方式虽然可行,但是可不推荐哦。听听就行了。事实上,一般也很少见到有人覆写finalize()方法。

垃圾收集算法

垃圾收集算法的目的是为了来提高我们垃圾的收集效率。下面将介绍几种垃圾收集算法的思想。

标记-清除算法

标记-清除算法是最早出现也是最基础的垃圾收集算法算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收未标记的对象。标记的过程就是对象是否属于垃圾的判定过程。

缺点:

  1. 执行效率不稳定。如果Java堆中包含大量对象,而且其中大部分是需要回收的,这时必须进行进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。
  2. 内存空间的碎片问题。标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

所谓复制算法,就是把内存分为2块等同大小的内存空间(A和B),使用A进行内存的使用,当A部分的内存不足以分配对象而引起内存回收时,就会把存活的对象从A内存块放到B内存块中,后把A内存块中的对象全部清除,在B内存块中使用,当B内存不足以分配内存时,就会把B中存活的对象放到A内存块中,然后把B中对象全部清除,如此循环。

使用这种方式可以避免出现空间碎片(内存中不连续的空间)。

缺点:
浪费了一半的内存,降低空间的使用率。

事实上,现在的商用虚拟机都采用这种收集算法来回收新生代,并且根据IBM公司的研究表明,新生代中的对象98%是“朝生夕死”的,所以不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,即每次新生代中可用内存空间为整个新生代容量的90%,只有10%会被浪费。
当然我们无法被保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。

标记整理算法

标记-整理算法的标记过程与“标记-清理算法”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
整理后的内存,避免了标记清除中的内存碎片的问题,也避免了复制算法中的内存浪费问题,当然,存在的问题就是效率问题了,会比前者的效率低。

分代收集算法

分代收集算法严格意义上来说并不算一个垃圾回收算法,因为该算法中并没有什么回收对象的内容。该算法的目的是针对不同存活时间的对象进行分类,对于不同的类别使用不同的垃圾回收算法来进一步提高垃圾回收的效率。
分代收集算法根据对象的存货周期的不同将内存划分为了几块。一般是把Java堆分为老年代和新生代,这样就可以根据各个年代的特点采用最适合的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象的存活率高,每次垃圾回收只需要回收少量的对象,同时也由于对象过多,缺少额外的空间进行分配担保,因此更适合标记-清理或标记-整理算法
下面是各垃圾回收器的使用区域以及组合图(相连的两个收集器可以组合)

常见的垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾回收器就是内存回收的具体实现。并且由于JVM并未规定垃圾收集器的实现方式。因此市面上也存在多种垃圾收集器。

Serial收集器(客户机默认)

Serial收集器是一个用于新生代的单线程的收集器。在该收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。这种方式下,固然GC效率非常高,但是这么长时间的暂停会使得很多应用无法接受。试想一下,我们的服务器每10秒都会被暂停1秒,也就是这1秒无法响应用户的请求,这在并发情况下是不能接受的。(这里只是举个例子)
下图示意了Serial收集器的运行过程:

这个收集器看似不太行,事实上到现在为止,他依然是虚拟机运行在Client模式下的默认新生代收集器。只要停顿时间控制好,且GC不频繁,这点停顿还是可以接受的。在单CPU情况下,该收集器不存在线程交互的开销,可以有很高的收集效率。

ParNew(Parallel New)收集器

ParNew收集器在Serial收集器的基础上升级为多线程的垃圾回收,在多核环境下更快。除了多线程GC外,其他部分与Serial收集器一致。

在多核CPU的环境下,ParNew势必具有更高的GC效率。但是在单CPU的环境中,ParNew收集器未必能够胜过Serial收集器。因为后者不存在线程切换的开销。

默认情况下,该收集器开启的收集线程数量与Cpu的数量相同。也可以通过-XX:ParallelGCThreads参数进行控制。

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,同样使用复制算法,也是并行的多线程收集器。
该收集器在ParNew收集器的基础上增加了对GC停顿时间与吞吐量的控制。其目的是为了在尽可能控制一个相对合适的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间))来使用户具有比较好的使用体验。为此,它提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来对GC时间和吞吐量进行控制。

GC时间的减少是通过限制新生代空间的大小来实现的。换句话来说,垃圾场越小,我需要清理的垃圾也就越少,每次清理垃圾所需耗费的时间也就越少。但是这种方式也会使得GC的频率更高,同样也降低了吞吐量。而吞吐量的设置则更容易找到合适的用户体验。

Serial Old收集器

前面几种收集器都是新生代的收集器,从本处开始讨论老年代的收集器。
Serial Old收集器是Serial收集器的老年代版本。其同样是一个单线程收集器,只不过与Serial收集器使用的场景不同。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。该收集器从JDK1.6中才开始提供,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,那么老年代收集器除了Serial Old别无其它选择。同时,由于服务端应用通常更追求高吞吐量和高响应,而Serial Old收集器的长时间GC停顿势必无法满足,反而拖累Parallel Scavenge收集器的吞吐量。因此直到Parallel Old收集器的出现才使得这个多线程GC组合完善了起来。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获得最短GC停顿时间为目标的收集器。非常适合一些假设在服务端并对响应速度有要求的应用。
从该收集器的名字上就可以看出使用的是“标记-清理”算法。其运行原理相较于前面几种收集器来说更为复制一些,整个过程分为4个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中,初始标记和重新标记两个步骤仍然需要暂停其他工作线程。
初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度非常快。并发标记阶段就是对GC Roots引用链上对象的追溯标记的过程,这个过程由于对象比较多,因此需要更多的时间。需要采用并发的方式进行,避免影响到正常的工作线程的执行。重新标记阶段是为了修正并发标记期间因用户程序继续运行而导致标记产生变化的那部分对象的标记记录。由于这部分的对象不会很多,且为了保证不会再因为变化而再次进行重新标记,因此重新标记阶段需要暂停其他工作线程从而避免在重新标记阶段又出现一些新的变化。在重新标记结束后,所有的需无效对象的都会被标记出来。此时进行并发的GC就可以完成清除。至于并发标记和并发清除阶段产生的新的垃圾则等到下一轮GC即可。

CMS收集器的缺点

  1. CMS收集器对处理器资源非常敏感。
  2. 由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次Full GC的产生,即让Serial Old来进行收集。(在并发标记和并发清理阶段由于程序正常运行产生的垃圾无法在本次GC中被处理,只好等到下一次GC,这部分的垃圾就称为浮动垃圾。而“Con-current Mode Failure”是指CMS在GC期间剩下的空间不够正在运行的程序进行内存分配而导致的异常。)
  3. 大量空间碎片的产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。(Full GC是指暂停所有工作线程,并对老年代和新生代进行一次完成垃圾回收的过程)

G1收集器

G1(Garbage First)收集器是JDK1.7的重要特征之一,该收集器远不同于前面的所有收集器,它不局限于新生代或者老年代,而是可以同时适用于两个区域的收集器。同时,G1也是一款面向服务端应用的垃圾收集器,这意味着它具有良好的GC停顿。下面介绍G1不同于其他收集器的特点:

  • 并行与并发:G1会充分利用多核CPU的优势来使用多个CPU来缩短GC的停顿时间,并在某些阶段同样通过并发方式让工作线程继续执行来提高效率。
  • 分代收集:G1可以不需要组合其他收集器就能独立管理整个堆,但它能够采用不同的方式去处理新的对象和已经存活较久的老对象以获得更好的收集效果。
  • 空间整合:G1从整体上看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上看是基于“复制”算法实现的。不论如何,这意味着G1运行期间不会产生不可用的内存空间碎片,收集后能够提供规整的可用内存。利于程序的长时间运行,减少Full GC的出现频率。(G1虽然保留了分代概念,但不局限于老年代和新生代,而是将整个堆分为多个等大小的独立区域Region)
  • 可预测的停顿:与CMS收集器一样,G1也关注低停顿,但除此之外,它还能建立可预测的停顿时间模型,能让使用者明确指定在某个确定的时间段内,消耗在GC上的时间不能超过某个指定时间长度。

G1收集器之所以能够建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会跟踪各个Region里的垃圾堆积的价值大小(回收所获得的空间大小和回收所需时间的比值),并在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这种方式保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1的收集步骤可划分为以下几个步骤

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收


其中,初始标记阶段仅仅是标记一下GC Roots能直接关联到的对象,速度非常快。并发标记阶段就是对GC Roots引用链上对象的追溯标记的过程,这个过程由于对象比较多,因此需要更多的时间。需要采用并发的方式进行,避免影响到正常的工作线程的执行。这两步与CMS是一致的。而最终标记阶段所作的事情与CMS的重新标记所作的行为非常相似,都是为了修正并发标记期间因用户程序继续运行而导致标记产生变化的那部分对象的标记而进行记录,不同的是在G1中,虚拟机会将这部分变化的记录放入线程Remembered Set Logs中并合并数据到Remembered Set(在后面解释)里,这阶段需停顿工作线程,但可以并行执行。最后在筛选回收阶段根据各Region的价值进行排序,并根据用户所期望的GC停顿时间来指定回收计划。

Remembered Set
在G1收集器中,堆被分为多个等大小的Region,而每个Region都有一个与之对应的Remembered Set。其存在是为了加快我们GC标记阶段的标记效率,快速确定一个对象是否存活。当我们Region1中有个对象引用了Region2中的对象时,这个引用关系会被记录在Region2对应的Remembered Set中。如果我们需要判断Region2中这个对象是否存活时,只需要根据其对应的Remembered Set向上追溯到某个GC Roots即可,而避免了通过全堆扫描的方式找到上一级的引用。

总结

总的来说,HotSpot的垃圾收集器是伴随着内存发展而不断前进的,早期几十M的内存,Serial+Serial Old单线程进行回收就足以;但是内存达到几百M时,就得使用PS+PO多线程的GC线程来回收;当内存达到几个G时,多线程也忙不过来,就得使用并发的CMS+ParNew收集器;当到了动辄几十个G内存的时候,以前那种每次GC都进行新生代或老年代或整个堆的回收的STW也无法忍受时,就得使用G1了。
目前绝大数的生产环境都是使用的JDK8,若没有进行过调优,默认使用的是PS+PO的收集器;但是要进行调优时,会在CMS和G1中来进行选择,如果内存比较大(10G以上),最好使用G1,内存较小(几个G)可以考虑CMS。
但是不管怎样,如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,HotSpot虚拟机完全没必要实现那么多种不同的收集器了。也就是只有最合适的收集器,没有最好的收集器。

JVM的内存分配与回收策略

对象的内存分配实际上就是在堆上分配。对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存,将按线程优先在TLAB(Thread Local Allocation Buffer本地线程分配缓存)上分配。少数情况下也可能会直接分配在老年代中。分配的具体细节取决于使用的垃圾收集器组合以及相关的虚拟机参数。

对象优先在Eden分配
一般情况来说,新的对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象。最典型的就是那种很长的字符串以及数组。

长期存活的对象将进入老年代
由于虚拟机采用分代收集的思想来管理内存,因此当我们的对象在经过多轮GC后都未被回收掉,那么可见的未来中,这个对象大概率是不太容易失效的。因此将这种对象直接放入老年代。一般情况下这个晋升的阈值默认为15轮,当然也可以通过-XX:MaxTenuringThreshold=14改变晋升老年代的年龄阈值。

动态对象年龄判定
为了更好地适应不同程序的内存情况,虚拟机并不是永远地要求对象的年龄必须达到晋升阈值才能晋升老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于这个年龄的对象可以直接进入老年代。

空间分配担保
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。只要老年代的连续空间大于新生代对象总大小或者历次晋升老年代对象的平均大小就会进行 Minor GC ,否则将进行 Full GC 。

Java虚拟机的垃圾回收器以及内存分配策略详解相关推荐

  1. 20200405——java之jvm 垃圾回收器和内存分配策略 二

    什么叫GC Garbage Collection 什么内存区域需要GC 共享区的都要被回收比如堆区以及方法区. 在进行内存回收之前要做的事情就是判断那些对象是'死'的,哪些是'活'的.常用方法有两种 ...

  2. 深入理解Java虚拟机-垃圾回收器与内存分配策略

    本博客主要参考周志明老师的<深入理解Java虚拟机>第二版 读书是一种跟大神的交流.阅读<深入理解Java虚拟机>受益匪浅,对Java虚拟机有初步的认识.这里写博客主要出于以下 ...

  3. 【java虚拟机序列】java中的垃圾回收与内存分配策略

    在[java虚拟机系列]java虚拟机系列之JVM总述中我们已经详细讲解过java中的内存模型,了解了关于JVM中内存管理的基本知识,接下来本博客将带领大家了解java中的垃圾回收与内存分配策略. 垃 ...

  4. 垃圾回收器和内存分配策略

    本文作者:李敏,叩丁狼高级讲师.原创文章,转载请注明出处. 4. 垃圾回收器和内存分配策略 **GC(Garbage Collection)**的历史比java久远.1960年诞生于MIT的Lisp是 ...

  5. 垃圾回收器与内存分配策略

    垃圾回收器与内存分配策略 1. 前言 计数器:计数器难以解决循环引用,需要大量的额外处理才能正确工作. 可达性分析算法 在java技术栈里GC Roots包括以下几种 虚拟机栈中引用的对象 方法区中类 ...

  6. 【JVM和性能优化】2.垃圾回收器和内存分配策略

    内存回收 为什么要了解GC(Garbage Collection)和内存分配策略 1.面试需要 2.GC对应用的性能是有影响的 3.写代码有好处 那些需要GC: 共享区的都要被回收比如堆区以及方法区. ...

  7. Java 垃圾回收器与内存分配策略 JVM

    文章目录 什么是垃圾回收? 哪些位置内存需要回收? 四种引用关系 如何判断一个对象是不是需要回收? GC Roots 对象有哪些? 如何回收? 垃圾回收算法的指导思想 常用的垃圾回收算法 看图即可明白 ...

  8. java虚拟机手动内存分配_《深入理解java虚拟机》-垃圾收集器与内存分配策略

    如何判断对象已死? 引用计数算法 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1:当引用失效时,计数器减1:其中计数器为0的对象是不可能再被使用的已死对象. 引用计数算法的实现很简单 ...

  9. 笔记:深入理解JVM 第3章 垃圾回收器与内存分配策略

    1.对象是否已死 (1). 引用计数法:无法回收相互引用的对象,故JVM没有采用 例子: public class ReferenceObj { public ReferenceObj refObj; ...

最新文章

  1. html显示数据库图片django,django将图片上传数据库后在前端显式的方法
  2. AngularJS安装配置与基础概要整理(上)
  3. canvas——橡皮筋式线条绘图应用
  4. 机器学习实战-KNN算法-20
  5. 236.二叉树的最近公共祖先
  6. TOJ3651确定比赛名次
  7. python 二重积分_Python机器学习(五十七)SciPy 积分
  8. android studio中把c/c++文件编译成.so库(一)
  9. Mysql你应该要懂索引知识
  10. 蓝桥杯 ALGO-38 算法训练 接水问题
  11. eclipse linux 中文,Eclipse (简体中文)
  12. Python华氏摄氏度的转换
  13. win10右键卡顿原因_右键菜单反应慢?win10系统解决右键菜单卡顿方法
  14. win10隐藏网络计算机,连接隐藏网络,教你win10系统电脑连接隐藏网络的方法
  15. matlab2018在图片上添加文字并保存且图片没有白边
  16. 视频剪辑软件调研(Corel VideoStudio 2018、爱剪辑、微剪辑)
  17. 对Jekyll的初步了解
  18. CSS 悬停的用法
  19. flutter 九宫格菜单_flutter九宫格图片查看器
  20. 访问网站报错‘您目前无法访问XXXX 因为此网站使用了 HSTS

热门文章

  1. Android Surface system analyze
  2. 计算机b级考试基础知识,全国计算机等级考试一级b
  3. 华为海思2022数字芯片笔试题(节选)
  4. FileZilla.exe下载
  5. RLT-DiMP: Robust Long-Term Object Tracking via Improved Discriminative Model Prediction
  6. 如何统一设置Word 的图片属性
  7. 豆瓣vs微博,内容社区究竟该争些什么?
  8. office 卸载工具
  9. 扫地机器人的轮子困住_扫地机器人老被困住,我来告诉你怎么一次性解决
  10. Word:Windows找不到文件‘C:\Program Files(x86)\Adobe\Acrobat DC\Acrobat\AcroTray.exe’。请确定文件名是否正确后,再试一次(已解决)