jvm垃圾回收

一:概述

程序计数器、虚拟机栈、本地方法栈,3个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。而Java堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是这部分内存。

接下来我们就讨论Jvm是怎么回收这部分内存的。在进行回收前垃圾收集器第一件事情就是确定哪些对象还存活,哪些已经死去。下面介绍两种基础的回收算法。

1.1 引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时计数器就+1,当引用失效时计数器就-1,。只要计数器等于0的对象就是不可能再被使用的。

此算法在大部分情况下都是一个不错的选择,也有一些著名的应用案例。但是Java虚拟机中是没有使用的。

优点:实现简单、判断效率高。

缺点:很难解决对象之间循环引用的问题。例如下面这个例子

Object a = new Object();
Object b = new Object();
a=b;
b=a;
a=b=null; //这样就导致gc无法回收他们。

1.2 可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。

主流的商用程序语言(Java、C#等)在主流的实现中,都是通过可达性分析来判定对象是否存活的。

通过下图来清晰的感受gc root与对象展示的联系。所示灰色区域对象是存活的,Object5/6/7均是可回收的对象

在Java语言中,可作为GC Roots 的对象包括下面几种

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中静态变量引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈(即一般说的 Native 方法)中JNI引用的对象

优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;

缺点:实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题)。

二:引用

在jdk1.2之后,Java对引用的概念进行了扩充,总体分为4类:强引用、软引用、弱引用、虚引用,这4中引用强度依次逐渐减弱。

  • 强引用:指在代码中普遍存在的,类似 Object obj = new Object(); 这类的引用,只有强引用还存在,GC就永远不会收集被引用的对象。
  • 软引用:指一些还有用但并非必须的对象。直到内存空间不够时(抛出OutOfMemoryError之前),才会被垃圾回收。采用SoftReference类来实现软引用
  • 弱引用:用来描述非必须对象。当垃圾收集器工作时就会回收掉此类对象。采用WeakReference类来实现弱引用。
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响, 唯一目的就是能在这个对象被回收时收到一个系统通知, 采用PhantomRenference类实现

2.1 判断一个对象生存还是死亡

宣告一个对象死亡,至少要经历两次标记。

1、第一次标记

如果对象进行可达性分析算法之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。

筛选条件:判断此对象是否有必要执行finalize()方法。

筛选结果:当对象没有覆盖finalize()方法、或者finalize()方法已经被JVM执行过,则判定为可回收对象。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;  

2、第二次标记

GC对F-Queue队列中的对象进行二次标记。

如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。

3、finalize() 方法

finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

特别说明:并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。

垃圾回收算法

一:标记-清除算法

最基础的收集算法,总共分为‘ 标记 ’和‘ 清除 ’两个阶段

1.标记
标记出所有需要回收的对象

在《Jvm垃圾回收器(基础篇)》中说明了判断对象是否回收需要两次标记,现在我们再来回顾一下

一次标记:在经过可达性分析算法后,对象没有与GC Root相关的引用链,那么则被第一次标记。并且进行一次筛选:当对象有必要执行finalize()方法时,则把该对象放入F-Queue队列中。

二次标记:对F-Queue队列中的对象进行二次标记。在执行finalize()方法时,如果对象重新与GC Root引用链上的任意对象建立了关联,则把他移除出“ 即将回收 ”集合。否则就等着被回收吧!!!

对被第一次标记切被第二次标记的,就可以判定位可回收对象了。

2.清除
两次标记后,还在“ 即将回收 ”集合的对象进行回收。

执行过程如下:

优点:基础最基础的可达性算法,后续的收集算法都是基于这种思想实现的

缺点:标记和清除效率不高,产生大量不连续的内存碎片,导致创建大对象时找不到连续的空间,不得不提前触发另一次的垃圾回收。

二:复制算法

将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉。

复制算法执行过程如下:

优点:实现简单,效率高。解决了标记-清除算法导致的内存碎片问题。

