浅谈JVM垃圾回收

JVM内存区域

要想搞懂啊垃圾回收机制,首先就要知道垃圾回收主要回收的是哪些数据,这些数据主要在哪一块区域。java

Java8和Java8以前的相同点有不少。算法

都有虚拟机栈,本地方法栈,程序计数器,这三个是线程隔离的也称是线程独有的;数组

本地内存和堆是线程共享的。安全

Java8和以前JVM内存区域不一样的是,Java8中增长了元空间,取消了永久代,Java8以前永久代是在堆中的,而以后方法区搬到了元空间中,元空间存在于本地内存中。多线程

下面详细说一下各个内存区域的特色。并发

  • 虚拟机栈:描述的是方法执行时的内存模型,是线程私有的,生命周期与线程同步,每一个方法被执行的时候都会建立本身的栈帧,主要保存的是局部变量表,操做数栈,动态连接和方法的返回地址等信息。方法执行完成后就清空了栈帧的信息,入栈出栈实际都很明确,而且这块区域不须要进行GC。
  • 本地方法栈:与虚拟机栈功能很是相似。主要区别是虚拟机栈是为虚拟机执行java方法,而本地方法栈是为虚拟机执行本地方法,所以这块区域也不须要进行GC。
  • 程序计数器:用来记录每一个线程执行到了哪一条指令。线程隔离的。好比每一个字节码以前都有一个数字,咱们能够认为他就是程序计数器存储的内容。这些数字的做用就是记录线程运行时的状态,方便线程下一次被唤醒的时候能从上次执行的位置继续执行,须要注意的是程序计数器是惟一一个在Java虚拟机中没有规定任何OOM状况的区域,所以这块区域也不须要进行GC。
  • 堆:对象实例和数组都是在堆上分配的,GC主要对这两类数据进行回收。
  • 本地内存:线程共享区域。本地内存也叫堆外内存,包含元空间和直接内存。从Java8开始,有了元空间的概念,咱们来看一下为何要取消永久代,永久代实际上指的是HotSpot虚拟机上的永久代,他用永久代实现了JVM规范定义方法区的功能,永久代主要存放类的信息,常量,静态变量,即时编译器编译后的代码等,永久代的大小是有限的,能够经过XX:MaxPermSize参数指定上限,因此若是动态生成类信息或者大量执行String.intern方法(直接将字符串放入永久代)就会形成永久代内存溢出引发OOM。所以在Java8中就将方法区的实现移动到本地内存中的元空间中,这样方法区就不受JVM的控制了,也就不进行GC,所以有必定的性能提高,一样这样方法区也方便在元空间中进行统一管理。

如何识别垃圾

引用计数法

引用计数法就是每一个对象引用你一次,你的对象头上就+1,若是没有对象引用你(引用次数为0),那你凉凉,等着被回收吧。框架

听着引用计数确实能够解决咱们没法识别哪些对象该被回收的问题,可是他还有个主要问题没被解决,那就是循环引用。什么是循环引用呢?jvm

例如性能

A a = new Instance("a");
B b = new Instance("b");
a.instance=b;
b.instance=a;
a=null;
b=null;

虽然到最后a和b两个对象都被置为null,可是由于他们以前都互相引用过,因此引用的次数都是1,所以没法被回收。因此现代虚拟机都不使用这种方法来判断对象是否该回收了。线程

可达性分析算法

现代虚拟机主要是采用这种算法进行判断独享是否是该被回收。它的原理是从一个叫作GC Root对象为起点出发,引出他们指向的下一个节点,再从下一个节点出发,继续引出下一个,以此类推。这样就经过GCRoot节点串成了一条引用链,若是相关对象不是这个引用链上的节点,则会被断定为垃圾,而后会被回收。

可达性分析算法能够解决上述循环引用的问题,由于两个对象a,b都没有在GC Root所在的引用链上。

对象最后一次垂死挣扎的机会,finalize方法。

当发生GC时,finalize方法给对象一个催死挣扎的机会,当对象可回收的时候,首先会判断这个对象是否是执行了finalize方法,若是未执行,则会先执行finalize,咱们能够在finalize方法内部将本对象和GC Root关联起来,这样执行完方法后,GC会再次判断对象是否可被回收,若是可达则不会进行回收。

finalize方法只会执行一次,若是第一次执行方法这个对象变成了可达确实不会回收可是再次对这个对象进行回收的时候,则会忽略finalize方法。

哪些对象能够做为GC Root呢?

  • 虚拟机栈中引用的对象(本地变量表中的对象)
  • 方法区中静态属性引用的对象。
  • 方法去中常量引用的对象。
  • 本地方法Native中引用的对象。

