第5章 JVM调优

##5.1 Java虚拟机内存模型## Java虚拟机内存模型是Java程序运行的基础。JVM将其内存数据分为程序计数器,虚拟机栈,本地方法栈,Java堆和方法区等部分。 程序计数器:用于存放下一条运行的指令; 虚拟机栈和本地方法栈:用于存放函数调用堆栈信息; Java堆:用于存放Java程序运行时所需的对象等数据; 方法区:用于存放程序的类元数据信息; ###5.1.1 程序计数器### 程序计数器是一块很小内存空间。由于Java是支持线程的语言,当线程数量超过CPU数量时,线程之间根据时间片轮询抢夺CPU资源。对于单核CPU而言,每一时刻,只能有一个线程在运行,而其他线程必须被切换出去。为此,每一个线程都必须用一个独立的程序计数器,用于记录下一条要运行的指令。各个线程之间的计数器互不影响,独立工作,是一块线程私有的内存空间。

如果当前线程正在执行一个Java方法,则程序计数器记录正在执行的Java字节码地址,如果当前线程正在执行一个Native方法,程序计数器为空。 ###5.1.2 Java虚拟机栈### Java虚拟机栈也是线程私有的内存空间,它和Java线程在同一时间创建,它保存方法的局部变量,部分结果,并参与方法的调用与返回。

Java虚拟机规范允许Java栈的大小是动态的或者是固定的。在Java虚拟机规范中,定义了两种异常与栈空间有关:StackOverflowError和OutOfMemoryError。如果线程在计算过程中,请求的栈深度大于最大可用的栈深度,则抛出StackOverflowError;如果Java栈可以动态扩展,而在扩展栈的过程中,没有足够的内存空间来支持栈的扩展,则抛出OutOfMemoryError。

在HotSpot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

虚拟机栈在运行时使用一种叫做栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表,操作数栈,动态连接方法和返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作。相应地,方法的返回则表示栈帧的出栈操作。如果方法调用时,方法的参数和局部变量相对较多,那么栈帧中的局部变量表就会比较大,栈帧会膨胀以满足方法调用所需传递的信息。因此,单个方法调用所需的栈空间大小也会比较多。

在此输入图片描述

注意:函数嵌套调用的次数由栈的大小决定。栈越大,函数嵌套调用次数越多。对一个函数而言,它的参数越多,内部局部变量越多,它的栈帧就越大,其嵌套调用次数就会越少。

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。局部变量表用于存放方法的参数和方法内部的局部变量。局部变量表以“字”为单位进行内存的划分,一个字为32位长度。对于long和double型的变量,则占用2个字,其余类型使用1个字。在方法执行时,虚拟机使用局部变量表完成方法的传递,对于非static方法,虚拟机还会将当前对象(this)作为参数通过局部变量表传递给当前方法。

注意:使用jclasslib工具可以深入研究Class类文件的结构,有助于读者对Java语言更深入的了解。

局部变量表的字空间是可以重用的。因为在一个方法体内,局部变量的作用范围并不一定是整个方法体。

// 最大局部变量表容量:2+1=3
public void test1() {
    {
        long a = 0;
    }
    long b = 0;
}
// 最大局部变量表容量:2+2+1=3
public void test2() {
    long a = 0;
    long b = 0;
}

局部变量表的字,对系统GC也有一定影响。如果一个局部变量被保存在局部变量表中,那么GC根就能引用到这个局部变量所指向的内存空间,从而在GC时,无法回收这部分空间。

// GC无法回收,因为b还在局部变量表中
public static void test1() {
    {
        byte[] b = new byte[6*1024*1024];
    }
    System.gc();
    System.out.println("first explict gc over");
}
// GC可以回收,因为赋值为null将销毁局部变量表中的数据
public static void test1() {
    {
        byte[] b = new byte[6*1024*1024];
        b = null;
    }
    System.gc();
    System.out.println("first explict gc over");
}

// GC可以回收,因为变量a复用了b的字,GC根无法找到b
public static void test1() {
    {
        byte[] b = new byte[6*1024*1024];
    }
    int a = 0;
    System.gc();
    System.out.println("first explict gc over");
}

// GC无法回收,因为变量a复用了c的字,b仍然存在
public static void test1() {
    {
        int c = 0;
        byte[] b = new byte[6*1024*1024];
    }
    int a = 0; // 复用c的字
    System.gc();
    System.out.println("first explict gc over");
}

// GC可以回收,因为变量d复用了b的字
public static void test1() {
    {
        int c = 0;
        byte[] b = new byte[6*1024*1024];
    }
    int a = 0; // 复用c的字
    int d = 0; // 复用b的字
    System.gc();
    System.out.println("first explict gc over");
}

在这个函数体内,即使在变量b失效后,又未能定义足够多的局部变量来复用该变量所占的字,变量b仍会在该栈帧的局部变量表中。因此GC根可以引用到该内存块,阻碍了其回收过程。在这种环境下,手工对要释放的变量赋值为null,是一种有效的做法。