缺点:代价太大,将内存缩小了一半。效率随对象的存活率升高而降低。

现在的商业虚拟机都采用这种算法(需要改良1:1的缺点)来回收新生代。

2.1 HotSpot虚拟机的改良算法

1.弱代理论 
分代垃圾收集基于弱代理论。具体描述如下:
大多说分配了内存的对象并不会存活太长时间,在处于年轻时代就会死掉。
很少有对象会从老年代变成年轻代。
其中IBM研究表明:新生代中98%的对象都是"朝生夕死"; 所以并不需要按1:1比例来划分内存(解决了缺点1);

2.Hotspot虚拟机新生代内存布局及算法
新生代内存分配一块较大的Eden空间和两块较小的Survivor空间

每次使用Eden和其中一块Survivor空间

回收时将Eden和Survivor空间中存活的对象一次性复制到另一块Survivor空间上

最后清理掉Eden和使用过的Survivor空间。

Hotspot虚拟机默认Eden和Survivor的大小比例是8:1。

分配担保

如果另一块Survivor空间没有足够内存来存放上一次新生代收集下来的存活对象,那么这些对象则直接通过担保机制进入老年代。

关于分配担保的内容,我会在讲述垃圾收集器时详细描述。

三:标记-整理算法

标记-整理算法是根据老年代的特点应运而生。

3.1 标记
标记过程和标记-清理算法一致(也是基于可达性分析算法)。

3.2 整理
和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理。让存活对象都向一端移动,然后直接清理掉边界以外的内存。

标记-整理算法示意图

优点:不会像复制算法那样随着存活对象的升高而降低效率,不像标记-清除算法那样产生不连续的内存碎片

缺点:效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率更低。

7种垃圾回收器

先来了解HotSpot虚拟机中的7种垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,先介绍一些垃圾收集的相关概念,再介绍它们的主要特点、应用场景、以及一些设置参数和基本运行原理。
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

垃圾收集器概述
垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法、火车算法)的具体实现,不同商家、不同版本的JVM所提供的垃圾收集器可能会有很在差别。这里主要讨论基于JDK1.7 Update14之后的HotSpot虚拟机。新生代收集器使用的收集器:Serial、PraNew、Parallel Scaveng;老年代收集器使用的收集器:Serial Old、Parallel Old、CMS;以及G1收集器。

在这里插入图片描述

1. 垃圾收集器组合
1.1 7种不同分代的收集器:
Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

1.2区域划分:
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;

1.3两个收集器间有连线,表明它们可以搭配使用:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

1.4其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案(后面介绍);
2.并发垃圾收集和并行垃圾收集的区别
2.1并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old;

2.2并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; 如CMS、G1(也有并行);

3.Minor GC和Full GC的区别
3.1Minor GC
又称新生代GC,指发生在新生代的垃圾收集动作;
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

3.2Full GC
又称Major GC或老年代GC,指发生在老年代的GC;
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上;

下面将介绍这些收集器的特性、基本原理和使用场景,并重点分析CMS和G1这两款相对复杂的收集器;但需要明确一个观点:
没有最好的收集器,更没有万能的收集;
选择的只能是适合具体应用场景的收集器。

Serial收集器(复制算法)
Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器。这是新生代单线程收集器。这里的“单线程”的意义,既是指它只会使用一个CPU和一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停所有用户线程。

虽然Serial收集器单线程的特点造成用户体验很差,但是它也有巨大的优点,简单而高效。尤其对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集可以获得最高的单线程手机效率。目前Serial收集器仍是桌面级Client模式下虚拟机的默认新生代收集器。

简而言之,缺点是会造成停顿,优点是简单高效,适合桌面级Client模式。

JDK1.3.1前是HotSpot新生代收集的唯一选择;

1、特点
针对新生代;
采用复制算法;
单线程收集;
进行垃圾收集时,必须暂停所有工作线程,直到完成;
即会"Stop The World";
Serial/Serial Old组合收集器运行示意图如下:

