JVM学习总结

Posted AC_Jobim

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习总结相关的知识,希望对你有一定的参考价值。

JVM学习总结

好的博客:JVM

一、JVM介绍

  1. Java虚拟机是一台执行Java字节码虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
  2. JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回收器,以及可靠的即时编译器。
  3. Java技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
  4. Java虚 拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。

特点:

  • 一次编译,到处运行
  • 自动内存管理
  • 自动垃圾回收功能

JVM是运行在操作系统之上的,它与硬件没有直接的交互

JVM、JRE 、JDK的区别

如何反编译字节码文件

  • 在 .class 文件的同级目录下,执行反编译javap -v StackStruTest.class

    javap -v HelloWorld.class
    

二、内存结构

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地方法接口)。

  • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。

  • Execution engine(执行引擎):执行classes中的指令。

  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。

  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

作用首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

JVM的内存模型:

线程私有的:

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

线程共享的:

  • 方法区(1.8 转到直接内存的元空间)
  • 直接内存 (非运行时数据区的一部分)

2.1 程序计数器

作用

  • 用于保存JVM中下一条所要执行的指令的地址

特点

  • 线程私有
    • CPU会为每个线程分配时间片,当当前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码
    • 程序计数器是每个线程私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
  • 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

2.2 虚拟机栈

  • 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,栈是线程私有的

  • 栈中的数据都是以栈帧(Stack Frame)的格式存在。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。(栈帧中拥有的数据:局部变量表、操作数栈、动态链接、方法出口信息)

  • 每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出

  • 局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。
  • OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。

我们可以使用参数 -Xss 选项来设置线程的最大栈空间栈的大小直接决定了函数调用的最大可达深度。

-Xss1024m		// 栈内存为 1024MBS
-Xss1024k		// 栈内存为 1024KB

2.3 本地方法栈

  • 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务,用来管理本地方法(Native Method)的调用。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

2.4 堆

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

上图所示的 eden 区、s0 区、s1 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

2.5 方法区

方法区演进过程:

  • 在 JDK7 及以前,习惯上把方法区,称为永久代。JDK8开始,使用元空间取代了永久代,元空间使用的是本地内存(JVM 内存之外的部分叫作本地内存)。JDK 1.8后,元空间存放在堆外内存中

  • 《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机 加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

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

常用参数:

  • JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小

    -XX:PermSize=N //方法区 (永久代) 初始大小
    -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
    
  • JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是本地内存。需要使用下面的参数:

    -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
    -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
    

    与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

常量池、运行时常量池、字符串常量池?

  • 运行时常量池在方法区中
  • 字节码文件内部包含了常量池(二进制字节码的组成:类的基本信息、常量池、类的方法定义、包含了虚拟机指令);
  • 字符串常量池在JDK7的时候放到了堆空间

2.5.1 常量池

  • 通过反编译来查看类的信息

    • 二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令).首先看看常量池是什么,反编译如下代码:

      public class HelloWorld 
          public static void main(String[] args) 
              System.out.println("Hello World!");
          
      
      
    • 使用 javap -v HelloWorld.class 命令反编译查看结果

      • 类的基本信息:

      • 常量池:

      • 类方法定义:

2.5.2 运行时常量池

  • 当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

2.5.3 字符串常量池

  • 字符串常量池在JDK7的时候放到了堆空间中
  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池。最后会把串池中的对象返回