当方法一结束,该方法的栈帧就会被销毁,即栈帧中的局部变量表也被销毁。

注意:局部变量表中的字可能会影响GC回收。如果这个字没有被后续代码复用,那么它所引用的对象不会被GC释放。

###5.1.3 本地方法栈### 本地方法栈和Java虚拟机栈的功能很相似,Java虚拟机栈用于管理Java函数的调用,而本地方法栈用于管理本地方法的调用。本地方法并不是用Java实现的,而是使用C语言实现的。

和虚拟机栈一样,它也会抛出StackOverflowError和OutOfMemoryError。 ###5.1.4 Java堆### Java堆分为新生代和老年代两个部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。新生代又可进一步分为:eden,survivor0,survivor1。eden意义为伊甸园,即对象的出生地,大部分对象刚刚建立时,通常会存放在这里。s0和s1为Survivor空间,直译为幸存者,也就是说存放其中的对象至少经历了一次垃圾回收,并得以幸存。如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代。

package com.king.gc;

/**
 * -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * -XX:MaxTenuringThreshold=15 -Xms40M -Xmx40M -Xmn20M
 * @author taomk
 * @version 1.0
 * @since 15-5-10 上午9:46
 */
public class TestHeapGC {
	public static void main(String [] args) {
		byte[] b1 = new byte[1024 * 1024 / 2];
		byte[] b2 = new byte[1024 * 1024 * 8];
		b2 = null;
		b2 = new byte[1024 * 1024 * 8];
		System.gc();
	}
}

###5.1.5 方法区### 方法区也是JVM内存区中非常重要的一块内存区域,与堆空间类似,它也是被JVM中所有线程共享的。方法区主要保存的信息是类的元数据。

方法区中最为重要的是类的类型信息,常量池,域信息,方法信息。类型信息包括类的完整名称,父类的完整名称,类型修饰符和类型的直接接口类表;常量池包括这个类方法,域等信息所引用的常量信息;域信息包括域名称,域类型和域修饰符;方法信息包括方法名称,返回类型,方法参数,方法修饰符,方法字节码,操作数栈和方法栈帧的局部变量区大小及异常表。总之,方法区内保持的信息,大部分来自于class文件,是Java应用程序运行必不可少的重要数据。

在HotSpot虚拟机中,方法区也称为永久区,是一块独立于Java堆的内存空间。虽然叫做永久区,但是在永久区中的对象,同样也是可以被GC回收的。只是对于GC的表现也和Java堆空间略有不同。对永久代GC的回收,通常主要从两个方面分析:(1)GC对永久代常量池的回收;(2)GC对永久代类元数据的回收。

**GC对永久代常量池的回收:**只要常量池中的常量没有被任何地方引用,就可以被回收。String.intern()方法的含义是:如果常量池中已经存在当前String,则返回池中的对象;如果常量池中不存在当前String的对象,则先将String加入常量池,并返回池中的对象引用。

**GC对永久代类元数据的回收:**如果虚拟机确认该类的所有实例已经被回收,并且加载该来的ClassLoader实例也已经被回收,GC就有可能回收该类型。 ##5.2 JVM内存分配参数## ###5.2.1 设置最大堆内存### 最大堆内存可以用-Xmx参数指定。最大堆指的是新生代和老年代的大小之和的最大值,它是Java应用程序的上限。 ###5.2.2 设置最小堆内存### 最小堆内存可以用-Xms参数指定。也就是JVM启动时,所占据的操作系统内存大小。

如果-Xms的数值较小,那么JVM为了保证系统尽可能地在指定内存范围内运行,就会更加频繁地进行GC操作,以释放失效的内存空间,从而,会增加Minor GC和Full GC的次数,对系统性能产生一定影响。

注意:JVM会试图将系统内存尽可能限制在-Xms中,因此,当内存实际使用量触及-Xms指定的大小时,会触发Full GC。因此把-Xms值设置为-Xmx时,可以在系统运行初期减少GC的次数和耗时。

###5.2.3 设置新生代### 参数-Xmn用于设置新生代的大小。新生代的大小一般设置为整个堆空间的1/4到1/3左右。设置-Xmn的效果等同于设置了相同的-XX:NewSize和-XX:MaxNewSize。若设置不同的-XX:NewSize和-XX:MaxNewSize可能会导致内存震荡,从而产生不必要的系统开销。 ###5.2.4 设置持久代### 持久代(方法区)不属于堆的一部分。在HotSpot虚拟机中,使用-XX:MaxPermSize可以设置持久代的最大值,使用-XX:PermSize可以设置持久代的初始大小。持久代的大小直接决定了系统可以支持多少个类定义和多次常量。

