一看就秒懂的java垃圾回收机制


说起垃圾收集(Garbage Collenction,GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC历史比java久远,很久之前,人们就在思考GC需要完成的3件事情:1、哪些内存需要回收?2、什么时候回收?3、如何回收? 本篇博客先介绍JVM的内存结构以及jvm垃圾回收机制两方面去介绍。

JVM内存结构

什么是JVM?

JVM(Java Virtual Machine,Java虚拟机),它是一个虚构出来的计算机,是软件。Java语言最重要的特点就是跨平台运行。JVM就是用来保证java跨平台性。java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行。如图所示:

在这里插入图片描述

JVM的体系结构

JVM体系结构图如下:

在这里插入图片描述

java程序从编译到执行过程流程图如下:

在这里插入图片描述

类装载器

每一个Java虚拟机都由一个类加载器子系统(class loader subsystem),负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。JVM的两种类装载器包括:启动类装载器和用户自定义类装载器,启动类装载器是JVM实现的一部分,用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。(下一篇文章会详细讲解java类加载机制)

执行引擎

执行引擎是JVM的核心部分,执行引擎的作用就是解析JVM字节码指令,得到执行结果。
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台,上的本地机器指令才可以。 简单来说,JVM中 的执行引擎充当了将高级语言翻译为机器语言的译者。

运行时数据区

JVM运行时数据区在逻辑上分为五部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器。

在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代。

在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代。

在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace) 。

1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

JDK1.8内存模型如下图:

在这里插入图片描述

JDK1.7 方法区
方法区的基本理解

方法区与堆一样 是各个线程共享的内存区域。方法区在JVM启动的时候就会被创建 并且它实例的物理内存空间和Java堆一样都可以不连续。方法区的大小 跟堆空间一样 可以选择固定大小或者动态变化。方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类导致方法区溢出 虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久带) Java 8之后 是MetaSpace(元空间) )。关闭JVM就会释放这个区域的内存。

在这里插入图片描述

方法区存储内容
  • 类型信息

    类型信息主要包括:

    1、类型的全限定名(全限定类名:就是类名全称,带包路径的用点隔开,即全限定名 = 包名+类型)

    2、超类的全限定名

    3、类型直接父类的有效名(interface 或者 Object 没有父类)

    4、类型的修饰符(public abstract final 的某个子集)

    5、类型的直接接口的一个有序列表(接口的实现顺序)

  • 域(Field)信息

    1、字段修饰符(public、protect、private、default)

    2、字段的类型

    3、字段名称

  • 方法(Method)信息

    1、方法修饰符

    2、方法返回类型

    3、方法名

    4、方法参数个数、类型、顺序等

    5、方法字节码

    6、操作数栈和该方法在栈帧中的局部变量区大小

    7、异常表

  • 类变量(静态变量)

    指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。

  • 运行时常量池

    一个有效的字节码文件中除了包含类的版本信息、字段、方法壹基金接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table) ,包括各种字面量和对象类型、域和方法的符号引用。

JDK1.8 元空间

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

  • 字符串存在永久代中,现实使用中易出问题, 由于永久代内存经常不够用或发生内存泄露,报出异常 java.lang.OutOfMemoryError: PermGen

  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

  • 即:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

通过new关键字创建的对象都会使用堆内存,堆内存是线程共享的,堆中的对象要考虑线程安全问题,堆也是java垃圾收集器管理的主要区域。

堆内存诊断

1、jps工具

查看当前系统有哪些java进程

2、jmap工具

查看堆内存占用情况

3、jconsole工具

图形界面的,多功能的监测工具,可以连续监测

4、jvisualvm

可视化方式展示虚拟机内容,图像化界面。
虚拟机栈

Java虚拟机栈也是线程私有的,每一条线程都拥有自己私有的Java虚拟机栈,它与线程同时创建。并且生命周期与线程相同。它描述了Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至完成的过程,都对应一个栈帧从入栈到出栈的过程。

线程运行诊断

案例一:cpu占用过多

定位:

  • 用top命令定位哪个进程对cpu占用高

  • ps -H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)

  • jstack进程id,可以根据线程id找到有问题的线程,进一步定位到问题代码的源码

本地方法栈

Java可以通过java本地接口JNI(Java Native Interface)来调用其它语言编写(如C)的程序,在Java里面用native修饰符来描述一个方法是本地方法。本地方法栈就是虚拟机线程调用Native方法执行时的栈,它与虚拟机栈发挥类似的作用。Java虚拟机规范规定该区域也可抛出StackOverFlowError和OutOfMemoryError。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器,是记住下一条JVM指令的执行地址。