在这里插入图片描述

2、应用场景
依然是HotSpot在Client模式下默认的新生代收集器;
也有优于其他收集器的地方:
简单高效(与其他收集器的单线程相比);
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;

在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的

3、设置参数
“-XX:+UseSerialGC”:添加该参数来显式的使用串行垃圾收集器;

4、Stop TheWorld说明
JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;

会带给用户不良的体验;

从JDK1.3到现在,从Serial收集器-》Parallel收集器-》CMS-》G1,用户线程停顿时间不断缩短,但仍然无法完全消除;
3、ParNew收集器
ParNew垃圾收集器是Serial收集器的多线程版本。

ParNew收集器(复制算法)
1、特点
ParNew收集器实际上就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop the World、对象分配规则、回收策略等都与Serial收集器完全一样。

ParNew收集器是许多Server模式下的虚拟机中首选的新生代收集器,其中一个重要原因是处理Serial收集器外,目前只有ParNew收集器可以和老年代的CMS收集器配合。

ParNew在单CPU情况下,由于存在线程切换的开销,哪怕在两个CPU的情况下效率都有可能比Serial更低;但是随着CPU数量的增加,ParNew对GC时系统资源的有效利用也回来越好。

ParNew/Serial Old组合收集器运行示意图如下:

在这里插入图片描述

2、应用场景

在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

3、设置参数
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;

“-XX:+UseParNewGC”:强制指定使用ParNew;

“-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

4、为什么只有ParNew能与CMS收集器配合
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;

因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

关于CMS收集器后面会详细介绍。

Parallel Scavenge收集器(复制算法)
Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

Parallel Scavenge收集器是新生代多线程收集器。它追求的是达到可控的吞吐量。所谓的吞吐量就是CPU用于运行用户代码的时间,与CPU总消耗时间的壁纸,即吞吐量=运行用户代码时间/(运行用户代码时间+GC线程时间)。

停顿时间越短就越适合需要与用户交互的程序,而高吞吐量则可以高效率利用CPU时间,适合在后台运算不需要太多交互的任务。

1、特点
(A)、有一些特点与ParNew收集器相似
新生代收集器;
采用复制算法;
多线程收集;

(B)、主要特点是:它的关注点与其他收集器不同
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
关于吞吐量与收集器关注点说明详见本节后面;

2、应用场景
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;

当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;

例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;

3、设置参数
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:

(A)、"-XX:MaxGCPauseMillis"

控制最大垃圾收集停顿时间,大于0的毫秒数;

MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;

因为可能导致垃圾收集发生得更频繁;

(B)、"-XX:GCTimeRatio"

设置垃圾收集时间占总时间的比率,0<n<100的整数;

GCTimeRatio相当于设置吞吐量大小;

垃圾收集执行时间占应用程序执行时间的比例的计算方法是:

1 / (1 + n)

例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%–1/(1+19);

默认值是1%–1/(1+99),即n=99;

垃圾收集所花费的时间是年轻一代和老年代收集的总时间;

如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;

此外,还有一个值得关注的参数:

(C)、"-XX:+UseAdptiveSizePolicy"

开启这个参数后,就不用手工指定一些细节参数,如:

新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;

JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);

这是一种值得推荐的方式:

(1)、只需设置好内存数据大小(如"-Xmx"设置最大堆);

(2)、然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;

(3)、那些具体细节参数的调节就由JVM自适应完成;

这也是Parallel Scavenge收集器与ParNew收集器一个重要区别;

4、吞吐量与收集器关注点说明
(A)、吞吐量(Throughput)
CPU用于运行用户代码的时间与CPU总消耗时间的比值;
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;
(B)、垃圾收集器期望的目标(关注点)
(1)、停顿时间
停顿时间越短就适合需要与用户交互的程序;
良好的响应速度能提升用户体验;
(2)、吞吐量
高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;
主要适合在后台计算而不需要太多交互的任务;
(3)、覆盖区(Footprint)
在达到前面两个目标的情况下,尽量减少堆的内存空间;
可以获得更好的空间局部性;