一般来说,-XX:MaxPermSize设置为64MB已经可以满足绝大部分应用程序正常工作。如果依然出现永久区溢出,可以将-XX:MaxPermSize设置为128MB。 ###5.2.5 设置线程栈### 线程栈是线程的一块私有空间。在JVM中,可以使用-Xss参数设置线程栈的大小。

在线程中进行局部变量分配,函数调用时,都需要在栈中开辟空间。如果栈的空间分配太小,那么线程在运行时,可能没有足够的空间分配局部变量或者达不到足够的函数调用深度,导致程序异常退出;如果栈空间过大,那么开设线程所需的内存成本就会上升,系统所能支持的线程总数就会下降。

由于Java堆也是向操作系统申请内存空间的,因此,如果堆空间过大,就会导致操作系统可用于线程栈的内存减少,从而间接减少程序所能支持的线程数量。当系统由于内存空间不够而无法创建新的线程时,会抛出OOM异常如下:

java.lang.OutOfMemoryError: unable to create new native thread

根据以上可知,这并不是由于堆内存不够而导致的OOM,而是因为操作系统内存减去堆内存后,剩余的系统内存不足而无法创建新的线程。在这种情况下,可以尝试减少堆内存,以换取更多的系统空间,来解决这个问题。 ###5.2.6 堆的比例分配### -XX:SurvivorRatio:可以设置eden区与survivor区的比例; -XX:NewRatio:可以设置老年代与新生代的比例; ###5.2.7 堆分配参数总结### -Xms:设置Java应用程序启动时的初始堆大小; -Xmx:设置Java应用程序能获得的最大堆大小; -Xss:设置线程栈的大小; -XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当堆空间的空闲内存小于这个数值时,JVM便会扩展堆空间; -XX:MaxHeapFreeRatio:设置堆空间最大空闲比例。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆; -XX:NewSize:设置新生代的初始大小; -XX:MaxNewSize:设置新生代能获得的最大大小; -XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小; -XX:SurvivorRatio:设置新生代中eden区与survivor区的比例; -XX:PermSize:设置永久代的初始大小; -XX:MaxPermSize:设置永久代能获得的最大大小; -XX:TargetSurvivorRatio:设置Survivor区的可使用率。当Survivor区的空间使用率达到这个数值时,会将对象送入老年代; ##5.3 垃圾收集基础## Java语言的一大特点就是可以进行自动垃圾回收处理,而无需开发人员过于关注系统资源(尤指内存资源)的释放情况。 ###5.3.1 垃圾收集的作用### 拥有垃圾收集器可以说是Java语言与C++语言的一项显著区别。在C++语言中,程序员必须小心谨慎地处理每一项内存分配,且内存使用完后,必须手工释放曾经占用的内存空间。当内存释放不够完全时,即存在分配但永不释放的内存块,就会引起内存泄漏,严重时,会导致程序崩溃。 ###5.3.2 垃圾回收算法与思想### 1. 引用计数法 引用计数法是最经典也是最古老的一种垃圾收集算法,其实现也非常简单,只需要为每个对象配备一个整型的计数器即可。但是,引用计数器有一个严重的问题,即无法处理循环引用的情况。因此,在Java的垃圾回收器中,没有使用这种算法。

2. 标记—清除算法 标记—清除算法是现代垃圾回收算法的思想基础。标记—清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记—清除算法可能产生的最大问题就是空间碎片。

该算法回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续的内存空间的工作效率要低于连续的空间。因此,这也是该算法的最大缺点。

3. 复制算法 与标记—清除算法,复制算法是一种相对高效的回收方法。它的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存块的角色,完成垃圾回收。

如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。又由于对象是在垃圾回收过程中统一被复制到新的内存空间中,因此,可确保回收后的内存空间是没有碎片的。虽然有以上两优点,但是,复制算法的代价缺点是将系统内存折半,因此,单纯的复制算法也很难让人接受。

4. 标记—压缩算法 复制算法的高效性是建立在存活对象少,垃圾对象多的前提下。这种情况在年轻代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用新的算法。

标记—压缩算法是一种老年代的回收算法,它在标记—清除算法的基础上做了一些优化。和标记—清除算法一样,标记—压缩算法也首先需要从根节点开始,对所有可达对象做吃一次标记。之后,清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比较高。

5. 增量算法 对大部分垃圾回收算法而言,在垃圾回收的过程中,应用将处于一种Stop the World的状态。在Stop the World状态下,应用所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间很长,应用会被挂起很久,将严重影响用户体验或者系统的稳定性。

增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。以此反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收的过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

6. 分代 分代算法就是基于这种思想,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的收集算法,以提高垃圾回收的效率。该算法被现有的HotSpot虚拟机广泛使用,几乎所有的垃圾回收器都区分年轻代和老年代。 ###5.3.3 垃圾收集器的类型### 按线程数分:串行垃圾回收器和并行垃圾回收器。串行垃圾回收器一次只使用一个线程进行垃圾回收;并行垃圾回收器将开启多个线程同时进行垃圾回收。在并行能力较强的CPU上,使用并行垃圾回收器可以缩短GC的停顿时间。