特点:

  • 线程私有的,每个线程都有各自的程序计数器

  • 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

JVM垃圾回收机制

哪些内存需要回收?

程序计数器、虚拟机栈、本地方法栈

这3个区域是随着线程而生,随着线程而灭,栈中的栈帧数据随着方法的调用到结束有条不紊的进行着入栈出栈,每一个栈帧中分配多少内部基本上在类结构确定下来(类加载)就已知了。内存分配和回收具有确定性,随着方法的结束或者线程的结束后,内存自然就会被回收了。

java堆和方法区

一个接口中的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也不一样,我们只有在程序处于执行期间才能知道会创建哪些对象,对这部分的内存分配和回收是动态的,垃圾回收主要关注的是这部分的内存。

对于堆中的对象,用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。没有任何对象引用常量池中的常量,也没有其他地方引用这个字面量,如果此时发生内存回收,而且必要的话,这个常量就会被系统清理出常量池,常量池中的其他类(接口)、方法、字段的符号引用也是与此类似。

类需要同时满足以下3个条件才能算是“无用的类”:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例

  • 加载该类的ClassLoader已经被回收

  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

注意:虚拟机可以对满足上述3个条件的无用类进行回收,这里仅仅只是“可以”,而并不是跟对象一样,不适用了就必然会回收。

内存溢出与内存泄漏的概念

随着程序的运行,内存中存在的实例对象、变量等信息占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至会因为可用内存不足造成一些不必要的系统异常,可能会导致内存溢出风险。

内存泄漏

是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。内存泄漏最终会导致内存溢出。

引起内存泄漏的原因

(1)静态集合类,例如HashMap和Vector。如果这些容器为静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前将不能被释放,从而造成内存泄露。

(2)各种连接,例如数据库连接、网络联接以及IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接、当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显示地关闭,将会造成大量的对象无法被回收,从而引起内存泄露。

(3)监听器。在Java语言中,往往会使用到监听器。通常一个应用中会用到多个监听器。但在释放对象的同时往往没有相应地删除监听器,这也可能导致内存泄露。

(4)变量不合理的作用域。一般而言,如果一个变量定义的作用范围大于其使用范围,很有可能会造成内存泄露,另一方面如果没有及时地把对象设置为null,很有可能会导致内存泄露的发生。

内存溢出

是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。

	栈内存溢出
		1、栈帧过多导致溢出。递归调用方法
		2、栈帧过大导致溢出。
	堆内存溢出
		1、循环创建对象
	jdk1.7方法区内存溢出
		1、永久代内存不足导致溢出
	jdk1.8元空间内存溢出
		1、元空间内存不足导致溢出

引起内存溢出的原因

1、内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

2、集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

3、代码中存在死循环或循环产生过多重复的对象实体;

4、使用的第三方软件中的BUG;

5、启动参数内存值设定的过小

如何避免内存泄露、内存溢出?

  1. 尽早释放无用对象的引用。

  2. 程序进行字符串处理时,尽量避免使用String,而应使用StringBuffer。因为每一个String对象都会独立占用内存一块区域。

  3. 尽量少用静态变量。

  4. 避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作。

  5. 尽量运用对象池技术以提高系统性能。

  6. 不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。

  7. 优化配置。

如何判断对象是否存活

为了确定哪些对象是垃圾,JVM需要判断对象是否存活。常用的两种判断对象是否存活的方法有::引用计数法和可达性分析法。下面我们分别来介绍着两种算法。

引用计数算法

引用计数算法:每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。引用计数法实现简单,判定效率也很高,但是它很难解决对象之间相互引循环引用的问题。

public class GCTest {
    public static void main(String[] args) {
       GcObject obj1 = new GcObject();   //Step 1
       GcObject obj2 = new GcObject();   //Step 2

       obj1.instance = obj2;    //Step 3
       obj2.instance = obj1;    //Step 4

       obj1 = null;             //Step 5
       obj2 = null;             //Step 6
    }
}
class GcObject{
    public Object instance = null;
}

在这里插入图片描述

当两个对象互相引用时,如上图所示,执行完step5、step6之后,GcObject1与GcObject2对象的引用计数均不为0,如果采用引用计数法的话,这两个实例所占的内存都不会得到释放,便产生了内存泄漏。

可达性算法

可达性算法:这是目前主流的虚拟机都是采用可达性算法,该算法的核心算法是从GC Roots 对象作为起始点,利用数学中的图论知识,图中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。这里涉及两个概念需要解释一下。一个是GC Roots,一个是可达性。可达性是指:从每一个GC Roots出发找到它所有可达的对象,走过的路径成为引用链,被引用链串起来的就是存活对象。
下图就是可达性分析的描述:

在这里插入图片描述

通过可达性算法,成功解决了引用计数所无法解决的循环依赖问题,只要你无法与GC Root建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于GC Root。

哪些对象可以作为GC Roots对象呢?
1、虚拟机栈的栈中引用的对象
2、本地方法栈中引用的变量
3、方法区的静态属性引用的对象
4、方法区中常量引用的对象

那么,可达性分析有没有问题(缺点)呢?
由于需要从GC Roots开始逐个检查引用,所以耗时是缺点之一,而且在此期间,需要保证整个执行系统的一致性,对象的引用关系不能发生变化,所以会导致GC进行时必须停顿所有Java执行线程(STW),所以这是缺点之二。

注意:即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候他们暂时出于“缓刑”阶段,要真正宣告死亡,一个对象真正死亡,至少要经历两次标记过程:如果对象不可达,那它将会被第一次标记并且进行一次筛选。筛选的条件是此对象是否需要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果对象被判定为有必要执行finalize()方法,会放置在F-Queue的队列之中,并稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,原因是如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那么基本上它就真的被回收了。

Java的四种引用

在JDK1.2之前,一个对象只有“已被引用”和“未被引用”两种状态,这将无法描述某些特殊情况下的对象。比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。在jdk1.2之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。

强引用

Java中默认声明的就是强引用,比如:
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null
软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

弱引用

弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

什么时候会触发垃圾回收

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。但是不建议手动调用该方法,因为GC消耗的资源比较大。

触发场景

1.当Eden区不够用了(Minor GC)

2.老年代空间不够用了

3.方法区空间不够用了

4.System.gc()

垃圾回收算法

前面我们介绍了哪些内存需要回收?什么时候回收?下面我们再介绍一下如何回收。垃圾回收有一系列的垃圾回收算法,如复制算法、标记清除算法、标记整理算法、分代收集算法。

标记-清除法

标记-清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需要回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程。标记-清除算法执行过程如图所示:

标记过程

清除过程

标记-清除算法的缺点:

1、效率问题:标记和清除两个过程的效率都不高。

2、空间问题:标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够连续内存而不得不触发一次垃圾回收,触发一次之后要是内存还不够,就会连续触发,导致OOM。

复制算法

为了解决效率问题,“复制”算法出现了。它是将内存分成大小相等的两部分,每次只使用其中一块,当这一块的内存用完了,就将还活着的对象复制到另一块上面,然后再把已使用过的内存空间进行一次清理。这样,这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要按序分配即可。此方法实现简单,运行高效。

复制算法的执行过程如图:

在这里插入图片描述

在这里插入图片描述

复制算法的缺点很明显:一次性分配的最大内存缩小了一半。现在商业虚拟机都采用这种回收算法去回收新生代。新生代老年代的概念后面在分代收集算法中会详细提到。

标记-整理法

复制算法在对象存活率较高的情况时就要进行许多的复制操作,效率将会变低。更关键的是如果不想浪费一半的空间,就需要额外的空间进行担保,以应对被使用的内存中所有对象都100%存活的的极端情况。所以在老年代一般不能直接选用这种算法。针对老年代的特点,提出了一种称之为"标记-整理"算法。标记过程仍与"标记-清除"中标记的过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界(除存活对象)以外的内存。
标记-整理算法的执行过程如图:
在这里插入图片描述

在这里插入图片描述

标记整理算法不仅可以弥补标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价,标记-整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记-整理算法要低于复制算法。

分代收集算法

当前商业虚拟机都采用的是分代收集算法。根据对象的存活周期将内存划分为几块。一般是将java堆分为新生代与老年代,这样就可以根据各个年代的特点采用最适当的收集算法。如新生代中,每次垃圾收集时都会有大批对象死去,只有少量存活,那就选用复制算法。而老年代中对象的存活率高,没有额外空间对他进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

堆内存分代如下图所示:

在这里插入图片描述

当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From(图中的S0)区;当Eden区再次触发Minor gc的时候,会扫描Eden和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,直接赋值到To(图中的S1)区域,并将Eden和From区域清空。

当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),如果对象还未被释放,就存入老年代。如果老年代空间也用完,那么就会触发Full GC。

垃圾收集器