Serial Old收集器(标记-整理算法)
Serial Old是 Serial收集器的老年代版本;
Serial收集器的老年代版本,也是单线程收集器,使用标记-整理算法。

这个收集器主要也是给Client模式下的虚拟机使用。
如果在Server模式下,还有两大用途:一种是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配;一种是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

1、特点
针对老年代;
采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
单线程收集;

Serial/Serial Old收集器运行示意图如下:
在这里插入图片描述

2、应用场景

主要用于Client模式;
而在Server模式有两大用途:

(A)
在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);

(B)
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);

Parallel Old收集器(标记-整理算法)
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

在吞吐量优先的场合,优先考虑Parallel Scavenge收集器+ Parallel Old收集器的组合。
JDK1.6中才开始提供;

1、特点
针对老年代;
采用"标记-整理"算法;
多线程收集;
Parallel Scavenge/Parallel Old收集器运行示意图如下:

在这里插入图片描述

2、应用场景

JDK1.6及之后用来代替老年代的Serial Old收集器;

特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;

3、设置参数
“-XX:+UseParallelOldGC”:指定使用Parallel Old收集器;

CMS收集器(标记-清除算法)
并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;

CMS(Concurrent Mark Sweep)收集器是一款以获取最短回收停顿时间为目标的收集器。目前很大一部分应用集中在互联网站或者B/S系统发服务器上,这类应用尤其重视服务的相应速度,希望系统停顿时间最短。

1、特点
针对老年代;
基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
以获取最短回收停顿时间为目标;
并发收集、低停顿;
需要更多的内存(看后面的缺点);
是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

2、应用场景
与用户交互较多的场景;
希望系统停顿时间最短,注重服务的响应速度;
以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用;

3、设置参数
“-XX:+UseConcMarkSweepGC”:指定使用CMS收集器;

4、CMS收集器运作过程
比前面几种收集器更复杂,可以分为4个步骤:

(A)、初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象;
速度很快;
但需要"Stop The World";

(B)、并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;
不需要“Stop the World”

(C)、重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记的时间短;
采用多线程并行执行来提升效率;

(D)、并发清除(CMS concurrent sweep)
回收所有的垃圾对象;
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;
不需要“Stop the World”

由于整个过程中耗时最长的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作,所以,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器运行示意图如下:
在这里插入图片描述

5、CMS收集器3个明显的缺点

(A)、对CPU资源非常敏感

并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
CMS的默认收集线程数量是=(CPU线程数+3)/4;
当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
增量式并发收集器:
针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);
类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;
但效果并不理想,JDK1.6后就官方不再提倡用户使用。

(B)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败,而导致Full GC
(1)、浮动垃圾(Floating Garbage)
浮动垃圾,指的是并发清理阶段产生的垃圾。因为并发清理阶段用户程序也在运行,产生的垃圾在标记过程之后,所以本次清理过程不会被清理,并且CMS还必须预留一部分空间提供给并发收集时的程序运作使用。

这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;
也要可以认为CMS所需要的空间比其他垃圾收集器大;

“-XX:CMSInitiatingOccupancyFraction”:设置CMS预留内存空间;
JDK1.5默认值为68%;
JDK1.6变为大约92%;

(2)、"Concurrent Mode Failure"失败
要是CMS运行期间预留的内存无法满足程序需要,就会触发“Concurrent Mode Failure”,这时虚拟机将启动后备预案,临时启用Serial Old收集器,但停顿时间会明显增加,而导致另一次Full GC的产生;

这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。
CMS提供了参数-XXCMSInitiatingOccupancyFraction来控制触发CMS的内存使用占比,设置太低会导致CMS触发过于频繁,设置太高则很容易出现大量的“Concurrent Mode Failure”。

(C)、产生大量内存碎片
由于CMS基于"标记-清除"算法,清除后不进行压缩操作;