字符串常量池 StringTable 为什么要调整位置?

  • JDK7中将StringTable放到了堆空间。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。

  • 使用拼接字符串变量对象创建字符串的过程(字节码分析)

    public class StringTableStudy 
    	public static void main(String[] args) 
    		String a = "a";
    		String b = "b";
    		String ab = "ab";
    		//拼接字符串对象来创建新的字符串
    		String ab2 = a+b; 
    	
    
    

    反编译后的结果

    	 Code:
          stack=2, locals=5, args_size=1
             0: ldc           #2                  // String a
             2: astore_1
             3: ldc           #3                  // String b
             5: astore_2
             6: ldc           #4                  // String ab
             8: astore_3
             9: new           #5                  // class java/lang/StringBuilder
            12: dup
            13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
            16: aload_1
            17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
    ;)Ljava/lang/StringBuilder;
            20: aload_2
            21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
    ;)Ljava/lang/StringBuilder;
            24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
    ing;
            27: astore        4
            29: returnCopy
    

    通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString(),StringBuilder的toString方法底层时通过new String();

    最后的toString方法的返回值是一个新的字符串,但字符串的和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

    String ab = "ab";
    String ab2 = a+b;
    //结果为false,因为ab是存在于串池之中,ab2是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
    System.out.println(ab == ab2); //false
    
  • 使用拼接字符串常量对象的方法创建字符串

    public class StringTableStudy 	public static void main(String[] args) 		String a = "a";		String b = "b";		String ab = "ab";		String ab2 = a+b;		//使用拼接字符串的方法创建字符串		String ab3 = "a" + "b";	
    

    反编译后的结果:

     	  Code:      stack=2, locals=6, args_size=1         0: ldc           #2                  // String a         2: astore_1         3: ldc           #3                  // String b         5: astore_2         6: ldc           #4                  // String ab         8: astore_3         9: new           #5                  // class java/lang/StringBuilder        12: dup        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V        16: aload_1        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;        20: aload_2        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;        27: astore        4        //ab3初始化时直接从串池中获取字符串        29: ldc           #4                  // String ab        31: astore        5        33: returnCopy
    

    可以看到第29行,ab3直接从池中获取值,因为内容常量,javac在编译期会进行优化,结果已在编译期确定为ab

    总结:

    • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。

    • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建

  • StringTable调优

    • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

      -XX:StringTableSize=xxxxCopy
      
    • 考虑是否需要将字符串对象入池,可以通过intern方法减少重复入池

2.6 直接内存

  • 属于操作系统,不是虚拟机运行时数据区的一部分
  • 常用于NIO操作,用于数据缓冲区
  • 不受JVM内存回收管理,使用了Unsafe类来完成直接内存的分配回收

直接内存回收原理(待补)

二、垃圾回收

2.1 标记阶段

垃圾标记阶段:判断对象是否存活

  • 已经死亡的对象, 就会被垃圾回收器进行回收
  • 判断对象存活一般有两种方式:引用计数算法可达性分析算法

2.1.1 引用计数法

  • 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

  • 缺点: 无法解决循环引用的问题, 所以Java没有采用这种方式

    如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。

2.1.2 可达性分析算法

  • JVM中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 可达性分析算法是以根对象集合(GCRoots)为起始点,看能否沿着GC Root对象为起点的引用链找到该对象。如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象

GC Roots可以是哪些元素?

  • 虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等。
  • 本地方法栈内JNI(一般说的Native方法)引用的对象。
  • 方法区中类静态属性引用的对象,比如:Java类的引用类型静态变量
  • 方法区中常量引用的对象,比如:字符串常量池(StringTable)里的引用
  • 所有被同步锁synchronized持有的对象
  • Java虚拟机内部的引用。

2.1.3 强、软、弱、虚四大引用

  1. 强引用(StrongReference)

    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。

    Object o=new Object();   //  强引用o=null;     // 帮助垃圾收集器回收此对象
    
  2. 软引用(SoftReference)

    如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

    软引用的使用:

    public class Demo1 
    	public static void main(String[] args) 
    		final int _4M = 4*1024*1024;
    		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
    		List<SoftReference<byte[]>> list = new ArrayList<>();
    		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
    	
    
    

    软引用结合引用队列使用:(查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合))

    public class Demo1 
    	public static void main(String[] args) 
    		final int _4M = 4*1024*1024;
    		//使用引用队列,用于移除引用为空的软引用对象
    		ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
    		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
    		List<SoftReference<byte[]>> list = new ArrayList<>();
    		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
    
    		//遍历引用队列,如果有元素,则移除
    		Reference<? extends byte[]> poll = queue.poll();
    		while(poll != null) 
    			//引用队列不为空,则从集合中移除该元素
    			list.remove(poll);
    			//移动到引用队列中的下一个元素
    			poll = queue.poll();
    		
    	
    
    
  3. 弱引用(WeakReference)

    对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

    //垃圾回收机制一运行,会回收该对象占用的内存
    WeakReference<MyObject> weakReference = new WeakReference<>(new Object());
    
  4. 虚引用(PhantomReference)

    顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。

    虚引用必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

    ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue();//和引用队列进行关联,当虚引用对象被回收后,会进入ReferenceQueue队列中
    PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);
    