按工作模式分:并发式垃圾回收器和独占式垃圾回收器。并发式垃圾回收器与应用程序线程交替执行,以尽可能减少应用程序的停顿时间;独占式垃圾回收器一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。

按碎片处理方式分:压缩式垃圾回收器和非压缩式垃圾回收器。压缩式垃圾回收器会在回收完成后,对存活对象进行压缩管理,消除回收后的碎片;非压缩式的垃圾回收器,不进行这步操作。

按工作区间分:新生代垃圾回收器和老年代垃圾回收器。顾名思义,新生代垃圾回收器只在新生代工作;老年代垃圾回收器则工作在老年代。 ###5.3.4 评价GC策略的指标### 吞吐量:指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。系统总运行时间=应用程序耗时+GC耗时。如果系统运行了100min,GC耗时1min,那么系统的吞吐量就是(100-1)/ 100=99%。

垃圾回收器负载:和吞吐量相反,垃圾回收器负载指垃圾回收器耗时与系统运行总时间的比值。

停顿时间:指垃圾回收器正在运行时,应用程序的暂停时间。对于独占回收器而言,停顿时间可能会比较长。使用并发的回收器时,由于垃圾回和应用程序交替执行,程序的停顿时间会变短,但是,由于其效率很可能不如独占回收器,故系统的吞吐量可能会降低。

垃圾回收频率:指垃圾回收器多长时间会运行一次。一般来说,对于固定的应用而言,垃圾回收的频率应该是越低越好。通常增大堆空间可以有效降低垃圾回收发生的频率,但是可能会增加回收产生的停顿时间。

反应时间:指当一个对象称为垃圾后,多长时间内,它所占据的内存空间会被释放。

堆分配:不同的垃圾回收器对堆内存的分配方式可能是不同的。一个良好的垃圾收集器应该有一个合理的堆内存区间划分。

通常情况下,很难让一个应用程序在所有的指标上都达到最优。因此,只能根据应用本身的特点,尽可能使垃圾回收器配合应用程序的工作。 ###5.3.5 新生代串行收集器### 串行收集器是所有垃圾收集器中最古老的一种,也是JDK中最基本的垃圾收集器之一。串行收集器主要有两个特点:第一,它仅仅使用单线程进行垃圾回收;第二,它是独占式的垃圾回收。

在串行收集器进行垃圾回收时,Java应用程序中的线程都需要暂停,等待垃圾回收的完成。这种现象称为“Stop the World”。虽然如此,串行收集器却是一个成熟,经过长时间生产环境考验的极为高效的收集器。新生代串行处理器使用复制算法,实现相对简单,逻辑处理特别高效,且没有线程切换的开销。在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,它的性能表现可以超越并行收集器和并发收集器。

在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定使用新生代串行收集器和老年代串行收集器。当JVM在Client模式下运行时,它是默认的垃圾收集器。 ###5.3.6 老年代串行收集器### 老年代串行收集器使用的是标记—压缩算法。和新生代串行收集器一样,它也是一个串行的,独占式的垃圾回收器。若要启用老年代串行收集器,可以尝试使用以下参数: **-XX:+UseSerialGC:**新生代,老年代都使用串行收集器; **-XX:+UseParNewGC:**新生代使用并行收集器,老年代使用串行收集器; **-XX:+UseParallelGC:**新生代使用并行收集器,老年代使用串行收集器; ###5.3.7 并行收集器### 并行收集器是工作在新生代的垃圾收集器,它只是简单地将串行收集器多线程化。它的回收策略,算法以及参数和串行收集器一样。并行收集器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行收集器使用多线程进行垃圾回收,因此,在并发能力比较强的CPU上,它产生的停顿时间要短于串行回收器,而在单CPU或者并发能力较弱的系统中,并行收集器的效果不会比串行收集器好,由于多线程的压力,它的实际表现很可能比串行收集器差。

开启并行收集器可以使用以下参数: **-XX:+UseParNewGC:**新生代使用并行收集器,老年代使用串行收集器; **-XX:+UseConcMarkSweepGC:**新生代使用并行收集器,老年代使用CMS;

并行收集器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定。一般,最好与CPU数量相当,避免过多的线程数,影响垃圾收集性能。在默认情况下,当CPU数量小于8个时,ParallelGCThreads的值等于CPU数量;当CPU数量大于8个时,ParallelGCThreads的值等于3+[(5*CPU_Count)/8]。 ###5.3.8 新生代并行回收收集器### 新生代并行回收收集器也是使用复制算法的收集器。从表面上看,它和并行收集器一样,都是多线程,独占式的收集器。但是,并行回收收集器有一个重要的特点:它非常关注系统的吞吐量。

新生代并行回收收集器可以使用以下参数启用: -XX:+UseParallelGC:新生代使用并行回收收集器,老年代使用串行收集器。 -XX:+UseParallelOldGC:新生代和老年代都使用并行回收收集器。