再谈引用

JDK1.2后,Java对引用的概念进行了补充,将引用分为强引用,软引用,弱引用,虚引用。强度依次递减。

  • 强引用:强引用就是new出来的引用,只要强引用存在,垃圾收集器就不会回收掉对象。
  • 软引用:用来描述一些有用可是未必须的引用,在进行发生内存溢出以前会对软引用进行回收,若是内存空间充足不会回收软引用指向的对象,提供了SoftReferemce来实现软引用。
  • 弱引用也是用来描述非必须对象。可是他的强度比软引用还要弱,弱引用关联的对象只能存活到下一次GC以前,不管内存是否充足都会回收弱引用关联的对象。弱引用用WeakReference类来实现。
  • 虚引用:也叫幽灵引用或者幻影引用,是最弱的一种引用关系,一个对象是否有虚引用的存在彻底不影响对象的生存时间,虚引用存在的目的就是能在这个对象被回收时收到一个系统通知。PhantomReference类来实现虚引用。

垃圾回收算法

上面讲了如何经过可达性分析算法来是被哪些数据是垃圾,那具体该经过什么方式回收垃圾呢?

垃圾回收算法主要由如下几种方式

  • 标记清除法
  • 复制算法
  • 标记整理法

标记清除法

先用可达性分析算法标记处可回收的对象。

对可回收对象进行回收。

image-20210115122547191

操做简单不须要移动数据,可是缺点也很明显,就是存在内存碎片。若是想要再申请的内存空间大小大于碎片的大小就会申请失败,那要是将回收过的内存区域和原先没有数据的区域都合并到一块就能够了。

复制算法

将堆等分红两块内存区域,咱们暂且把他记做区域A和区域B,A负责分配对象,区域B不分配,A区域中的对象标记为可回收时,将A中全部不可回收的对象都赶到B中,对A进行统一清除,B中存活的对象紧邻排列。

这种算法的缺陷也很明显,我明明堆中还有不少空余的空间可是不能分配,只能使用一半的空间,另外每次回收都要移动对象,这是很浪费资源而且效率低下。

标记整理法

标记整理法与标记清除法不一样的是他多了一步整理内存碎片的操做。将全部存活对象都往一端移动,紧邻排列,再清除另外一端的全部区域,这样就解决了内存碎片的问题。

可是还有缺点:每次清除可回收对象都要进行对象的移动,效率很低下。

image-20210115123647316

分代收集算法

分代收集算法整合了上面所讲的全部算法,综合以上算法优势,最大程度避免他们的缺点,所以使现代虚拟机采用的首选算法,于其说他是算法,倒不是说它是一种收集策略。

通过有关专家研究代表,大部分对象(98%)都是朝生夕死,通过一次年轻代的GC就会被回收,因此分代收集算法是根据对象存活周期的不一样将堆分红新生代和老年代,在Java8以前还有永久代,新生代和老年代的比例是1:2,新生代又分为Eden区,from Survivor区,to Survivor区,简称S0区和S1区,Eden:S0:S1=8:1:1,咱们将新生代发生的GC叫作Young GC或Minor GC,将老年代发生的GC叫作 Old GC也叫Full GC。

工做原理

新生代的分配和回收

