jvm-垃圾回收机制--即将作废
一、基础概念JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信..
一、基础概念
二、Java垃圾回收机制
什么是垃圾回收?
顾名思义,垃圾回收就是释放垃圾占用的空间,那么在Java中,什么样的对象会被认定为“垃圾”?那么当一些对象被确定为垃圾之后,采用什么样的策略来进行回收(释放空间)?在目前的商业虚拟机中,有哪些典型的垃圾收集器?下面我们就来逐一探讨这些问题。以下是本文的目录大纲:
一.如何确定某个对象是“垃圾”?
二.典型的垃圾收集算法
三.典型的垃圾收集器
4.1 确定某个对象是“垃圾”的方法
4.1.1 引用计数法
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。
不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。这种方式成为引用计数法。
不足:无法解决对象之间的循环引用。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
所谓对象之间的相互引用问题,如下面代码所示:
除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。
但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
4.1.2 可达性分析法
该算法的基本思路就是通过一些被称为(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
在Java中,可作为GC Root的对象包括以下几种:
- 栈中引⽤的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
4.1.3 finalize()方法最终判定对象是否存活
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
-
第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。 -
第二次标记
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。
这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
子类可以override该方法,用于防止对象被回收,亦或是防止对象不被回收。
要防止对象被回收,只需让该对象与GC ROOTS之间存在可达链即可。
我们重点看看FileInputStream、FileOutputStream、Connection等类怎么防止用户忘记释放资源吧,如下是FileInputStream的部分源码:
/**
* Ensures that the <code>close</code> method of this file input stream is
* called when there are no more references to it.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FileInputStream#close()
*/
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
object的finalize()方法
垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
finalize的执行过程(生命周期)
首先,大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
@see java finalize方法总结、GC执行finalize的过程 https://www.cnblogs.com/Smina/p/7189427.html
实例
1.如何确定某个对象是“垃圾”?
主要有以下几种
1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象
Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;
原来的obj所引用的对象已经不能使用了,故可以被标记为垃圾对象。
obj1 已经指向了另一个对象obj2,故obj1原来所指向的对象也将被标记为垃圾对象。
2)局部方法里引用所指向的对象,比如下面这段代码:
void fun() {
.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
}
循环每执行完一次,生成的Object对象都会成为可回收的对象。
原理分析:
在一个方法的内部有一个强引用,这个引用保存在栈中,而真正的引用内容(Object)保存在堆中。当这个方法运行完成后就会退出方法栈,则引用内容的引用不存在,这个Object会被回收。
但是如果这个o是全局的变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。
3)只有弱引用与其关联的对象,比如
WeakReference<String> wr = new WeakReference<String>(new String("world"));
原理分析:
当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
4.1.3 对象的四种引用方式
1、强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:
Object o=new Object(); // 强引用
当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:
o=null; // 帮助垃圾收集器回收此对象
2、软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
3、弱引用(WeakReference)
当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
弱引用与软引用的区别在于:
只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
4、虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
表格说明
弱引用WeakReference
注意:真正弱应用指向的对象是T,而不是继承WeakReference的子类。例如:
/**
* @author daiwei
* @date 2019/2/26 17:12
*/
public class Salad extends WeakReference<Apple>{
public Salad(Apple referent) {
super(referent);
}
}
Apple才是若引用对象而不是对象Salad,当进行GC时,Apple对象将会为空
4.2 典型的垃圾收集算法
4.2.1 Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间
缺陷:
1.容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2.效率问题 ,如果需要标记的对象太多,效率不高。
4.2.2 Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
即:划分为二,另一块主要用来保存存活的对象,真正可以使用的其实只用一半。
缺陷:
对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。而且Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。(因为存活的对象少,不会占用较大地空间)
4.2.3 Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。但它又会比Mark-Sweep的效率慢很多。
4.2.4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
新生代的Copying算法:
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
老年代的Mark-Compact算法:
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact或者是 Mark-Sweep算法。但“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
三、垃圾收集器
垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。
下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。
上面是目前比较常用的垃圾收集器,和他们直接搭配使用的情况,上面是新生代收集器,下面则是老年代收集器,这些收集齐都有自己的特点,根据不同的业务场景进行搭配使用。如果存在连线表示可以搭配使用。
查看Java的垃圾收集器命令
java -XX:+PrintCommandLineFlags -version
从结果可知Java默认使用的垃圾收集器是:Parallel Scavenge + Parallel Old组合
注意:Ps MarkSweep是以Serial Old为模板设计的
几种机制解释:
串行(Serial):只有一个gc线程工作,如果发生GC 用户线程会暂停。
并行(Parallel):多个垃圾收集线程并行工作,如果发生GC 用户线程会暂停。
并发(concurrent):用户线程和GC线程可以同时执行(不一定并行,可能交替执行),如果发生GC,用户线程依然可以执行。
4.3.1 Serial/Serial Old
jvm参数:(-XX:+UseSerialGC -XX:+UseSerialOldGC)
Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
Serial/Serial Old运行流程如下:
4.3.2 ParNew
ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
ParNew运行流程如下
4.3.3 Parallel Scavenge
jvm参数:-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)。
新生代采用复制算法,老年代采用标记-整理算法。
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。
parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。
吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
Parallel Scavenge提供了两个参数用来精确控制,分别是
1.控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数
2.直接设置吞吐量大小的-XX:GCTimeRatio参数
在注重吞吐量或CPU资源敏感的场合,可以优先考虑Parallel Scavenge收集器 + Parallel Old收集器
4.3.4 Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
4.3.5 CMS(Concurrent Mark Sweep)
CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程 (基本上)同时工作。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
1.初始标记(CMS initial mark)(stw)
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
2.并发标记(CMS concurrent mark)
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但 是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。
因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
如原来GC不可达,后因用户线程又建立了可达关系。垃圾对象变为了非垃圾对象。
3.重新标记(CMS remark)(stw)
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
只会判断旧垃圾的变动如是否已经复活,但对新垃圾不会处理。
主要用到三 色标记里的增量更新算法(见下面详解)做重新标记。
4.并发清除(CMS concurrent sweep)
开启用户线程,同时GC线程开始对未标记的区域做清扫。
这个阶段如果有新增对象会被标记为黑 色不做任何处理(见下面三色标记算法详解)。
执行流程如下
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快.
并发标记阶段就是进行GC Roots Tracing的过程。
而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
4.3.6 G1
G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
G1具备如下特点
1.并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
2.分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
3.空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
4.可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1 GC的流程如下:
1.初始标记
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
2.并发标记
进行可答性分析的过程。
3.最终标记
为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
4.筛选回收
将更据用户期望的回收时间,制定回收计划。
@see https://wangkang007.gitbooks.io/jvm/content/
思考
1.CMS收集器和Parallel Scavenge的对比
1.Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU),并且尽可能缩短STW的时间。
2.CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验),CMS将STW的总时间分散了开来,只有在初始标记和重新标记的时候会STW,用户停顿的感知较少。
2.CMS收集器的优劣对比
优点
并发收集、低停顿。
CMS收集器的不足
1.CMS收集器无法处理浮动垃圾(Floating Garbage)
在并发标记和并发清理阶段又产生新的垃圾,这种浮动垃圾只能等到下一次gc再清理了。
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
2.需要预留空间
也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,
可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。
3.可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
4.CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。然后再整理阶段会提高用户停顿时间。
当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理。
CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
虚拟机设计者还提供了另外一个参数XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)
5.CMS收集器对cpu资源非常敏感,并发过程会和服务抢资源。
会消耗掉大量进程中的线程资源
3. CMS的相关核心参数
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整,建议不要指定,让jvm自己去动态调整。
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
4.4 对象的生命周期-分配过程与垃圾回收
为什么要进行分代?
我们先来屡屡,为什么需要把堆分代?
不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。
你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。
而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来并提升了回收效率。
1. jvm Stop-The-World
Java中一种全局暂停的现象。全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互 多半由于GC引起 Dump线程,死锁检查,堆Dump
GC时为什么会有全局停顿?
类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。类比jvm机制如果在垃圾回收过程中,又有新的垃圾产生,那么此次垃圾回收就并不能清理完所有的垃圾/
危害
长时间服务停止,没有响应。遇到HA系统(HA是Highly Available高可用缩写,是双机集群系统简称,提高可用性集群,是保证业务连续性的有效解决方案,一般有两个或两个以上的节点,且分为活动节点及备用节点),可能引起主备切换,严重危害生产环境。
2 Minor GC、Major GC和Full GC
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:
1.当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
2.执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
3.质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。
其中的真相就是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存,且stop-the-world时间非常短
Major GC 和Full GC
Major GC:清理老年代。
Full GC :清理整个堆空间—包括年轻代和老年代。
这两种GC的stop-the-world时间,都会影响系统的性能。
Full GC触发条件
其实堆内存的Full GC一般都是两个原因引起的,要么是老年代内存过小,要么是老年代连续内存过小。无非是这两点,而元数据区Metaspace引发的Full GC可能是阈值引起的,详细原因还是建议参考其他文章,我就不误人子弟了。
System.gc()方法的调用
在代码中调用System.gc()方法会建议JVM进行Full GC,但是注意这只是建议,JVM执行不执行是另外一回事儿,不过在大多数情况下会增加Full GC的次数,导致系统性能下降,一般建议不要手动进行此方法的调用,可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
老年代(Tenured Gen)空间不足
在Survivor区域的对象满足晋升到老年代的条件时,晋升进入老年代的对象大小大于老年代的可用内存,这个时候会触发Full GC。
Metaspace区内存达到阈值
从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。-XX:MetaspaceSize=21810376B(约为20.8MB)超过这个值就会引发Full GC,这个值不是固定的,是会随着JVM的运行进行动态调整的,与此相关的参数还有多个,详细情况请参考这篇文章jdk8 Metaspace 调优
统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
Survivor区域对象晋升到老年代有两种情况:
一种是给每个对象定义一个对象计数器,如果对象在Eden区域出生,并且经过了第一次GC,那么就将他的年龄设置为1,在Survivor区域的对象每熬过一次GC,年龄计数器加一,等到到达默认值15时,就会被移动到老年代中,默认值可以通过-XX:MaxTenuringThreshold来设置。
另外一种情况是如果JVM发现Survivor区域中的相同年龄的对象占到所有对象的一半以上时,就会将大于这个年龄的对象移动到老年代,在这批对象在统计后发现可以晋升到老年代,但是发现老年代没有足够的空间来放置这些对象,这就会引起Full GC。
堆中产生大对象超过阈值
这个参数可以通过-XX:PretenureSizeThreshold进行设定,大对象或者长期存活的对象进入老年代,典型的大对象就是很长的字符串或者数组,它们在被创建后会直接进入老年代,虽然可能新生代中的Eden区域可以放置这个对象,在要放置的时候JVM如果发现老年代的空间不足时,会触发GC。
老年代连续空间不足
JVM如果判断老年代没有做足够的连续空间来放置大对象,那么就会引起Full GC,例如老年代可用空间大小为200K,但不是连续的,连续内存只要100K,而晋升到老年代的对象大小为120K,由于120>100的连续空间,所以就会触发Full GC。
CMS GC时出现promotion failed和concurrent mode failure
这个原因引发的Full GC可以参考这篇文章,下面也摘抄自这篇文章:JVM 调优 —— GC 长时间停顿问题及解决方法
提升失败(promotion failed),在 Minor GC 过程中,Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为过早提升(Premature Promotion)。 这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步, 如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为提升失败(Promotion Failure)。
在 CMS 启动过程中,新生代提升速度过快,老年代收集速度赶不上新生代提升速度。在 CMS 启动过程中,老年代碎片化严重,无法容纳新生代提升上来的大对象,这是因为CMS采用标记清理,会产生连续空间不足的情况,这也是CMS的缺点
4.5 年轻代中的GC详解
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。
JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
1.长期存活的对象将进入老年代
一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。
对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到老年代中。(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。
对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置.
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄平均值的对象,就可以直接进入老年代了。
例如
Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。
这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的
2.大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
为什么要这样呢?
为了避免为大对象分配内存时在新生代频繁minor gc操作而降低效率。
那么为什么年轻代的垃圾回收算法使用的是复制算法?
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。Minor GC会一直重复这样的过程,直到“To”区被填满之后,会将所有对象移动到年老代中。
java 大对象为何容易oom?
因为大对象是直接是放在了老年代,而要清理老年代的对象就只能通过触发full Gc,并且大对象会占用连续的内存空间,有时哪怕进行了full Gc,也会因为没有连续的内存空间存储也易造成oom.
4 对象的分配过程与垃圾回收流程
注意:在整个GC的流程之中是针对新生代和老年代进行内存清理的操作,而元空间和永久代都不在GC的范围之内。
1.当新创建一个对象的时候,那么对象一定需要在堆内存中分配内存空间。所以就需要对该对象申请内存空间
2.首先会判断Eden区中是否有充足的内存空间,如果有,那么直接将对象保存在Eden区中
3.如果此时Eden中内存空间不足,那么会自动执行一个Minor GC的操作,将伊甸园区的无用的内存空间进行清理,并将存活的对象迁移到对存活区的to区。
清理之后继续判断伊甸园区的内存空间是否充足。如果充足,那么将对象直接在伊甸园区中进行内存分配。
4.如果执行了MinorGC之后发现伊甸园区的内存依然不足,则会对存活区进行判断,如果存活区内存足够。那么将伊甸园区的一部分活跃对象迁移到存活区,随后继续判断伊甸园区内存,此时肯定足够,则在伊甸园区进行新对象的内存分配。
5 如果此时存活区没有足够的内存空间,则继续判断老年区。如果老年区的内存空间充足,则将存活的部分活跃对象保存到老年代,而后存活区会出现剩余空间。随后将伊甸园区的活跃对象迁移到存活区,然后在伊甸园区开辟空间,保存新对象。
6 如果此时老年代也没有剩余空间,则执行FullGC,清理老年代内存
7 如果执行了FullGC之后依然无法保存对象,就会产生OOM异常“OutofMemoryError”。
总结:从流程中可以看出,新创建的对象始终会在伊甸园区开辟空间存储对象,但大对象会直接存储在老年代。
3.一个对象的生命周期自述
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。
直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
思考
1.Eden与Survivor区为何默认是8:1:1?
原因主要有
1.99%以上的对象成为垃圾被回收掉,所以Survivor区没有必要过大。
2.eden区满了就会触发minor gc,所以Eden区太小,容易频繁minor gc。
大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区。
因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可。
如果eden区设置的很小,将会频繁触发minor gc,导致对象的移动增多,gc效率低
老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会先触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM",然后在执行minor gc。
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM。
为何这样做?
可以担保minor gc后,老年代是能够放下存活的对象的,如果已经预知了无法存下,可以提前告知会发生oom.
方法区回收的是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
参考资料
1.浅析JVM 第一篇(JVM执行流程)http://blog.csdn.net/qq_25235807/article/details/61920877
2.JVM的内存区域划分https://www.cnblogs.com/dolphin0520/p/3613043.html
3.Java垃圾回收机制https://www.cnblogs.com/dolphin0520/p/3783345.html
4.浅析JVM 第二篇(JVM内存模型和垃圾回收)https://blog.csdn.net/qq_25235807/article/details/61929343
5.浅析JVM 第三篇(堆内存参数调整) https://blog.csdn.net/qq_25235807/article/details/62054017
6.内存泄漏和内存溢出的区别和联系https://blog.csdn.net/ruiruihahaha/article/details/70270574
7.SpringBoot项目优化和Jvm调优(楼主亲测,真实有效)http://www.cnblogs.com/jpfss/p/9753215.html
8.【随笔】JVM核心:JVM运行和类加载 https://www.jianshu.com/p/d856ee954f9c
9.Java类加载机制及自定义加载器 https://www.cnblogs.com/gdpuzxs/p/7044963.html
10.《深入理解Java虚拟机》周志明著
11.聊聊JVM的年轻代 http://ifeve.com/jvm-yong-generation/
12.Minor GC、Major GC和Full GC之间的区别http://www.importnew.com/15820.html
13.java虚拟机:运行时常量池 https://www.cnblogs.com/xiaotian15/p/6971353.html
更多推荐
所有评论(0)