注意:对于清除的时候是把起始和结束地址记录到一个空闲地址的列表里

2.2 垃圾回收算法

2.2.1 标记-清除算法

标记清除算法分为**“标记”和“清除”阶段**:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

何为清除?

  • 这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。

标记-清除算法的缺点:

  • 容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢

2.2.2 标记-复制算法

  • 将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

优点

  • 没有标记和清除过程,实现简单,运行高效
  • 复制过去以后保证空间的连续性,不会出现“碎片”问题。
  • 特别适合垃圾对象很多,存活对象很少的场景

缺点:

  • 可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制

2.2.3 标记-整理算法

新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。

因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收

**优点:**解决了标记-清理算法存在的内存碎片问题。

**缺点:**仍需要进行局部对象移动,一定程度上降低了效率。

2.2.4 分代收集算法

  • 根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

  • 比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。

  • 老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

为什么要使用分代收集算法?

  • 不同的对象生命周期是不一样的, 因此, 不同生命周期的对象, 可以采用不同的收集方式, 以便提高回收效率
  • 分为年轻代、老年代, 对这两种分代收集

简述分代垃圾回收器是怎么工作的?

  • 分代回收器有两个分区:老年代和新生代。新生代默认的空间占比总空间的 1/3,老年代的默认占比是 2/3.

  • 新创建的对象都被放在了新生代的伊甸园

  • 当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

  • Minor GC 会将伊甸园和幸存区FROM存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换两个幸存区

  • 再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

  • 如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中。大对象也会直接进入老年代。

  • 如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC(使用标记整理或者标记清除算法),扫描新生代和老年代中所有不再使用的对象并回收

大对象处理策略:

  • 当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

System.gc() 方法 或 Runtime.getRuntime().gc()

  • 主动触发Full GC, 同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存

2.3 垃圾回收器(难点)

2.3.1 垃圾收集器概述

垃圾回收器的分类:

  • 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器


    串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。

    • 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中
    • 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器

    和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-World”机制。

  • 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器

    • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
    • 独占式垃圾回收器(Stop the World)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

  • 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器

    • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。再分配对象空间使用指针碰撞
    • 非压缩式的垃圾回收器不进行这步操作,分配对象空间使用空闲列表
  • 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器

评估 GC 的性能指标

吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)

垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。Stop The World

收集频率:相对于应用程序的执行,收集操作发生的频率。

内存占用:Java堆区所占的内存大小。

快速:一个对象从诞生到被回收所经历的时间。

主要两点:吞吐量和暂停时间

吞吐量(throughput)

  1. 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
    • 比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
  2. 这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的
  3. 吞吐量优先,意味着在单位时间内,STW的时间最短:0.2+0.2=0.4

暂停时间(pause time)

  1. “暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态。
    • 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的
  2. 暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5,但是总的GC时间可能会长

标准:在最大吞吐量优先的情况下,降低停顿时间

7款经典收集器:

  1. 两个收集器间有连线,表明它们可以搭配使用:
    • Serial/Serial old
    • Serial/CMS (JDK9废弃)
    • ParNew/Serial Old (JDK9废弃)
    • ParNew/CMS
    • Parallel Scavenge/Serial Old (预计废弃)
    • Parallel Scavenge/Parallel Old
    • G1
  2. 其中Serial Old作为CMS出现”Concurrent Mode Failure”失败的后备预案。
  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
  4. (绿色虚线)JDK14中:弃用Parallel Scavenge和Serial Old GC组合(JEP366)
  5. (青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)