并行回收收集器提供了两个重要的参数用于控制系统的吞吐量: (1)-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,它的值是一个大于0的整数。收集器在工作时,会调整Java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMillis以内。如果希望减少停顿时间,而把这个值设得很小,为了达到预期的停顿时间,JVM可能会使用一个较小的堆(一个小堆比一个大堆回收快),而这将导致垃圾回收变得很频繁,从而增加了垃圾回收总时间,降低了吞吐量。 (2)-XX:GCTimeRatio:设置吞吐量大小,它的值一个0~100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。比如GCTimeRatio等于19默认值,则系统用于垃圾收集的时间不超过1/(1+19)=5%。默认情况下,它的取值是99,即不超过1/(1+99)=1%的时间用于垃圾收集。

除此以外,并行回收收集器与并行收集器另一个不同之处在于,它还有一种自适应的GC调节策略。使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。在这种模式下,新生代的大小,eden和survivor的比例,晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小,吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆,目标吞吐量(GCTimeRatio)和最大停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。 ###5.3.9 老年代并行回收收集器### 老年代并行回收收集器也是一种多线程并行的收集器。和新生代并行回收收集器一样,它也是一种关注吞吐量的收集器。老年代并行回收收集器使用标记—压缩算法。

使用-XX:+UseParallelOldGC可以在新生代和老年代都使用并行回收收集器,这是一对非常关注吞吐量的垃圾收集器组合,在对吞吐量敏感的系统中,可以考虑使用。参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。 ###5.3.10 CMS收集器### 与并行回收收集器不同,CMS收集器主要关注于系统停顿时间。CMS是Concurrent Mark Sweep的缩写,意味并发标记清除。从名称上就可以得知,它使用的是标记—清除算法,同时它又是一个使用多线程并行回收的垃圾收集器。

CMS收集器的工作过程与其他垃圾收集器相比,略显复杂。CMS工作时,主要步骤有:初始标记,并发标记,重新标记,并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而并发标记,并发清除和并发重置是可以和用户线程一起执行的。因此,从整体上说,CMS收集不是独占式的,它可以在应用程序运行过程中进行垃圾回收。

根据标记—清除算法,初始标记,并发标记和重新标记都是为了标记出需要回收的对象。并发清理,则是在标记完成后,正式回收垃圾对象;并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数据,为下一次垃圾回收做好准备。并发标记,并发清理和并发重置都是可以和应用程序线程一起执行的。

CMS收集器在其主要的工作阶段虽然没有暴力地彻底暂停应用程序线程,但是由于它和应用程序线程并发执行,相互抢占CPU,故在CMS执行期内对应用程序吞吐量将造成一定影响。CMS默认启动的线程数是((ParallelGCThreads+3)/4),ParallelGCThreads是新生代并行收集器的线程数,也可以通过-XX:ParallelGCThreads参数手工设定CMS的线程数量。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用系统的性能在垃圾回收阶段可能会非常糟糕。

由于CMS收集器不是独占式的回收器,在CMS回收过程中,应用程序仍然在不停地工作。在应用程序工作过程中,又会不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的。同时,因为应用程序没有中断,故在CMS回收过程中,还应该确保应用程序有足够的内存可用。因此,CMS收集器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阀值时,便开始进行回收,以确保应用程序在CMS工作过程中,依然有足够的内存空间支持应用程序运行。

这个回收阀值可以使用-XX:CMSInitiatingOccupancyFraction来指定,默认是68。即,当老年代的空间使用率达到68%时,会执行一次CMS回收。如果应用程序的内存使用率增长很快,在CMS的执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,JVM将启动老年代串行收集器进行垃圾回收。如果这样,应用程序将完全中断,直到垃圾收集完成,这时,应用程序的停顿时间可能很长。

-XX:+UseCMSCompactAtFullCollection开关可以使CMS在垃圾收集完成后,进行一次内存碎片整理。内存碎片的整理不是并发进行的。-XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少次CMS回收后,进行一次内存压缩。

综上,CMS收集器是一个关注停顿的垃圾收集器。同时CMS收集器在部分工作流程中,可以与用户程序同时运行,从而降低应用程序的停顿时间。 ###5.3.11 G1收集器### G1收集器的目标是作为一款服务端的垃圾收集器,因此,它在吞吐量和停顿控制上,预期要优于CMS收集器。G1收集器是基于标记—压缩算法的。因此,它不产生空间碎片,也没有必要在收集完成后,进行一次独占式的碎片整理工作。G1收集器还可以进行非常精确的停顿控制。它可以让开发人员指定在长度为M的时间段中,垃圾回收时间不超过N。