前面《Java虚拟机垃圾回收(二) 垃圾回收算法》"标记-清除"算法介绍时曾说过:

产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。

解决方法:

(1)、"-XX:+UseCMSCompactAtFullCollection"

默认打开。这个参数用于在CMS快要进行FullGC时开启内存碎片的合并整理过程,内存的整理过程无法并发,虽然减少了空间碎片,但是也增加了停顿时间。
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);

(2)、"-XX:+CMSFullGCsBeforeCompaction"

这个参数用于设置执行多少次不压缩的FullGC后,执行一次带碎片整理的FullGC。默认是0,表示每次Full GC都不进行碎片整理。
为减少合并整理过程的停顿时间;

默认为0,也就是说每次都执行Full GC,不会进行压缩整理;

由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;

总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;

但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;

G1收集器
G1(Garbage-First)是JDK7-u4才推出商用的收集器;

G1收集器是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

在G1中分代概念仍然保留。虽然G1不需要和其他收集器配合,可以独立管理GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。

G1将内存分成多个大小相等的独立区域,虽然还保留着新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,它们都是一部分Region(不需要连续)的集合。

1、特点
(A)、并行与并发
能充分利用多CPU、多核环境下的硬件优势;
可以并行来缩短"Stop The World"停顿时间;
也可以并发让垃圾收集与用户程序同时进行;

(B)、分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
虽然保留分代概念,但Java堆的内存布局有很大差别;
将整个堆划分为多个大小相等的独立区域(Region);
新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;

(C)、结合多种垃圾收集算法,空间整合,不产生碎片
从整体看,是基于标记-整理算法;
从局部(两个Region间)看,是基于复制算法;
这是一种类似火车算法的实现;

都不会产生内存碎片,有利于长时间运行;

(D)、可预测的停顿:低停顿的同时实现高吞吐量
降低停顿时间是G1和CMS共同关注的,G1除了追求低停顿外,还能建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,GC垃圾收集消的时间不得超过N毫秒。

(E)、有计划的垃圾回收
G1可以有计划的在Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆的价值大小(回收所获得的空间大小,以及回收所需要的时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这就是Garbage-First的由来。

2、应用场景
面向服务端应用,针对具有大内存、多处理器的机器;

最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;

如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;

用来替换掉JDK1.5中的CMS收集器;

在下面的情况时,使用G1可能比CMS好:

(1)、超过50%的Java堆被活动数据占用;

(2)、对象分配频率或年代提升频率变化很大;

(3)、GC停顿时间过长(长于0.5至1秒)。

是否一定采用G1呢?也未必:

如果现在采用的收集器没有出现问题,不用急着去选择G1;

如果应用程序追求低停顿,可以尝试选择G1;

是否代替CMS需要实际场景测试才知道。

3、设置参数
“-XX:+UseG1GC”:指定使用G1收集器;

“-XX:InitiatingHeapOccupancyPercent”:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;

“-XX:MaxGCPauseMillis”:为G1设置暂停时间目标,默认值为200毫秒;

“-XX:G1HeapRegionSize”:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;

更多关于G1参数设置请参考:

《垃圾收集调优指南》 10.5节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#important_defaults

4、为什么G1收集器可以实现可预测的停顿
G1可以建立可预测的停顿时间模型,是因为:

可以有计划地避免在Java堆的进行全区域的垃圾收集;

G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;

每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);

这就保证了在有限的时间内可以获取尽可能高的收集效率;

5、一个对象被不同区域引用的问题
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?

在其他的分代收集器,也存在这样的问题(而G1更突出):

回收新生代也不得不同时扫描老年代?

这样的话会降低Minor GC的效率;

解决方法:

无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:

每个Region都有一个对应的Remembered Set;

每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;

然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);

如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;

当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;

就可以保证不进行全局扫描,也不会有遗漏。

6、G1收集器运作过程
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

(A)、初始标记(Initial Marking)

仅标记一下GC Roots能直接关联到的对象;