新生代收集器:Serial、ParNew、Parallel Scavenge;

老年代收集器:Serial old、Parallel old、CMS;

整堆收集器:G1


串行回收器:Serial、Serial old

并行回收器:ParNew、Parallel Scavenge、Parallel old

并发回收器:CMS、G1

2.3.2 Serial 回收器:串行回收

Serial 回收器:串行回收

  1. Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
  2. Serial收集器用于执行年轻代垃圾收集,采用复制算法、串行回收和”Stop-the-World”机制的方式执行内存回收
  3. Serial Old收集器执行老年代垃圾收集,采用了串行回收、标记-压缩算法和”Stop the World”机制
  4. Serial Old在Server模式下主要有两个用途:①在jdk1.5与新生代的Parallel Scavenge配合使用②作为老年代CMS收集器的后备垃圾收集方案

这个收集器是一个单线程的收集器,“单线程”的意义:它只会使用一个CPU(串行)或一条收集线程去完成垃圾收集工作。更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)

Serial 回收器的优势

  1. 优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。运行在Client模式下的虚拟机是个不错的选择。
  2. 在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。
    • 等价于新生代用Serial GC,且老年代用Serial Old GC

2.3.3 ParNew 回收器:并行回收

  1. 如果说Serial GC是年轻代中的单线程垃圾收集器,而ParNew收集器则是Serial收集器的多线程版本(用于年轻代)
    • Par是Parallel的缩写,New:只能处理新生代
  2. ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、”Stop-the-World”机制
  3. ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用

ParNew 回收器与 Serial 回收器比较:

  1. ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。

  2. 但是在单个CPU的环境下,ParNew收集器不比Serial收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。

设置 ParNew 垃圾回收器:

  1. 在程序中,开发人员可以通过选项”-XX:+UseParNewGC”手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
  2. -XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。

2.3.4 Parallel 回收器:吞吐量优先

Parallel Scavenge回收器和Parallel Old回收器:

  • HotSpot的年轻代中**Parallel Scavenge收集器同样也采用了复制算法、并行回收和”Stop the World”机制**。

那么Parallel收集器的出现是否多此一举?

  • 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器
  • 自适应调节策略也是Parall el Scavenge与ParNew一个重要区别。(动态调整内存分配情况,以达到一个最优的吞吐量或低延迟)
  • Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和”Stop-the-World”机制。
  • 在Java8中,默认是此垃圾收集器(Parallel Scavenge+Parallel Old)

Parallel Scavenge 回收器参数设置:

  1. -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。

  2. -XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器。

    • 分别适用于新生代和老年代
    • 上面两个参数分别适用于新生代和老年代。默认jdk8是开启的。默认开启一个,另一个也会被开启。(互相激活)
  3. -XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。

    1. 在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。
    2. 当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU_Count]/8]
  4. -XX:MaxGCPauseMillis: 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。

    1. 为了尽可能地把停顿时间控制在XX:MaxGCPauseMillis 以内,收集器在工作时会调整Java堆大小或者其他一些参数。
    2. 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
    3. 该参数使用需谨慎。
  5. -XX:GCTimeRatio垃圾收集时间占总时间的比例,即等于 1 / (N+1) ,用于衡量吞吐量的大小。

    1. 取值范围(0, 100)。默认值99,也就是垃圾回收时间占比不超过1。
    2. 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,STW暂停时间越长,Radio参数就容易超过设定的比例。
  6. -XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略

    1. 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
    2. 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。

2.3.5 CMS 回收器:低延迟(重点)

CMS 回收器:

  1. 在JDK1.5时期,Hotspot推出了一款在**强交互应用中(就是和用户打交道的引用)**几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
  2. CMS(Concurrent Mark Sweep)收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
  3. CMS的垃圾收集算法采用标记-清除算法,并且也会”Stop-the-World”