使用以下参数可以启用G1回收器:-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC,设置G1回收器的目标停顿时间:-XX:MaxGCPauseMillis=50 -XX:GCPauseIntervalMillis=200,表示指定在200ms内,停顿时间不能超过50ms,这两个参数是G1回收器的目标,G1回收器并不保证能执行它们。 ###5.3.12 Stop the World### 在JVM垃圾回收时,应用系统会产生一定的停顿。尤其在独占式的垃圾回收器中,整个应用程序会被停止,直到垃圾回收的完成。这种现象称为Stop the World。慎重选择回收器并对其进行调优,是相当重要的。 ###5.3.13 GC相关参数总结###

  1. 与串行回收器相关的参数 **-XX:+UseSerialGC:**在新生代和老年代使用串行收集器; **-XX:SurvivorRatio:**设置Eden区大小和survivor区大小的比例; **-XX:PretenureSizeThreshold:**设置大对象直接进入老年代的阀值。当对象的大小超过这个值时,将直接在老年代分配; **-XX:MaxTenuringThreshold:**设置对象进入老年代的年龄的最大值。每一次MinorGC后,对象年龄就加1。任何大于这个年龄的对象,一定会进入老年代;

  2. 与并行GC相关的参数 **-XX:+UseParNewGC:**在新生代使用并行收集器; **-XX:+UseParallelOldGC:**在老年代使用并行回收收集器; **-XX:ParallelGCThreads:**设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的; **-XX:MaxGCPauseMillis:**设置最大垃圾停顿时间。它的值是一个大于0的整数。收集器在工作时,会调整Java堆大小或者其他一些参数。尽可能地把停顿时间控制在MaxGCPauseMillis以内; **-XX:GCTimeRatio:**设置吞吐量大小,它的值是一个0~100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集; **-XX:+UseAdaptiveSizePolicy:**打开自适应GC策略。在这种模式下,新生代的大小,eden和survivor的比例,晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小,吞吐量和停顿时间之间的平衡点;

  3. 与CMS回收器相关的参数 **-XX:+UseConcMarkSweepGC:**新生代使用并行收集器,老年代使用CMS+串行收集器; **-XX:ParallelCMSThreads:**设置CMS的线程数量; **-XX:CMSInitiatingOccupancyFraction:**设置CMS收集器在老年代空间被使用多少后触发。默认为68%; **-XX:+UseCMSCompactAtFullCollection:**设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理; **-XX:CMSFullGCsBeforeCompaction:**设置进行多少次CMS垃圾收集后,进行一次内存压缩; **-XX:+CMSClassUnloadingEnabled:**允许对类元数据进行回收; **-XX:+CMSParallelRemarkEnabled:**启用并行重标记; **-XX:CMSInitiatingPermOccupancyFraction:**当永久区占用率达到这一百分比时,启动CMS回收(前提是-XX:+CMSClassUnloadingEnabled开启); **-XX:UseCMSInitiatingOccupancyOnly:**表示只在到达阀值的时候,才进行CMS回收; **-XX:+CMSIncrementalMode:**使用增量模式,比较适合单CPU;

  4. 与G1回收器相关的参数 **-XX:+UseG1GC:**使用G1回收器; **-XX:MaxGCPauseMillis:**设置最大垃圾收集停顿时间; **-XX:GCPauseIntervalMillis:**设置停顿间隔时间;

  5. 其他参数 **-XX:+DisableExplicitGC:**禁用显示GC(System.gc());

##5.4 常用调优案例和方法## ###5.4.1 将新对象预留在新生代### 在JVM参数调优中,可以为应用程序分配一个合理的新生代空间, 以最大限度避免新对象直接进入老年代的情况

  1. 通过参数-Xmn6M,分配足够大的新生代空间;通过参数-XX:NewRatio可以指定新生代大小;
  2. 通过参数-XX:TargetSurvivorRatio,提高from区的利用率,使from区使用到90%时,再将对象送入到老年代;通过参数-XX:SurvivorRatio,设置了一个更大的from区;

###5.4.2 大对象进入老年代### 在大部分情况下,将对象分配在新生代是合理的。但是,对于大对象,这种做法确实值得商榷的。因为大对象出现在新生代可能扰乱新生代GC,并破坏新生代原有的对象结构。因为尝试在新生代分配大对象,很可能导致空间不足,为了有足够的连续空间容纳大对象,JVM不得不将新生代中的年轻对象挪到老年代。因为大对象占用空间多,所以,可能需要移动大量小的年轻对象进入老年代。这对GC来说是相当不利的。

基于以上原因, 通过参数-XX:PretenureSizeThreshold设置大对象进入老年代的大小阀值(字节),可以将大对象直接分配到老年代。尤其是短命的大对象,对于垃圾回收是一场灾难。 ###5.4.3 设置对象进入老年代的年龄### 通过参数-XX:MaxTenuringThreshold,设置进入老年代的对象的年龄的阀值。默认值是15。这不意味着新对象非要达到这个年龄才能进入老年代。事实上,对象实际进入老年代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阀值年龄的最大值。即,实际晋升老年代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold中较小的哪个。 ###5.4.4 稳定与震荡的堆大小### 一般来说,稳定的堆大小是对垃圾回收有利的。获得一个稳定的堆大小的方式是使-Xms和-Xmx的大小一致,即最大堆和最小堆一致。如果这样设置,系统在运行时,堆大小是恒定的,稳定的堆空间可以减少GC的次数。