且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;

需要"Stop The World",但速度很快;

(B)、并发标记(Concurrent Marking)

进行GC Roots Tracing的过程;

刚才产生的集合中标记出存活对象;

耗时较长,但应用程序也在运行;

并不能保证可以标记出所有的存活对象;

(C)、最终标记(Final Marking)

为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

上一阶段对象的变化记录在线程的Remembered Set Log;

这里把Remembered Set Log合并到Remembered Set中;

需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

采用多线程并行执行来提升效率;

(D)、筛选回收(Live Data Counting and Evacuation)

首先排序各个Region的回收价值和成本;

然后根据用户期望的GC停顿时间来制定回收计划;

最后按计划回收一些价值高的Region中垃圾对象;

回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;

可以并发进行,降低停顿时间,并增加吞吐量;G1收集器运行示意图如下:
在这里插入图片描述

记录几个问题

1. 对象一定分配在堆中吗?有没有了解逃逸分析技术?

对象一定分配在堆中吗? 不一定的,JVM通过逃逸分析,那些逃不出方法的对象会在栈上分配。

  • 什么是逃逸分析?

逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。

逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。

  • 一个逃逸分析的例子
/**
 *  @author 捡田螺的小男孩
 */
public class EscapeAnalysisTest {

    public static Object object;

    //StringBuilder可能被其他方法改变,逃逸到了方法外部。
    public StringBuilder  escape(String a, String b) {
        //公众号:捡田螺的小男孩
        StringBuilder str = new StringBuilder();
        str.append(a);
        str.append(b);
        return str;
    }

    //不直接返回StringBuffer,不发生逃逸
    public String notEscape(String a, String b) {
        //公众号:捡田螺的小男孩
        StringBuilder str = new StringBuilder();
        str.append(a);
        str.append(b);
        return str.toString();
    }

    //外部线程可见object,发生逃逸
    public void objectEscape(){
        object = new Object();
    }

    //仅方法内部可见,不发生逃逸
    public void objectNotEscape(){
        Object object = new Object();
    }
}
复制代码

逃逸分析的好处

  • 栈上分配,可以降低垃圾收集器运行的频率。
  • 同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
  • 标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。 二、程序内存回收效率高,并且GC频率也会减少。

2.虚拟机为什么使用元空间替换了永久代?

什么是元空间?什么是永久代?为什么用元空间代替永久代? 我们先回顾一下方法区吧,看看虚拟机运行时数据内存图,如下:

方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

什么是永久代?它和方法区有什么关系呢?

如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代。可以说方法区是规范,永久代是Hotspot针对该规范进行的实现。在Java7及以前的版本,方法区都是永久代实现的。

什么是元空间?它和方法区有什么关系呢?

对于Java8,HotSpots取消了永久代,取而代之的是元空间(Metaspace)。换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。

为什么使用元空间替换了永久代?

  • 永久代的方法区,和堆使用的物理内存是连续的。

永久代是通过以下这两个参数配置大小的~

  • -XX:PremSize:设置永久代的初始大小
  • -XX:MaxPermSize: 设置永久代的最大值,默认是64M

对于永久代,如果动态生成很多class的话,就很可能出现java.lang.OutOfMemoryError: PermGen space错误,因为永久代空间配置有限嘛。最典型的场景是,在web开发比较多jsp页面的时候。

  • JDK8之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器内存有多大,元空间就有多大

可以通过以下的参数来设置元空间的大小:

  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

所以,为什么使用元空间替换永久代?

表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。 当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦。

3.什么是Stop The World ? 什么是OopMap?什么是安全点?

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为Stop The World

在HotSpot中,有个数据结构(映射表)称为OopMap。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在特定的位置生成 OopMap,记录下栈上和寄存器里哪些位置是引用。

这些特定的位置主要在:

  • 1.循环的末尾(非 counted 循环)
  • 2.方法临返回前 / 调用方法的call指令后
  • 3.可能抛异常的位置