在这里插入图片描述

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,那说明它们可以搭配使用。虚拟机所处的区域说明它是属于新生代收集器还是老年代收集器。多说一句,我们必须明确一个观点:没有最好的垃圾收集器,更加没有万能的收集器,只能选择对具体应用最合适的收集器。这也是HotSpot为什么要实现这么多收集器的原因。接下来我们一一介绍各种收集器。

Serial收集器

Serial收集器是最基本的、发展历史最悠久的收集器,曾经在(JDK1.3之前)是虚拟机新生代的唯一选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。

优点:简单高效、拥有较高的单线程收集效率
缺点:收集过程需要暂停所有线程(STW)
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样。Serial收集器运行过程如下图所示:

在这里插入图片描述

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括使用的也是复制算法。ParNew收集器除了多线程以外和Serial收集器并没有太多创新的地方,但是它却是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作一般老年代如果使用CMS收集器,则默认会使用ParNew作为新生代收集器。

在这里插入图片描述

优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是打到一个可控制的吞吐量。所谓吞吐量的意思就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。Parallel Scavenge收集器也被称为“吞吐量优先收集器”。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK 1.6之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合。运行过程如下图所示:

在这里插入图片描述

CMS收集器

CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步:

(1). 初始标记,标记GCRoots能直接关联到的对象,需要“Stop The World”,时间很短。

(2). 并发标记,进行GCRoots Tracing(可达性分析:查找与gc roots非直接相连的对象,以GCRoots的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain,当一个对象到GCRoots没有任何ReferenceChain相连时,这个对象不可到达,则证明这个对象不可用)过程,时间很长。

(3). 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。

(4). 并发清除,回收内存空间,时间很长。

其中初始标记和重新标记阶段,要“stop the world”(停止工作线程)。

在这里插入图片描述

优点

并发收集,低停顿,所以CMS收集器适合与用户交互较多的场景,注重服务的响应速度,能给用户带来较好的体验!所以我们在做WEB开发的时候,经常会使用CMS收集器作为老年代的收集器。

缺点:

(1)不能处理浮动垃圾 (在最后一步并发清理阶段,用户线程还在运行,这时候可能就会又有新的垃圾产生,而无法在此次GC过程中被回收,这成为浮动垃圾)

(2)对 cpu 资源敏感,占用CPU资源较大。CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。

(3)产生大量内存碎片(因为使用的是标记-清除算法)

G1垃圾收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1收集器有以下特点:

1、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。

2、分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

3、空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。

4、可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

在这里插入图片描述

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂。如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中设置Region大小:-XX:G1HeapRegionSize=M。所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域。

在这里插入图片描述

初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
G1为什么能建立可预测的停顿时间模型?

因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。

G1与其他收集器的区别:

其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。

G1收集器存在的问题:

Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。

G1收集器是如何解决上述问题的?

采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。

Minor GC与Full GC

MinorGC触发条件

eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。

Full GC触发条件

1、老生代空间不够分配新的内存

2、持久代空间不足

3、显示调用System.gc( 并不一定会立马就触发FullGC)

4、通过Minor GC后进入老年代的平均大小大于老年代的可用内存

5、由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

JVM性能调优

有关JVM性能调优的请参考下文:
JVM性能调优经验总结

相关问题

为什么要分代?

分代的垃圾回收策略,是基于这样一个事实:不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。还有一些对象主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

分代的好处:如果单纯从JVM的功能考虑(用简单粗暴的标记-清理删除垃圾对象),并不需要新生代,完全可以针对整个堆进行操作,但是每次GC都针对整个堆标记清理回收对象太慢了。把堆划分为新生代和老年代有2个好处:

1、简化了新对象的分配(只在新生代分配内存)
2、可以更有效的清除不再需要的对象(死对象)。在新生代中,GC可以快速标记回收“死对象”,而不需要扫描整个堆中的存活一段时间的“老对象”。

为什么不是一块Survivor空间而是两块?

这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次(仅供参考)GC,就可以移到老年代了。问题来了,当我们第一次GC的时候,我们可以把Eden区的存活对象放到Survivor A空间,但是第二次GC的时候,Survivor A空间的存活对象也需要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。

为什么Eden空间这么大而Survivor空间要分的少一点?

新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使用率,缓解Copying算法的缺点。
我看8:1:1就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的1:2的比例也是可以调整的。
新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。

吞吐量是怎么计算的

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)

比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。

什么是STW?

可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化,所以需要将用户的正常的工作线程全部停掉,避免对象的引用关系变化,与可达性分析不一致;导致GC进行时必须停顿所有Java执行线程(称为"Stop The World");(几乎不会发生停顿的CMS收集器中,枚举GC ROOTS时也是必须要停顿的)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值