但是,一个不稳定的堆也并不是毫无用处。稳定的堆大小虽然可以减少GC次数,但同时也增加了每次GC的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使GC应对一个较小的堆,可以加快单次GC的速度。基于这样的考虑,JVM提供了两个参数用于压缩和扩展堆空间。

**-XX:MinHeapFreeRatio:**设置堆空间最小空闲比例,默认是40。当堆空间的空闲内存小于这个数值时,JVM便会扩展堆空间。

**-XX:MaxHeapFreeRatio:**设置堆空间最大空闲比例,默认是70。当堆空间的空闲内存大于这个数值时,JVM便会压缩堆空间。

注意:当-Xms和-Xmx相等时,-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio这两个参数是无效的。

###5.4.5 吞吐量优先案例### 吞吐量优先的方案将会尽可能减少系统的执行垃圾回收的总时间,故可以考虑关注系统吞吐量的并行回收收集器。

**-XX:+UseParallelGC:**设置新生代使用并行回收收集器。这是一个关注吞吐量的收集器,可以尽可能地减少GC时间。 **-XX:+UseParallelOldGC:**设置老年代使用并行回收收集器。 **-XX:ParallelGCThreads:**设置用于垃圾回收的线程数,通常情况下可以和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。 ###5.4.6 使用大页案例### 使用大的内存分页可以增强CPU的内存寻址能力,从而提升系统的性能。

**-XX:LargePageSizeInBytes:**设置大页的大小。如:-XX:LargePageSizeInBytes=256m。 ###5.4.7 降低停顿案例### 首先,考虑的是使用关注系统停顿的CMS回收器,其次,为了减少Full GC次数,应尽可能将对象留在新生代。

**-XX:ParallelGCThreads:**设置用于垃圾回收的线程数,通常情况下可以和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。

**-XX:+UseParNewGC:**设置新生代使用并行回收器。 **-XX:+UseConcMarkSweepGC:**设置老年代使用CMS收集器降低停顿。 **-XX:SurvivorRatio:**设置eden区和survivor区的比例为8:1。稍大的survivor空间可以提高在新生代回收生命周期较短的对象的可能性(如果survivor不够大,一些短命的对象可能直接进入老年代,这对系统是不利的)。 **-XX:TargetSurvivorRatio:**设置survivor区的可使用率。这里设置为90%,则允许90%的survivor空间被使用。默认值是50%。故该设置提高了survivor区的使用率。当存放的对象超过这个百分比,则对象会向老年代压缩。因此,这个选项更有助于将对象留在新生代。 **-XX:MaxTenuringThreshold:**设置年轻对象晋升到老年代的年龄。默认值是15次。也就是说对象经过15次MinorGC依然存活,则进入老年代。 ##5.5 实用JVM参数## ###5.5.1 JIT编译参数### JVM的JIT编译器,可以在运行时将字节码编译成本地代码,从而提高函数的执行效率。-XX:CompileThreshold为JIT编译的阀值,当函数的调用次数超过-XX:CompileThreshold时,JIT就将字节码编译成本地机器码。在client模式下,-XX:CompileThreshold取值是1500;在server模式下,取值是10000;JIT编译完成后,JVM便会用本地代码代替原来的字节码解释执行。

JIT编译会花费一定的时间,为了能合理地设置JIT编译的阀值,可以使用-XX:CITime打印出JIT编译的耗时,也可以使用-XX:+PrintCompilation打印出JIT编译的信息。 ###5.5.2 堆快照(堆Dump)### 在性能排查问题中,分析堆快照(Dump)是必不可少的一环。-XX:+HeapDumpOnOutOfMemoryError参数在程序发生OOM时,导出应用程序的当前堆快照。通过参数-XX:HeapDumpPath可以指定堆快照的保存位置。

-Xmx10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/log/dump.hprof ###5.5.3 错误处理### 在系统发生OOM错误时,虚拟机在错误发生时运行一段第三方脚本。比如:当OOM发生时,重置系统:

-XX:OnOutOfMemoryError=/data/reset.bat ###5.5.4 取得GC信息###

  1. 如果需要获得一段简要的GC信息,可以使用-verbose:gc或者-XX:+PrintGC。
  2. 如果需要获得更加详细的信息,可以使用-XX:+PrintGCDetails。
  3. 如果需要在GC发生的时候,打印GC发生的时间,则可以追加使用-XX:+PrintGCTimeStamps选项。
  4. 如果需要查看新生代对象晋升老年代的实际阀值,则可以使用参数-XX:+PrintTenuringDistribution选项。
  5. 如果需要在GC时打印详细的堆信息,则可以打开-XX:+PrintHeapAtGC开关。一旦打开它,那么每次GC时,都将打印堆的使用情况。
  6. 如果需要查看GC与应用程序相互执行的耗时,可以使用-XX:+PrintGCApplicationStoppedTime和-XX:+PrintGCApplicationConcurrentTime参数。它们将分别显示应用程序在GC发生时的停顿时间和应用程序在GC停顿期间的执行时间。还可以使用-Xloggc参数指定GC日志的输出位置,如:-Xloggc:/data/gc.log。