新生代对象通常在Eden区分配,当Eden满的时候,会发生一次Minor GC,此次Minor GC不多有对象存活,由于大部分对象都是朝生夕死的,少部分存活的对象会被移动到S0区,同时这些对象的年龄+1,最后将Eden区中的全部对象都清除,释放空间。(复制算法

当发生下一次Minor GC时,会把Eden区中存活的对象和S0中存活的对象都移动到S1,这些对象的年龄+1,同时清空Eden和S0空间。

若再次发生MinorGC重复上面的步骤,只不过此次是将Eden和S1中存活的对象移动到S1,每次Young GC都是S0和S1来回之间移动。由于S0和S1区域比较小,因此下降复制算法频繁拷贝带来的开销。

对象是如何进入到老年代的

大对象直接进入老年代

大对象通常指的是很长的字符串或者数组,当出现大对象时,会致使提早触发GC,虚拟机提供了一个-XX:PertenureSizeThreshold参数若是对象大小大于这个参数设置的阈值,就认为是大对象,直接分配到老年代,这样作的目的是避免Eden和S1,S0区域之间发生大对象的拷贝。

长期存活的对象进入老年代

虚拟机给每一个对象都定义了一个年龄计数器,每次通过Minor GC后还存活下来的对象,他们的年龄+1,当计数器的值加到必定程度(默认是15),就会晋升到老年代,对象晋升老年代的阈值能够经过参数-XX:MaxTenuringThershold设置。

动态对象年龄断定

这种状况也会晋升到老年代,若是Survivor区中相同年龄的对象大小之和大于Survivor区空间大小的一半,这时候年龄大于等于该年龄的对象也会直接进入老年代,无需和MaxTrnuringThershold参数进行比较。

空间分配担保

在发生Minor GC以前,虚拟机会检查老年代最大可用的连续空间是否大于新生代全部对象的总空间,若是大于,那么就能够确保Minor GC是安全的,若是不大于,虚拟机会查看HandlePromotionFailure设置值是否容许担保失败,若是容许的话,那么会继续检查老年代对象的平均大小,若是大于则进行GC,否者可能进行一次Full GC。尽管空间分配担保绕的圈子很大,可是平时仍是会开启担保的,由于能够减小Full GC的频率。

Stop The World

若是老年代满了,会触发Full GC,Full GC会同时回收新生代和老年代,也就是对整个堆进行GC,他会致使Stop The World,形成很大的性能开销。Stop The World就是指在这个GC期间,除了垃圾回收线程在工做,其余线程会被挂起。

通常Full GC会致使工做线程停顿时间过长,若是再次期间,服务端收到了客户端不少的请求,则会被拒绝服务,因此才要尽可能减小Full GC的次数。

所以虚拟机设计成新生代分为Eden,S0,S1,而且设置对象年龄阈值,默认新生代和老年代的比例是1:2都是为了不对象过早的进入老年代,尽量晚的触发Full GC。

老年代采用标记整理法进行垃圾回收。

由于GC都会影响性能,因此咱们要在一个合适的时间点发起GC,这个时间点被称为安全点(Safe Point),这个时间点的选定既不能太少让GC时间太长,也不能过于频繁以致于过度的增大运行时的负荷,安全点通常是如下特定的位置:

  • 循环的末尾
  • 方法返回前
  • 调用方法的call以后
  • 抛出异常的位置。

垃圾收集器的种类

收集算法实际上是理论层面的,垃圾收集器才是这些理论具体的实现。

image-20210115134007568

新生代收集器

Serial收集器

Serial收集器收集的是新生代,单线程的垃圾收集器,单线程意味着他只会使用一个CPU或者一个收集线程来进行垃圾回收,他在进行垃圾回收的时候,其余用户线程会暂停,在GC期间这个应用不可用。可是在用户端模式下,他是简单有效的,对于限定单个CPU的环境来讲,Serial单线程模式无需与其余线程进行交互,较少了开销,专心作GC能将单线程的优点发挥到极致,在桌面应用场景下,通常不会给虚拟机分配很大的内存,所以STW(Stop The World)的时间会在100ms之内,这点停顿是能够接受的,因此对于Client模式下的虚拟机,Serial收集器是新生代的默认收集器。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用多线程,其余收集算法以及对象分配,回收策略都和Serial同样。ParNew主要工做在服务端,服务端若是接受的请求多了,响应时间就很重要,多线程可让垃圾与回收更快,也就是减小了STW时间,提高响应速度,因此许多运行在服务端的虚拟机采用的新生代垃圾收集器是ParNew ,还有一点,他只能和CMS收集器配合工做,CMS是一个彻底并发的收集器,第一次实现了垃圾收集线程和用户线程同时工做,采用的是传统的GC收集器代码框架,与Serial,ParNew共用一套代码框架,因此能够和这两个收集器配合工做。

在多CPU状况下,ParNew收集器垃圾收集更快,能够有效减小STW时间,提高服务端响应速度。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一个使用复制算法,多线程,工做在新生代的垃圾收集器。看起来他的功能和ParNew收集器同样。可是还有一些不一样。

关注点不一样:CMS等垃圾收集器关注的是尽量缩短垃圾收集时用户线程停顿的时间,而Parallel Scavenge目标是达到一个可控制的吞吐量。
$$
吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间)
$$
CMS等垃圾收集器更适合用于与用户交互的应用,提高用户体验。而Parallel Scavenge收集器关注的是吞吐量,因此更适合用于后台运算等不须要太多用户交互的任务。

Parallel Scavenge收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾手机时间的-XX:MaxGCPauseMillis以及设置吞吐量大小的-XX:GCtimeRatio默认是99%。

除了这两个参数外,还有第三个参数-XX:UseAdaptiveSizePolicy开启这个参数后,就不要手工指定新生代大小比例等细节,只须要设置好堆的大小,以及最大垃圾收集时间和吞吐量,虚拟机就会根据当前系统运行状况动态调整这些参数尽量的达到设定的最大垃圾收集时间和吞吐量,自适应策略是ParallelScavenger和ParNew的重要区别。