这些位置就叫作安全点(safepoint)。 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

5. 守护线程是什么?守护线程和非守护线程的区别是?守护线程的作用是?

守护线程是区别于用户线程哈,用户线程即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程。

守护线程和非守护线程的区别是? 我们通过例子来看吧~

    /**
      * 关注公众号:捡田螺的小男孩
      */
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        System.out.println("我是子线程(用户线程.I am running");
                    } catch (Exception e) {
                    }
                }
        });
        //标记为守护线程
        t1.setDaemon(true);
        //启动线程
        t1.start();

        Thread.sleep(3000);
        System.out.println("主线程执行完毕...");
    }
复制代码

运行结果:

可以发现标记为守护线程后,主线程销毁停止,守护线程一起销毁。我们再看下,去掉 t1.setDaemon(true)守护标记的效果:

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        System.out.println("我是子线程(用户线程.I am running");
                    } catch (Exception e) {
                    }
                }
        });
        //启动线程
        t1.start();

        Thread.sleep(3000);
        System.out.println("主线程执行完毕...");
    }
复制代码

所以,当主线程退出时,JVM 也跟着退出运行,守护线程同时也会被回收,即使是死循环。如果是用户线程,它会一直停在死循环跑。这就是守护线程和非守护线程的区别啦。

守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是为什么垃圾回收线程需要是守护线程啦

7. 是否了解Java语法糖嘛?说下12种Java中常用的语法糖?

语法糖(Syntactic Sugar),也称糖衣语法,让程序更加简洁,有更高的可读性。Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等12种。

  • 语法糖一、switch 支持 String 与枚举
  • 语法糖二、 泛型
  • 语法糖三、 自动装箱与拆箱
  • 语法糖四 、 方法变长参数
  • 语法糖五 、 枚举
  • 语法糖六 、 内部类
  • 语法糖七 、条件编译
  • 语法糖八 、 断言
  • 语法糖九 、 数值字面量
  • 语法糖十 、 for-each
  • 语法糖十一 、 try-with-resource
  • 语法糖十二、Lambda表达式

感兴趣的朋友,可以看下这篇文章哈:不了解这12个语法糖,别说你会Java!

8. 什么是指针碰撞?什么是空闲列表?什么是TLAB?

一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是“指针碰撞”。

如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是“空闲列表

对象创建在虚拟机中是非常频繁的行为,可能存在线性安全问题。如果一个线程正在给A对象分配内存,指针还没有来的及修改,同时另一个为B对象分配内存的线程,仍引用这之前的指针指向,这就出问题了。

可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过-XX:UseTLAB设定它的。

9.CMS垃圾回收器的工作过程,CMS收集器和G1收集器的区别。

CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。如图(下图来源互联网):

CMS收集器和G1收集器的区别:

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
  • G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
  • CMS收集器以最小的停顿时间为目标的收集器;
  • G1收集器可预测垃圾回收的停顿时间
  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  • G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

10.JVM 调优

JVM调优其实就是通过调节JVM参数,即对垃圾收集器和内存分配的调优,以达到更高的吞吐和性能。JVM调优主要调节以下参数

堆栈内存相关

  • -Xms 设置初始堆的大小
  • -Xmx 设置最大堆的大小
  • -Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
  • -Xss 每个线程的堆栈大小
  • -XX:NewSize 设置年轻代大小(for 1.3/1.4)
  • -XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
  • -XX:NewRatio 年轻代与年老代的比值(除去持久代)
  • -XX:SurvivorRatio Eden区与Survivor区的的比值
  • -XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
  • -XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代

垃圾收集器相关

  • -XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
  • -XX:ParallelGCThreads=20: 配置并行收集器的线程数
  • -XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
  • -XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。
  • -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片

辅助信息相关

  • -XX:+PrintGCDetails 打印GC详细信息
  • -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用
  • -XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题.
  • -XX:+PrintTLAB 查看TLAB空间的使用情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值