###5.5.5 类和对象跟踪### JVM还提供了一组参数用于获取系统运行时加载,卸载类的信息。-XX:+TraceClassLoading参数用于跟踪类加载情况,-XX:+TraceClassUnloading参数用于跟踪类卸载情况。

如果需要同时跟踪类的加载和卸载信息,可以同时打开这两个开关,也可以使用-verbose:class参数。除了类的跟踪,JVM还提供了-XX:+PrintClassHistogram开关用于打印运行时实例的信息。当此开关被打开时,当ctrl+break被按下,会输出系统内类的统计信息。 ###5.5.6 控制GC### -XX:+DisableExplicitGC选项用于禁止显示的GC操作,即禁止在程序中使用System.gc()触发的Full GC。对应用程序来说,在绝大多数的情况下,是不需要进行类的回收的。因为回收类的性价比非常低,类元数据一旦被载入,通常会伴随应用程序整个声明周期。

如果应用程序不需要回收类,则可以使用-Xnoclassgc参数启动应用程序,那么在GC的过程中,就不会发生类的回收,进而提升GC的性能。因此,如果尝试使用-XX:+TraceClassUnloading -Xnoclassgc参数运行程序,将看不到任何输出,因为系统不会卸载任何类,所以类卸载是无法跟踪到任何信息的。

另一个应用的GC控制参数是-Xincgc,一旦启用这个参数,系统便会进行增量式的GC。增量式的GC使用特定算法让GC线程和应用程序线程交叉执行,从而减小应用程序因GC而产生的停顿时间。 ###5.5.7 使用大页### 对同样大小的内存空间,使用大页后,内存分页的表项就会减少,从而可以提升CPU从虚拟内存地址映射到物理内存地址的能力。在支持大页的操作系统中,使用JVM参数让虚拟机使用大页,从而提升系统性能: -XX:+UseLargePages:启用大页; -XX:LargePageSizeInBytes:指定大页的大小; ###5.5.8 压缩指针### 在64位虚拟机上,应用程序所占内存的大小要远远超出其32为版本(约1.5倍左右)。这是因为64位系统拥有更宽的寻址空间,与32位系统相比,指针对象的长度进行了翻倍。为了解决这个问题,64位的JVM虚拟机可以使用-XX:+UseCompressedOops参数打开指针压缩,从一定程度上减少了内存消耗。可以对以下指针进行压缩:

  1. Class的属性指针(静态成员变量);
  2. 对象的属性指针;
  3. 普通对象数组的每个元素指针;

虽然压缩指针可以节省内存,但是压缩和解压缩指针也会对JVM造成一定的性能损失。 ##5.6 实战JVM调优## ###5.6.1 Tomcat启动加速###

  1. 减少启动过程中的Full GC次数:(1)查看GC发生在哪个代中,相应的增大代内存空间;(2)禁用显示GC;
  2. 进一步减少启动过程中的Minor GC次数:扩大新生代的大小;
  3. 在新生代中使用并行回收收集器:-XX:+UseParallelGC;
  4. 考虑禁用类校验:-Xverify:none;

###5.6.2 JMeter介绍与使用### JMeter是Apache下基于Java的一款性能测试和压力测试工具。它基于Java开发,可对HTTP服务器和FTP服务器,甚至是数据库进行压力测试。(P293) ###5.6.3 WEB应用调优过程###

  1. 合理设置堆大小和永久代大小,减少GC次数;
  2. 禁用显示GC,并去掉类校验;
  3. 一律使用并行回收收集器代替串行收集器;
  4. 尽量将对象保留在新生代中:(1)通过-XX:SurvivorRatio设置了一个较大的survivor区;(2)通过-XX:CMSInitiatingOccupancyFraction的值,将CMS的Full GC触发的阀值设置78%,即当老年代使用到78%时,才触发Full GC。

综上所述,JVM调优的主要过程有:确定堆内存大小(-Xmx,-Xms),合理分配新生代和老年代(-XX:NewRatio,-XX:SurvivorRatio),确定永久代大小(-XX:Permsize,-XX:MaxPermSize),选择垃圾收集,对垃圾收集器进行合理的设置。除此之外,禁用显示GC(-XX:+DisableExplicitGC),禁用类数据回收(-Xnoclassgc),禁用类校验(-Xverify:none)等设置。

转载于:https://my.oschina.net/xianggao/blog/412818

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值