老年代收集器

Serial Old

Serial收集器是工做在新生代的单线程收集器。Serial Old是工做在老年代的单线程收集器。这个收集器的主要意义是给Client模式下的虚拟机使用,若是在Server模式下,他还有两大用途,一种是和JDK1.5以及以前的版本的Parallel Scavenge收集器配合使用,另外一种是做为CMS的备用方案。

Parallel Old

Parallel Old收集器是相对于Parallel Scavenge收集器的老年代版本,使用多线程和标记整理法。

CMS

CMS收集器是以实现最短STW时间为目标的收集器,若是应艳红很重视服务的相应速度,但愿给用户最好的体验,则CMS收集器是不错的选择。

CMS虽然工做在老年代可是回收算法使用的是标记清除法。

一、初始标记

二、并发标记

三、从新标记

四、并发清除

在这四个步骤中,初始标记和从新标记两个阶段会发生STW,形成用户线程挂起,不过初始标记仅仅标记GC Root可以关联的对象,速度很快,从新标记是进行GC Root跟踪引用链的过程,是为了修正并发标记期间由于用户线程继续运行而致使标记产生变更的哪一部分对象的标记记录,这一阶段停顿时间通常比初始标记更长,但比并发标记短。

整个过程执行时间最长的是并发标记和标记整理,不过这两个阶段用户线程均可以工做,因此不影响应用的正常使用,因此整体上看,能够认为CMS是内存回收线程和用户线程一块儿并发执行的。

可是他有三个缺点:

  • CMS收集器对CPU资源很是敏感。好比原本有10个用户线程处理请求,如今要分出三个线程作垃圾回收工做,吞吐量降低了30%,CMS默认启动的回收线程数=(CPU数量+3)/4,若是CPU是2个,那么吞吐量直接下降50%。显然是不可接受的。
  • CMS没法处理浮动垃圾,什么是浮动垃圾?由于并发清理阶段,用户线程还在工做,因此还会出现新的可回收对象,这部分垃圾只能在下一次GC时再清理,因此这部分垃圾就是浮动垃圾。由于垃圾收集阶段用户线程还在运行因此须要预留足够多的空间确保用户线程正常执行,这就意味着CMS收集器要提早进行Full GC,JDK1.5默认当老年代使用68%空间就后被激活,这个比例能够经过-XX:CMSInitiatingOccupancyFraction来设置,可是若是设置过高容易致使CMS运行期间预留的内存不够,致使Concurrent Model Failure,这时会启用Serial Old收集器进行老年代的收集工做,可是Serial old 是单线程的,这就致使STW时间更长了。
  • CMS由于采用的是标记清除法,因此会存在大量的内存碎片,若是没法找到足够的内存空间进行分配,就会触发FUllGC进行垃圾回收,影响应用的性能,咱们能够开启-XX:+UseCMSCompactAtFullCollection,这个参数是当CMS顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理会致使STW,停顿时间会变长,还能够用另外一个参数-XX:CMSFullGCsBeforeCompation用来设置执行多少次不压缩的Full GC事后再进行一次压缩。

G1(Garbage First)

G1收集器欧式面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器。

特色以下:

  • 向CMS收集器同样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 须要GC停顿时间更好预测。
  • 不会像CMS那样牺牲大量的吞吐性能。
  • 不须要打的java 堆。

与CMS相比,它有如下方面表现得更为出色。

  1. 运行期间不会产生内存碎片。总体采用标记整理法,局部采用复制算法,两种算法都不会产生内部碎片。
  2. STW创建在可预测的停顿时间模型,用户能够指按期望停顿时间,G1将会停顿时间控制在用户设定的停顿时间之内。

他为何能创建可预测模型呢?

主要缘由是他和传统的内存分配存储方式不同。传统内存分配是连续的,新生代,老年代。可是G1的存储地址不是连续的,每一代都是用N个不连续的大小相同的Region,每一个Region占有一块连续的虚拟内存地址。和传统相比还多了一个H区,表明Humongous,标会存储的是大对象。当对象大小大于Region的通常,就直接分给老年代,防止GC时反复拷贝大对象。

这样作G1就能够根据Region的价值大小(回收所得到的空间大小以及回收经验值)进行排序,维护成一个优先级列表,根据容许的时间,回收截止最大的Region,也就避免了整个老年代的回收,减小了STW形成的停顿时间。

G1收集器工做步骤

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

筛选阶段会根据各个Region的回收价值和成本进行排序,根据用户指望的GC停顿时间来制定回收计划。

相关文章
相关标签/搜索