01学习预热篇(D6_正式踏入JVM深入学习前的铺垫)

目录

学习前言

一、虚拟机的结构

[1. Java虚拟机参数设置](#1. Java虚拟机参数设置)

[2. java 堆](#2. java 堆)

[3. 出入栈](#3. 出入栈)

[4. 局部变量表](#4. 局部变量表)

[1> 局部变量的剖析](#1> 局部变量的剖析)

[2> 局部变量的回收](#2> 局部变量的回收)

[5. 操作数栈](#5. 操作数栈)

[1> 常量入栈指令](#1> 常量入栈指令)

[2> 局部变量值转载到栈中指令](#2> 局部变量值转载到栈中指令)

[3> 将栈顶值保存到局部变量中指令](#3> 将栈顶值保存到局部变量中指令)

[6. 帧数据区](#6. 帧数据区)

[7. 栈上分配](#7. 栈上分配)

[8. 类都去哪了?识别方法区](#8. 类都去哪了?识别方法区)

[二、常用的 java 虚拟机参数](#二、常用的 java 虚拟机参数)

[1. 跟踪调试参数](#1. 跟踪调试参数)

[2. 系统参数查看](#2. 系统参数查看)

三、堆内存的参数配置

[1. 最大堆和初始堆的设置](#1. 最大堆和初始堆的设置)

[2. 新生代的配置](#2. 新生代的配置)

[-XX: SurvivorRatio](#-XX: SurvivorRatio)

-XX:NewRatio=老年代/新生代

[3. 堆溢出参数](#3. 堆溢出参数)

四、非堆内存的参数配置

[1. 方法区配置](#1. 方法区配置)

[2. 栈配置](#2. 栈配置)

[3. 直接内存配置](#3. 直接内存配置)

[4. 虚拟机的工作模式Server和Client](#4. 虚拟机的工作模式Server和Client)

五、垃圾回收算法

[1. 垃圾回收的思想](#1. 垃圾回收的思想)

[2. 引用计数法](#2. 引用计数法)

[3. 标记清除法](#3. 标记清除法)

[4. 复制算法](#4. 复制算法)

[5. 标记压缩算法](#5. 标记压缩算法)

[6. 分代算法](#6. 分代算法)

[7. 分区算法](#7. 分区算法)

六、谁是真正的垃圾

[1. 对象复活](#1. 对象复活)

[2. 引用和可触及性的强度](#2. 引用和可触及性的强度)

强引用

软引用

弱引用

虚引用

七、垃圾回收器

[1. 串行回收器](#1. 串行回收器)

垃圾回收的停顿

新生代的串行

优点

缺点

应用场景

老年代的串行

[2. 并行回收器](#2. 并行回收器)

新生代并行回收器ParNew

新生代并行垃圾回收器ParallelGC

老年代并行垃圾回收器ParallelOldGC

[3. CMS 垃圾回收器](#3. CMS 垃圾回收器)

[CMS 参数设置](#CMS 参数设置)

[CMS 日志](#CMS 日志)

[4. G1回收器](#4. G1回收器)

新生代GC

回收过程

必要时FullGC

G1的参数设置

对象何时进入老年代

老年对象进入老年代

大对象进入老年代

方法finalize对垃圾回收器的影响

[5. 知识小结](#5. 知识小结)

与串行回收器相关的参数

与并行GC相关的参数

与CMS回收器相关的参数

与G1回收器相关的参数

八、性能监控工具

[1. java 进程 Jps](#1. java 进程 Jps)

[2. jstat 查看虚拟机运行时信息](#2. jstat 查看虚拟机运行时信息)

-class

-gc

-gccapacity

-gcmetacapacity

-gcnew

-gcnewcapacity

-gcold

-gcoldcapacity

-gccause

[3. 导出堆到文件 jmap](#3. 导出堆到文件 jmap)

[4. JDK自带堆分析工具 jhat](#4. JDK自带堆分析工具 jhat)

[5. 可视化性能监控工具 Visual VM](#5. 可视化性能监控工具 Visual VM)

[1> 监控概况](#1> 监控概况)

[2> ThreadDump分析](#2> ThreadDump分析)

[3> 性能分析](#3> 性能分析)

[4> 内存快照分析](#4> 内存快照分析)

九、堆分析

[1. 堆溢出 OOM](#1. 堆溢出 OOM)

[2. 直接内存溢出](#2. 直接内存溢出)

[3. 过多的线程导致OOM](#3. 过多的线程导致OOM)

[4. 元数据溢出](#4. 元数据溢出)

[5. String字符串的优化](#5. String字符串的优化)

[1> 不变性](#1> 不变性)

[2> 字符串常量池的优化](#2> 字符串常量池的优化)

[3> 字符串常量的位置](#3> 字符串常量的位置)

[6. 使用MAT分析java堆](#6. 使用MAT分析java堆)

[1> 初始MAT](#1> 初始MAT)

[2> 浅堆和深堆](#2> 浅堆和深堆)

[3> 支配树](#3> 支配树)

[7. tomcat 溢出分析](#7. tomcat 溢出分析)

[1> 准备tomcat](#1> 准备tomcat)

[2> 准备Jemeter测试工具进行测试](#2> 准备Jemeter测试工具进行测试)


学习前言

正式踏入JVM深入学习前,我们先简单的走一个流程,通过代码结合JVM的形式,先搭建一套专门

学习JVM的项目板块,以便于后续拓展JVM原理的时候,更加方便迭代!

一、虚拟机的结构

1、类加载子系统

类加载子系统负责从文件系统或者网络中加载 Class 信息,加载的类信息存放于一块称为方法区的

内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,

包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

2、Java堆

在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。

几乎所有的Java对象实例都存放于Java堆中。

堆空间是所有线程共享的,这是一块与Java应用密切相关的内存区间。

3、直接内存

Java的NIO库允许Java程序使用直接内存。

直接内存是在Java堆外的、直接向系统申请的内存区间。

通常,访问直接内存的速度会优于Java堆。

因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。

由于直接内存在Java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,

但是系统内存是有限的,Java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。

4、垃圾回收器

垃圾回收器是Java虛拟机的重要组成部分,垃圾回收器可以对方法区、Java堆和直接内存进行回

收。其中,Java 堆是垃圾收集器的工作重点。

5、Java栈

每一个Java虚拟机线程都有一个私有的 Java栈。

一个线程的Java栈在线程创建的时候被创建。

Java 栈中保存着帧信息,Java 栈中保存着局部变量、方法参数,同时和Java方法的调用、返回密

切相关。

本地方法栈和Java栈非常类似,最大的不同在于Java栈用于Java方法的调用,而本地方法栈则用

于本地方法调用。

作为对Java虛拟机的重要扩展,Java虚拟机允许Java直接调用本地方法(通常使用C编写)。

6、PC ( Program Counter)寄存器

PC ( Program Counter)寄存器也是每个线程私有的空间,Java 虚拟机会为每一个 Java线程创建

PC寄存器。

在任意时刻,一个Java 线程总是在执行一个方法,这个正在被执行的方法称当前方法。

7、执行引擎

执行引擎是Java虚拟机的最核心组件之一, 它负责执行虚拟机的字节码。

现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。

1. Java虚拟机参数设置

Java虚拟机可以使用JAVA_ HOME/bin/java 程序启动(JAVA_ HOME为JDK的安装目录),

一般来说,Java 进程的命令行使用方法如下:

java [-options] class [args...]

其中,

-options表示Java虚拟机的启动参数,

class 为带有main()函数的Java类,

args 表示传递给主函数main()的参数。

如果需要设定特定的 Java 虚拟机参数,在options处指定即可。

目前,Hotspot 虚拟机支持大量的虚拟机参数,可以帮助开发人员进行系统调优和故障排查。

看下面的示例:

public class SimpleArgs {
    public static void main (String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println("参数" + (i + 1) + ":" + args[i]);
            System.out.println("-Xmx" + Runtime.getRuntime().maxMemory() / 1000 / 1000 + "M");
        }
    }
}

从结果可以看到,第一个参数-Xmx32m传递给Java虚拟机,生效后,使得系统最大可用堆空间为32MB,

参数a则传递给主函数main作为应用程序的参数。

2. java 堆

Java堆是和Java应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆中。

并且Java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显式地释

根据垃圾回收机制的不同,Java堆有可能拥有不同的结构。

最为常见的一种构成是将整个Java堆分为新生代和老年代。

新生代包含Eden+Survivor区,survivor区里面分为from和to区,

内存回收时,如果用的是复制算法,从from复制到to(我们也叫s0,s1),

当经过一次或者多次GC之后,存活下来的对象会被移动到老年区其中,

新生代有可能分为eden区、s0区、s1区,s0 和s1也被称为from和to区域,它们是两块大小相等、可以互换角色

的内存空间。

在绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1,

之后,每经过一次新生代回收, 对象如果存活,它的年龄就会加1。

当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。

下面通过一个简单的示例,来展示Java堆、方法区和Java栈之间的关系。

public class SimpleHeap {
    private int id;

    public SimpleHeap(int id) {
        this.id = id;
    }

    public void show() {
        System.out.println("My ID is " + id);
    }

    public static void main(String[] args) {
        SimpleHeap s1 = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);
        s1.show();
        s2.show();
    }
}

上述代码声明了一个SimpleHeap类,并在main(函数中创建了两个SimpleHeap实例。

此时,各对象和局部变量的存放如图所示。

SimpleHeap实例本身分配在堆中,描述SimpleHeap类的信息存储在方法区,

main中的s1和s2的局部变量存储在栈中并且指向两个实例。

3. 出入栈

Java栈是一块线程私有的内存空间。

如果说,Java堆和程序数据密切相关,那么Java栈就是和线程执行密切相关的。

线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。

Java栈只支持出栈和入栈两种操作。

栈帧:

在Java栈中保存的主要内容为栈帧。

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

如图所示:

函数1对应栈帧1,函数2对应栈帧2,依此类推。

函数1中调用函数2,函数2中调用函数3,函数3中调用函数4。

当函数1被调用时,栈帧1入栈;当函数2被调用时,栈帧2入栈;当函数3被调用时,栈帧3入栈;当函数4被调用时,栈

帧4入栈。

当前正在执行的函数所对应的帧就是当前的帧(位于栈顶),它保存着当前函数的局部变量、中间运算结果等数据。

当函数返回时,栈帧从Java栈中被弹出。

Java 方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。

不管使用哪种方式,都会导致栈帧被弹出。

在一个栈帧中,至少要包含局部变量表、操作数栈和帧数据区几个部分。

注意:

由于每次函数调用都会生成对应的栈帧,从而占用一定的栈空间,因此,如果栈空间不足,那么函数调用自然无

法继续进行下去。

当请求的栈深度大于最大可用栈深度时,系统就会抛出StackOverflowError 栈溢出错误。

Java虚拟机提供了参数-Xss来指定线程的最大栈空间,这个参数也直接决定了函数调用的最大深度。

下面的代码是一个递归调用,由于递归没有出口,这段代码可能会出现栈溢出错误,在抛出错误后,程序打印了最大的调用深度。

public class TestStackDeep {
    private static int count = 0;


    public static void recursion(long a, long b, long c) {
        long e=1, f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
        count++;
        recursion(a,b,c) ;
    }

    public static void recursion() {
        count++;
        recursion();
}

    public static void main(String args[]) {
        try {
            recursion(1,2,3);
        } catch (Throwable e) {
            System.out.println("deep of calling = " + count);
            e.printStackTrace();
        }
    }
}

-Xss128k 的参数来执行代码

-Xss256k 的参数来执行代码

可以看到,在进行大约2700次调用后,发生了栈溢出错误,通过增大-Xss的值,可以获得更深的调用层次,

尝试使用参数-Xss256K执行上述代码,可能产生如下输出,很明显,调用层次有明显的增加。

4. 局部变量表

1> 局部变量的剖析

局部变量表用于保存函数的参数以及局部变量。

局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之

销毁。

由于局部变量表在栈帧之中,因此,如果函数的参数和局部变量较多,会使得局部变量表膨胀,从而每一次函数

调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。

示例:

下面的代码演示了这种情况,第1个recursion()函数含有3个参数和10个局部变量,因此,其局部变量表含有13个

变量。

而第2个recursion()函数不含有任何参数和局部变量。

当这两个函数被嵌套调用时,第2个recursion()函数可以拥有更深的调用层次。

public class TestStackDeep {
    private static int count = 0;


    public static void recursion(long a, long b, long c) {
        long e=1, f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
        count++;
        recursion(a,b,c) ;
    }

    public static void recursion() {
        count++;
        recursion();
    }

    public static void main(String args[]) {
        try {
            recursion(1,2,3);
        } catch (Throwable e) {
            System.out.println("deep of calling = " + count);
            e.printStackTrace();
        }
    }
}

-Xss128k调用无参数的方法

-Xss128k调用有参数的方法

可以看到,在相同的栈容量下,局部变量少的函数可以支持更深的函数调用。

使用javap -s -l 字节马文件 查看局部变量信息!

每次用命令操作非常麻烦且不易读,使用jclasslib工具可以更进一步查看函数的局部变量信息。

IDEA安装jclasslib工具

安装好后:

点开之后非常方便查看:

下图显示了第一个recursion()函数的最大局部变量表的大小为26个字。

因为该函数包含总共13 个参数和局部变量,且都为long型,

long 和double在局部变量表中需要占用2个字,其他如int、short、 byte、 对象引用等占用1个字。

第一个方法

第二个方法

可以看到,在Class文件的局部变量表中,显示了每个局部变量的作用域范围、所在槽位的索引(index 列)、变量名

(name 列)和数据类型(J 表示long型)。

栈帧中的局部变量表中的槽位是可以重用的,

如果一个 局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽

位,从而达到节省资源的目的。

示例:

下面的代码显示了局部变量表槽位的复用。

在localvar1()函数中,局部变量a和b都作用到了函数末尾,故b无法复用a所在的位置。

而在localvar2()函数中,局部变量a在第16行时不再有效,故局部变量b可以复用a的槽位(1个字)。

我们看到localvar1有三个槽位

我们看到localvar2中槽位1得到复用,b复用了a的槽位

2> 局部变量的回收

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都是不会被回收的。

因此,理解局部变量表对理解垃圾回收也有一定帮助。

示例:

public class LocalVarGCTest {

    public void localvarGc1() {
        byte[] a = new byte[6 * 1024 * 1024];
        System.gc();
    }

    public void localvarGc2() {
        byte[] a = new byte[6 * 1024 * 1024];
        a = null;
        System.gc();
    }

    public void localvarGc3() {
        {
            byte[] a = new byte[6 * 1024 * 1024];
        }
        System.gc();
    }

    public void localvarGc4() {
        {
            byte[] a = new byte[6 * 1024 * 1024];
        }

        int c = 10;
        System.gc();
    }

    public void localvarGc5() {
        localvarGc1();
        System.gc();
    }

    public static void main(String[] args) {
        LocalVarGCTest ins = new LocalVarGCTest();
        ins.localvarGc4();
    }
}

上述代码中,每一个localvarGc函数都分配了一块6MB的堆空间,并使用局部变量引用这块空间。

在localvarGc1中,在申请空间后,立即进行垃圾回收,很明显,由于byte 数组被变量a引用,因此无法回收这块

空间。

在localvarGc2中,在垃圾回收前,先将变量a置为null,使byte数组失去强引用,故垃圾回收可以顺利回收byte数

组。

对于localvarGc3, 在进行垃圾回收前,先使局部变量a失效,虽然变量a已经离开了作用域,

但是变量a依然存在于局部变量表中,并且也指向这块byte数组,故byte数组依然无法被回收。

对于localvarGc4,在垃圾回收之前,不仅使变量a失效,更是申明了变量c,使变量c复用了变量a的字,由于变量

a此时被销毁,故垃圾回收器可以顺利回收byte数组。

对于localvarGc5,它首先调用了localvarGc1很明显,在localvarGc1中并没有释放byte数组,但在localvarGc1

返回后,它的栈帧被销毁,自然也包含了栈帧中的所有局部变量,故byte数组失去引用,在localvarGc5的垃圾回

收中被回收。

可以使用参数-XX:+PrintGC执行上述几个函数,在输出的日志中,可以看到垃圾回收前后堆的大小,进而推断

byte数组是否被回收。

下面的输出是函数localvarGc4的运行结果:

从日志中可以看到,堆空间从回收前的10081KB变为回收后的816KB,释放了约很多空间。

进而可以推断,byte 数组已被回收释放

5. 操作数栈

操作数栈也是栈帧中重要的内容之一,它主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存

储空间。

操作数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操作。

许多Java字节码指令都需要通过操作数栈进行参数传递。

比如iadd指令,它就会在操作数栈中弹出两个整数并进行加法计算,计算结果会被入栈,

如图所示,显示了iadd前后操作数栈的变化。

1> 常量入栈指令

2> 局部变量值转载到栈中指令

3> 将栈顶值保存到局部变量中指令

6. 帧数据区

Java栈帧需要一些数据来支持常量池解析、正常方法返回和异常处理等。

大部分Java字节码指令需要进行常量池访问,在帧数据区中保存着访问常量池的指针,方便程序访问常量池。

7. 栈上分配

栈上分配是Java虚拟机提供的一项优化技术,它的基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以

将它们打散分配在栈上,而不是分配在堆上。

分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。

栈上分配的一个技术基础是进行逃逸分析。

逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。

如下代码显示了一"个逃逸的对象:

private static User u;

public static void alloc() {
    u=new User ();
    u.id=5;
    u.name="geym";
}

对象 User u是类的成员变量,该字段有可能被任何线程访问,因此属于逃逸对象。

而以下代码片段显示了一个非逃逸的对象:

public static void alloc1() {
    User u=new User();
    u.id=5;
    u.name="geym";
}

在上述代码中,对象User以局部变量的形式存在,并且该对象并没有被alloc()函数返回,或者出现了任何形式的公开,因此,它并未发生

逃逸,所以对于这种情况,虚拟机就有可能将User分配在栈上,而不在堆上。

下面这个简单的示例显示了对非逃逸对象的栈上分配。

public class OnStackTest {
    public static class User {
        public int id = 0;
        public String name = "";
    }
    public static void alloc() {
        User u=new User() ;
        u.id=5;
        u.name="geym";
    }

    public static void main(String[] args) throws InterruptedException {
        long b = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
            long e = System.currentTimeMillis();
            System.out.println(e - b);
        }
    }
}

上述代码在主函数中进行了1亿次alloc(调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累

计分配空间达到将近1.5GB,如果堆空间小于这个值,就必然会发生GC。

使用如下参数运行上述代码:

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations

这里使用参数-server执行程序,因为在Server模式下,才可以启用逃逸分析。

参数-XX:+DoEscapeAnalysis启用逃逸分析,-Xmx 10m指定了堆空间最大为10MB,显然,如果对象在堆上分

配,必然会引起大量的GC。

如果GC真的发生了。

参数-XX:+PrintGC 将打印GC日志。

参数-XX:+EliminateAllocations开启了标量替换(默认打开),允许将对象打散分配在栈上。

比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配。

参数-XX:-UseTLAB关闭了TLAB。

可以看到,没有任何形式的GC输出,程序就执行完毕了。

说明在执行过程中,User 对象的分配过程被优化。

如果关闭逃逸分析或者标量替换中任何一个,再次执行程序,就会看到大量的GC日志,

说明栈上分配依赖逃逸分析和标量替换的实现。

知识小结

对于大量的零散小对象,栈上分配提供了一种很好的对象分配优化策略,栈上分配速度快,并且可以有效避免垃圾回收带来的负面影响,

但由于和堆空间相比,栈空间较小,因此对于大对象无法也不适合在栈上分配。

8. 类都去哪了?识别方法区

和Java堆一样,方法区是一块所有线程共享的内存区域。

它用于保存系统的类信息,比如类的字段、方法、常量池等。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。

在JDK 1.6、JDK 1.7中,方法区可以理解为永久区(Perm)。

永久区可以使用参数-XX:PermSize和-XX:MaxPermSize指定,默认情况下,-XX:MaxPermSize为64MB。

一个大的永久区可以保存更多的类信息。

如果系统使用了一些动态代理,那么有可能会在运行时生成大量的类,

如果这样,就需要设置一个合理的永久区大小,确保不发生永久区内存溢出。

-XX:+PrintGCDetails  -XX:PermSize=5M  -XX:MaxPermSize= 5m

这里指定了初始永久区5MB,最大永久区5MB,即当5MB空间耗尽时,系统将抛出内存溢出。

在JDK 1.8中,永久区已经被彻底移除。

取而代之的是元数据区,元数据区大小可以使参数-XX:MaxMetaspaceSize指定(一个大的元数据区可以使系统支持更多的类),

这是一块堆外的直接内存。

与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存

运行参数:

-XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=40m 	

通过 Visual VM 也可以查看到移除情况。

二、常用的 java 虚拟机参数

要诊断虚拟机,我们需要知道如何对Java虚拟机进行最基本的配置和跟踪。

接下来,主要介绍一些常用的Java虛拟机参数,它们可以对系统进行跟踪和配置,对系统故障诊断、性能优化有着

重要的作用。

1. 跟踪调试参数

Java的一大特色就是 支持自动的垃圾回收(GC) ,但是有时候,如果垃圾回收频繁出现,或者占用了太长的CPU时

间,就不得不引起重视。此时,就需要一些跟踪参数来进一步甄别垃圾回收器的效率和效果。

最简单的一个GC参数是-XX:+PrintGC,使用这个参数启动Java虚拟机后,只要遇到GC,就会打印日志,如下所示:

[GC 4793K->377K (15872K),0. 0006926 secs]
[GC 4857K->377K(15936K), 0. 0003595 secs]
[GC 4857K->377K(15936K), 0.0001755 secs]
[GC 4857K->377K (15936K),0. 0001957 secs]

该日志显示,共进行了4次GC,每次GC占用一行,在GC前,堆空间使用量(已占用)约为4MB,GC后,堆空间使用量(已占用)为

377KB,当前可用的堆空间总和(空闲)约为16MB ( 15936KB)。

最后,显示的是本次GC所花费的时间。

如果需要更加详细的信息,则可以使用-XX:+PrintGCDetails参数。

它的输出可能如下:

[GC (System.gc()) [PSYoungGen: 4957K->856K(76288K)] 4957K->864K(251392K), 0.0028907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 856K->0K(76288K)] [ParOldGen: 8K->629K(175104K)] 864K->629K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0057303 secs] [Times: user=0.16 sys=0.00, real=0.01 secs] 
[GC (System.gc()) [PSYoungGen: 2334K->0K(76288K)] 2964K->629K(251392K), 0.0010263 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 629K->629K(175104K)] 629K->629K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0024450 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 3645K->96K(76288K)] 4275K->725K(251392K), 0.0009841 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 96K->0K(76288K)] [ParOldGen: 629K->624K(175104K)] 725K->624K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0060740 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (System.gc()) [PSYoungGen: 3645K->96K(76288K)] 4270K->720K(251392K), 0.0007587 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 96K->0K(76288K)] [ParOldGen: 624K->624K(175104K)] 720K->624K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0028285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 4956K->64K(76288K)] 5580K->688K(251392K), 0.0006692 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 64K->0K(76288K)] [ParOldGen: 624K->624K(175104K)] 688K->624K(251392K), [Metaspace: 3405K->3405K(1056768K)], 0.0062451 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b380000,0x000000076b56b9e0,0x000000076f380000)
  from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
  to   space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
 ParOldGen       total 175104K, used 624K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1a9c2d8,0x00000006cc500000)
 Metaspace       used 3419K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 371K, capacity 388K, committed 512K, reserved 1048576K

[GC (System.gc()) [PSYoungGen: 4957K->856K(76288K)] 4957K->864K(251392K), 0.0028907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

PSYoungGen是指GC发生的区域,还有一个ParOldGen

4957K->856K(76288K),这三个数字分别对应GC之前占用年轻代的大小,GC之后年轻代占用,以及整个年轻代的

大小。

4957K->864K(251392K),这三个数字分别对应GC之前占用堆内存的大小,GC之后堆内存占用,以及整个堆内存

的大小。

0.0028907是该时间点GC占用耗费时间

-Xms --jvm堆的最小值

-Xmx --jvm堆的最大值

-XX:MaxNewSize --新生代最大值

-XX:MaxPermSize=1028m --永久代最大值

-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)

-XX:+PrintGCDateStamps 输出GC的时间戳

-XX:+PrintGCDetails --打印出GC的详细信息

-verbose:gc --开启gc日志

-Xloggc:d:/gc.log -- gc日志的存放位置

-Xmn -- 新生代内存区域的大小

-XX:SurvivorRatio=8 --新生代内存区域中Eden和Survivor的比例

2. 系统参数查看

参数-XX:+PrintVMOptions可以在程序运行时,打印虚拟机接受到的命令行显式参数。

其输出-XX:+PrintVMOptions

参数-XX:+PrintCommandLineFlags可以打印传递给虚拟机的显式和隐式参数,隐式参数未必是通过命令行直接给出的,

它可能是由虚拟机启动时自行设置的,使用-XX:+PrintCommandLineFlags

-XX:InitialHeapSize=266658240 -XX:MaxHeapSize=4266531840 -XX:+PrintCommandLineFlags 
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

三、堆内存的参数配置

1. 最大堆和初始堆的设置

1.8后永久代变成matespace在jvm之外。

主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,不受MaxPermSize控制

当Java进程启动时,虚拟机就会分配一块初始堆空间,可以使用参数-Xms指定这块空间的大小。

一般来说,虚拟机会尽可能维持在初始堆空间的范围内运行。

但是如果初始堆空间耗尽,虚拟机将会对堆空间进行扩展,其扩展上限为最大堆空间,最大堆空间可以使用参数-

Xmx指定。

-Xms:初始堆大小

-Xmx:最大堆大小

参数:-XX:+PrintGCDetails -XX:+PrintCommandLineFlags -Xmx20m

使用-Xmx20m -Xms20m -Xmn2m -XX:SurvivorRatio=2 -XX:+PrintGCDetails运行

最大可用内存就是指-Xmx的取值,

当前总内存应该不小于-Xms的设定,因为当前总内存总是在-Xms和-Xmx之间,从-Xms开始根据需要向上增长。

而当前空闲内存应该是当前总内存减去当前已经使用的空间。

但实际也会很快就能发现中间的偏差。

工作应用:

在实际工作中,也可以直接将初始堆-Xms与最大堆-Xmx设置相等。

这样的好处是可以减少程序运行时进行的垃圾回收次数,从而提高程序的性能。

2. 新生代的配置

-XX: SurvivorRatio

参数-Xmn可以用于设置新生代的大小,设置一个较大的新生代会减小老年代的大小,这个参数对系统性能以及GC行为有很大的影响。

新生代的大小一般般设置为整个堆空间的1/3到1/4左右。

SurvivorRatio定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,

From幸存区和To幸存区各占新生代的1/10。

在实际工作中,应该根据系统的特点做合理的设置,基本策略是:尽可能将对象预留在新生代,减少老年代GC的次数

-XX:NewRatio=老年代/新生代

除了可以使用参数-Xmn指定新生代的绝对大小外,还可以使用参数-XX:NewRatio来设置新生代和老年代的比例

使用-Xmx20m -Xms20m -XX:NewRatio=2 -XX:+PrintGCDetails运行

堆的分配参数示意图

3. 堆溢出参数

在Java程序的运行过程中,如果堆空间不足,则有可能抛出内存溢出错误(OutOfMemory),简称为OOM

一旦发生这类问题,系统就会被迫退出。

如果发生在生产环境,可能会引起严重的业务中断。

为了能够不断改善系统,避免或减少这类错误的发生,需要在发生错误时,获得尽可能多的现场信息,以帮助研

发人员排查现场问题。

Java 虚拟机提供了参数-XX:+HeapDumpOnOutOfMemoryError,使用该参数,可以在内存溢出时导出整个堆信

息。和它配合使用的还有-XX:HeapDumpPath,可以指定导出堆的存放路径。

示例:

-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump

虚拟机将当前的堆导出,并保存到D·:/a.dump文件下,

可以使用MAT等工具打开该文件进行分析,如图所示,可以很容易地找到这些byte数组和保存它们的 Vector 对象实例。

四、非堆内存的参数配置

1. 方法区配置

在JDK 1.8中,永久区被彻底移除,使用了新的元数据区存放类的元数据。

默认情况下,元数据区只受系统可用内存的限制,但依然可以使用参数-XX:MaxMetaspaceSize指定永久区的值

大小

2. 栈配置

在Java虚拟机中可以使用-Xss参数指定线程的栈最大大小

3. 直接内存配置

直接内存也是Java程序中非常重要的组成部分,特别是在NIO被广泛使用后,直接内存的使用也变得非常普遍。

直接内存跳过了Java堆,使Java程序可以直接访问原生堆空间,因此,从一定程度上加快了内存空间的访问速度。

但是,武断地认为使用直接内存一定可以提高内存访问速度也是不正确的。

最大可用直接内存可以使用参数-XX:MaxDirectMemorySize设置,如不设置,默认值为最大堆空间,即-Xmx。

当直接内存使用量达到-XX:MaxDirectMemorySize时,就会触发垃圾回收,如果垃圾回收不能有效释放足够空

间,直接内存溢出依然会引起系统的OOM。

应用场景:

直接内存适合申请次数较少、访问较频繁的场合。

如果内存空间本身需要频繁申请,则并不适合使用直接内存。

4. 虚拟机的工作模式Server和Client

目前的Java虚拟机支持Client和Server两种运行模式。

使用参数-client可以指定使用Client模式,使用参数-server可以指定使用Server模式。

默认情况下,虚拟机会根据当前计算机系统环境自动选择运行模式。

使用-version参数可以查看当前的模式,如下所示:

与Client模式相比,Server模式的启动比较慢,

因为Server模式会尝试收集更多的系统性能信息,使用更复杂的优化算法对程序进行优化。

因此,当系统完全启动并进入运行稳定期后,Server模式的执行速度会远远快于Client 模式。

所以,对于后台长期运行的系统,使用-server参数启动对系统的整体性能可以有不小的帮助。

但对于用户界面程序,运行时间不长,又追求启动速度,Client 模式也是不错的选择。

从发展趋势上看,未来64位系统必然会逐步取代32位系统,而在64位系统中虚拟机更倾向于使用Server模式运

行。

五、垃圾回收算法

我们知道什么是垃圾回收和为什么要进行自动化的垃圾回收了。

接下来,垃圾回收又分为许多种类,这里主要是讨论实现垃圾回收的方法,主要内容是理解Java垃圾回收机制的理

论基础。

这里主要讨论:引用计数法、标记压缩法、标记清除法、复制算法和分代、分区的思想

1. 垃圾回收的思想

垃圾回收的基本思想是考察每一个对象的可触及性,即从根节点开始是否可以访问到这个对象,如果可以,则说

明当前对象正在被使用,

如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。

2. 引用计数法

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。

只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

引用计数器的实现也非常简单,只需要为每个对象配备一 个整型的计数器即可。

但是,引用计数器有两个非常严重的问题:

  1. 无法处理循环引用的情况。因此,在Java的垃圾回收器中,没有使用这种算法。
  2. 引用计算器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

一个简单的循环引用问题描述如下:

有对象A和对象B, 对象A中含有对象 B 的引用,对象B中含有对象A的引用。

此时,对象A和B的引用计数器都不为0。

但是,在系统中,却不存在任何第3个对象引用了A或B。

也就是说,A和B是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存

泄漏。

如图所示,不可达的对象出现循环引用,它的引用计数器均不为0。

3. 标记清除法

标记清除算法是现代垃圾回收算法的思想基础。

标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。

一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。

因此,未被标记的对象就是未被引用的垃圾对象。

然后,在清除阶段,清除所有未被标记的对象。

标记清除算法可能产生的最大问题是空间碎片。

缺点:

回收后的空间是不连续的。

在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续的空间。

因此,这也是该算法的最大缺点。

4. 复制算法

复制算法的核心思想是:

将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清

除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

优点:

  1. 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量就会相对较少。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。
  2. 又由于对象是在垃圾回收过程中,统一被复制到新的内存空间中的,因此,可确保回收后的内存空间是没有碎片的。

缺点:

虽然有以上两大优点,但是,复制算法的代价却是将系统内存折半,因此,单纯的复制算法也很难让人接受。

如图所示,A、B两块相同的内存空间,A在进行垃圾回收时,将存活对象复制到B中,B中的空间在复制后保持连

续。

复制完成后,清空A,并将空间B设置为当前使用空间。

在Java的新生代串行垃圾回收器中,使用了复制算法的思想。

新生代分为eden空间、from空间和to空间3个部分。

其中from和to空间可以视为用于复制的两块大小相同、地位相等、且可进行角色互换的空间块。

from和to空间也称为survivor空间,即幸存者空间,用于存放未被回收的对象。

在垃圾回收时,eden空间中的存活对象会被复制到未使用的survivor空间中(假设是to),正在使用的survivor空间

(假设是from)中的年轻对象也会被复制到to空间中(大对象,或者老年对象会直接进入老年代,如果to空间已满,

则对象也会直接进入老年代)。

此时,eden 空间和from空间中的剩余对象就是垃圾对象,可以直接清空,to空间则存放此次回收后的存活对象。

这种改进的复制算法,既保证了空间的连续性,又避免了大量的内存空间浪费。

如图所示,显示了复制算法的实际回收过程。

当所有存活对象都复制到survivor区后(假设是to),简单地清空eden区和备用的survivor区(假设为from)即可。

应用场景:

复制算法比较适用于新生代。因为在新生代,垃圾对象通常会多于存活对象。复制算法的效果会比较好。

5. 标记压缩算法

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生。

但是在老年代,更常见的情况是大部分对象都是存活对象。

如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。

因此,基于老年代垃圾回收的特性,需要使用其他的算法。

标记压缩算法是一种老年代的回收算法。

它在标记清除算法的基础上做了一些优化。

和标记清除算法一样,标记压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。

但之后,它并不只是简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所

有的空间。

这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比较高。

标记压缩算法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理,

因此,也可以把它称为标记清除压缩(MarkSweepCompact)算法。

6. 分代算法

前文中介绍了复制、标记清除、标记压缩等垃圾回收算法。

在所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。

因此,根据垃圾回收对象的特性,使用合适的算法回收,才是明智的选择。

分代算法就是基于这种思想,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回

收算法,以提高垃圾回收的效率。

一般来说,Java 虛拟机会将所有的新建对象都放入称为新生代的内存区域,新生代的特点是对象朝生夕灭,大约

90%的新建对象会被很快回收,因此,新生代比较适合使用复制算法。

当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。

在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以存活的。

因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。

在极端情况下,老年代对象的存活率可以达到100%。

如果依然使用复制算法回收老年代,将需要复制大量对象。

再加上老年代的回收性价比也要低于新生代,因此这种做法是不可取的。

根据分代的思想,可以对老年代的回收使用与新生代不同的标记压缩或标记清除算法,以提高垃圾回收效率。

如图所示,显示了这种分代回收的思想。

对于新生代和老年代来说,通常,新生代回收的频率很高,但是每次回收的耗时都很短,而老年代回收的频

率比较低,但是会消耗更多的时间。

为了支持高频率的新生代回收,虚拟机可能使用一种叫作卡表的数据结构。

卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的

引用。

这样在新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对 象的引用关系,而可以先扫描卡

表,只有当卡表的标记位为1时,才需要扫描给定区域的老年代对象,而卡表位为0的所在区域的老年代对象,一

定不含有新生代对象的引用。

如图所示,卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,

只有卡表位为1的区域才有对象包含新生代引用,因此在新生代GC时,只需要扫描卡表位为1所在的老年代空间。

使用这种方式,可以大大加快新生代的回收速度。

7. 分区算法

分代算法将按照对象的生命周期长短划分成两个部分,分区 算法将整个堆空间划分成连续的不同小区间,如图所

示。

每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,从而产生的停顿也越长,有关GC产生的

停顿后面会讲。

为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回

收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

六、谁是真正的垃圾

1. 对象复活

所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收。

但事实上,一个无法触及的对象有可能在某一个条件下"复活"自己,如果这样,那么对它的回收就是不合理

的,为此,需要给出一个对象可触及性状态的定义,并规定在什么状态下,才可以安全地回收对象。

简单来说,可触及性可以包含以下3种状态。

  1. 可触及的:从根节点开始,可以到达这个对象。
  2. 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()函数中复活。
  3. 不可触及的:对象的finalize()函数被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为finalize()函数只会被调用一次。

以上3种状态中,只有在对象不可触及时才可以被回收。

注意:

finalize()函数是一 个非常不推荐的模式,也不推荐使用finalize()函数释放资源。

第一,因为finalize()函数有可能发生引用外泄,在无意中复活对象;

第二,由于finalize()是被系统调用的,调用时间是不明确的,因此不是一个好的资源释放方案。

2. 引用和可触及性的强度

在Java中提供了4个级别的引用:强引用、软引用、弱引用和虚引用。

除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。

如图所示,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。

其中FinalReference意味"最终"引用,它用以实现对象的finalize()方法, 后面会说。

强引用

强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。

相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虛可触及的,在一定条件下,都是可以被回收

的。

示例:

StringBuffer str = new StringBuffer("hello world");

假设以上代码是在函数体内运行的,那么局部变量str将被分配在栈上,而对象StringBuffer实例被分配在堆上。

局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是StringBuffer实例的强引

用,如图所示:

再加一条语句:

StringBuffer str1 = str;

那么,str所指向的对象也将被str1所指向,同时在局部变量表上会分配空间存放strl变量,如图所示。

此时,该StringBuffer实例就有两个引用。

对引用的"=="操作用于表示两操作数所指向的堆空间地址是否相同。

如本例中的两个引用,都是强引用,强引用具备以下特点:

  1. 强引用可以直接访问目标对象。
  2. 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿拋出OOM异常,也不会回收强引用所指向对象。
  3. 强引用可能导致内存泄漏。

软引用

软引用是比强引用弱一点的引用类型。

一个对象只持有软引用,那么当堆空间不足时,就会被回收。

软引用使用java.lang.ref.SoftReference类实现。

GC未必会回收软引用的对象,

但是,当内存资源紧张时,软引用对象会被回收,所以软引用对象不会引起内存溢出。

弱引用

弱引用是一种比软引用较弱的引用类型。

在系统GC时,只要发现弱引用,不管系统堆空间使用情况如何,都会将对象进行回收。

但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。

在这种情况下,弱引用对象可以存在较长的时间。

注意:

软引用、弱引用都非常适合来保存那些可有可无的缓存数据。

如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。

而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

虚引用

虚引用是所有引用类型中最弱的一个。

一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。

当试图通过虚引用的get()方法取得强引用时,总是会失败。

并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,

将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

七、垃圾回收器

1. 串行回收器

垃圾回收的停顿

垃圾回收器的任务是识别和回收垃圾对象进行内存清理。

为了让垃圾回收器可以正常且高效地执行,大部分情况下,会要求系统进入一个停顿的状态。

停顿的目的是终止所有应用线程的执行,只有这样,系统中才不会有新的垃圾产生,同时停顿保证了系统状态在

某一个瞬间的一致性,

也有益于垃圾回收器更好地标记垃圾对象。

因此,在垃圾回收时,都会产生应用程序的停顿。

停顿产生时,整个应用程序会被卡死,没有任何响应,因此这个停顿也叫做"Stop-The-World" (STW)"。

新生代的串行

串行收集器是是JDK中最基本的垃圾回收器之一。

串行回收器主要有两个特点:

第一,它仅仅使用单线程进行垃圾回收。

第二,它是独占式的垃圾回收。

在串行收集器进行垃圾回收时,Java应用程序中的线程都需要暂停,等待垃圾回收的完成。

如图所示,在串行回收器运行时,应用程序中的所有线程都停止工作,进行等待。

这种现象称之为"Stop-The-World"。

它将造成非常糟糕的用户体验,在实时性要求较高的应用场景中,这种现象往往是不能被接受的。

新生代串行处理器使用复制算法,实现相对简单、逻辑处理特别高效、且没有线程切换的开销。

在诸如单CPU处理器等硬件平台不是特别优越的场合,它的性能表现可以超过并行回收器和并发回收器。

使用-XX:+UseSerialGC参数可以指定使用新生代串行收集器和老年代串行收集器。

当虚拟机在Client模式下运行时,它是默认的垃圾收集器。

使用-Xmx1g -Xms1g -Xmn900m -XX:SurvivorRatio=1 -XX: +UseSerialGC -Xloggc:gc. log

-XX: +PrintGCDetails

优点

① 简单而高效

② 对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率

缺点

收集期间需要暂停所有应用线程,用户体验不好

应用场景

Java虚拟机运行在Client模式下默认的新生代垃圾收集器对应JVM参数是:-XX:+UseSerialGC

当我们使用此参数开启Serial,老年代默认会开启Serial Old

即开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合

表示新生代和老年代都会使用串行回收收集器

新生代使用复制算法,老年代使用标记压缩算法

老年代的串行

老年代串行收集器使用的是标记压缩算法。

由于老年代垃圾回收通常会使用比新生代回收更长的时间,

因此,在堆空间较大的应用程序中,一旦老年代串行收集器启动,应用程序很可能会因此停顿较长的时间。

虽然如此,老年代串行回收器可以和多种新生代回收器配合使用,若要启用老年代串行回收器,可以尝试使用以下参数。

-XX:+UseSerialGC: 新生代、老年代都使用串行回收器。

-XX:+UseParNewGC: 新生代使用ParNew回收器,老年代使用串行收集器。

-XX:+UseParallelGC: 新生代使用ParallelGC回收器,老年代使用串行收集器。

一次老年代串行回收器的工作输出日志类似如下信息:

2. 并行回收器

新生代并行回收器ParNew

ParNew回收器是一个工作在新生代的垃圾收集器。

它只是简单地将串行回收器多线程化,它的回收策略、算法以及参数和新生代串行回收器一样。

ParNew 回收器的工作示意图如图所示。

ParNew回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。

但由于并行回收器使用多线程进行垃圾回收,

因此,在并发能力比较强的CPU上,它产生的停顿时间要短于串行回收器,

而在单CPU或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,

由于多线程的压力,它的实际表现很可能比串行回收器差。

开启ParNew回收器可以使用以下参数。

-XX:+UseParNewGC:新生代使用ParNew回收器,老年代使用串行回收器。

-XX:+UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS。

ParNew回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定。

一般,最好与CPU数量相当,避免过多的线程数,影响垃圾收集性能。

在默认情况下,当CPU数量小于8个时,ParallelGCThreads 的值等于CPU数量,

当CPU数量大于8个时,ParallelGCThreads 的值等于3+((5*CPU_ Count)/8)。

使用:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseParNewGC

应用场景

最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样

它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器

对应JVM参数是:-XX:+UseParNewGC

启用ParNewGC收集器,只影响新生代的收集,不影响老年代

开启后会使用:ParNew(Young区用)+ Serial Old(Old区用)的收集器组合

新生代使用复制算法,老年代使用标记压缩算法

新生代并行垃圾回收器ParallelGC

新生代ParallelGC回收器也是使用复制算法的收集器。

从表面上看,它和ParNew回收器一样,都是多线程、独占式的收集器。

但是, ParallelGC回收器有个重要的特点:它非常关注系统的吞吐量。

新生代ParallelGC回收器可以使用以下参数启用。

-XX:+UseParalleIGC: 新生代使用ParallelGC回收器,老年代使用ParallelOldGC。

-XX:+UseParallelOldGC: 新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。

对应JVM参数是:XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活,即配置其中一个,另一个会自动连带激活)

ParallelGC回收器提供了两个重要的参数用于控制系统的吞吐量。

Parallel重点关注的是:可控制的吞吐量

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

-XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间。它的值是一个大于0的整数。

ParallelGC在工作时,会调整Java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMillis以内。

如果希望减少停顿时间,而把这个值设得很小,为了达到预期的停顿时间,

虚拟机可能会使用一个较小的堆(一个小堆比一个大堆回收快),

而这将导致垃圾回收变得很频繁,从而增加了垃圾回收总时间,降低了吞吐量。

-XX:GCTimeRatio: 设置吞吐量大小。它的值是一个0到100之间的整数。

假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。

比如GCTimeRatio等于19 (默认值),则系统用于垃圾收集的时间不超过1/(1+19)=5%。

默认情况下,它的取值是99,即不超过1/(1+99)=1%的时间用于垃圾收集。

除此以外,ParallelGC回收器与ParNew回收器另一个不同之处在于它还支持一种自适应的GC调节策略。

使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。

在这种模式下,新生代的大小、eden和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,

以达到在堆大小、吞吐量和停顿时间之间的平衡点。

在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虛拟机的最大堆、目标吞吐量( GCTimeRatio)和停顿时间

(MaxGCPauseMillis),让虚拟机自己完成调优工作。

使用-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseParallelGC

老年代并行垃圾回收器ParallelOldGC

老年代ParallelOldGC回收器也是一种多线程并发的收集器。

和新生代ParallelGC回收器一样,它也是一种关注吞吐量的收集器。

并且和ParallelGC新生代回收器搭配使用。

ParallelOldGC回收器使用标记压缩算法,图显示了老年代ParallelOldGC回收器的工作模式。

使用-XX:+UseParallelOldGC可以在新生代使用·ParallelGC回收器,老年代使用ParallelOldGC回收器。

这是一对非常关注吞吐量的垃圾回收器组合。在对吞吐量敏感的系统中,可以考虑使用。

参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。

3. CMS 垃圾回收器

CMS收集器(Concurrent Mark Sweep:并发标记清除)

是一种以获取最短回收停顿时间为目标的收集器

与ParallelGC 和 ParallelOldGC 不同,CMS回收器主要关注于系统停顿时间。

CMS工作时,主要步骤有:初始标记、并发标记、预清理、重新标记、并发清除和并发重置。

其中初始标记和重新标记是独占系统资源的,而预清理、并发标记、并发清除和并发重置是可以和用户线程一起执行的。

因此,从整体上说,CSM收集不是独占式的,它可以在应用程序运行过程中进行垃圾回收。

CMS 参数设置

XX:+UseConcMarkSweepGC CMS 启用CMS回收器,是多线程回收器,设置合理的工作线程数量也对系统性能有重要的影响。

CMS默认启动的并发线程数是(ParallelGCThreads+3)/4)。

并发线程数量也可以通过-XX:ConcGCThreads或者-XX:ParallelCMSThreads参数手工设定。

当CPU资源比较紧张时,受到CMS回收器线程的影响,应用系统的性能在垃圾回收阶段可能会非常糟糕。

在CMS回收过程中,应用程序仍然在不停地工作,又会不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的。

同时,因为应用程序没有中断,所以在CMS回收过程中,还应该确保应用程序有足够的内存可用。

因此,CMS回收器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值时便开始进行回收,

以确保应用程序在CMS工作过程中,依然有足够的空间支持应用程序运行。

这个回收阈值可以使用-XX:CMSInitiatingOccupancyFraction来指定,默认是68。

即当老年代的空间使用率达到68%时,会执行一次CMS回收。

如果应用程序的内存使用率增长很快,在CMS的执行过程中,已经出现了内存不足的情况,

此时,CMS回收就会失败,虚拟机将启动老年代串行收集器进行垃圾回收。

如果这样,应用程序将完全中断,直到垃圾回收完成,这时,应用程序的停顿时间可能会较长。

通过-XX:CMSInitiatingOccupancyFraction 可以指定当老年代空间使用率达到多少时,进行一次CMS垃圾回收。

因此,根据应用程序的特点,可以对-XX:CMSInitiatingOccupancyFraction进行调优。

如果内存增长缓慢,则可以设置个稍大的值,大的阈值可以有效降低CMS的触发频率,

减少老年代回收的次数可以较为明显地改善应用程序性能。

反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。

CMS是一个基于标记清除算法的回收器。

我们标记清除算法将会造成大量内存碎片,离散的可用空间无法分配较大的对象。

在这种情况下,即使堆内存仍然有较大的剩余空间,也可能会被迫进行一次垃圾回收,以换取一块可用的连续内存。

这种现象对系统性能是相当不利的,为了解决这个问题,CMS回收器还提供了几个用于内存压缩整理的参数。

-XX:+UseCMSCompactAtFullCollection开关可以使CMS在垃圾收集完成后,进行一次内存碎片整理,

内存碎片的整理不是并发进行的。

-XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少次CMS回收后,进行一次内存压缩。

CMS 日志

使用-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC

在CMS回收器的工作过程中,包括了初始化标记、并发标记、预清理、重新标记、并发清理和重发重置等几个重要阶段。

在日志中,还可以看到CMS的耗时以及堆内存信息。

4. G1回收器

G1回收器(Garbage-First) 是在JDK 1.7 中正式使用的全新的垃圾回收器,从长期目标来看,它是为了取代CMS回收器。

从分代上看,G1依然属于分代垃圾回收器,它会区分年轻代和老年代,依然有eden区和survivor区,

但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续。它使用了分区算法。

G1同时使用了全新的分区算法,其特点如下。

并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力尽量缩短STW。

并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,

因此一般来说,不会在整个回收期间完全阻塞应用程序。

分代GC:G1依然是一个分代收集器,但是和之前回收器不同,它同时兼顾年轻代和老年代。

对比其他回收器,它们或者工作在年轻代,或者工作在老年代,

宏观上看G1之中不再区分年轻代和老年代,把内存划分成多个独立的子区域(Region)。

空间整理: G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单地标记清理对象,在若干次GC后,CMS必须进行一次碎片整

理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片。

可预见性: 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿也能得到较好的控制。

新生代GC

Region区域化垃圾收集器:

最大的好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可区域化内存划片Region,

整体变为了一些不连续的内存区域,避免了全内存区的GC操作

核心思想:

将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小

在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,

每个分区也不会固定地位某个代服务,可以按需在年轻代和老年代切换

启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),

默认将整堆划分为2048个分区

大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G内存

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器

这些Region的一部分包含新生代

新生代的垃圾收集依然采用暂停所有应用线程的方式,将存或对象拷贝到老年代或Survivor空间

这些Region的一部分包含老年代

G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作

这意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存问题的存在了

在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域

如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。

这种巨型对象默认直接会被分配在老年代,

但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,

为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象

如果H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储,为了能找到连续的H区,有时候不得不启动Full GC

回收步骤:

G1收集器下的Young GC

针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部分晋升到Old区

Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区

最后Eden区收拾干净了,GC结束,用户的应用程序继续执行

回收过程

G1的并发阶段和CMS有点类似,它们都是为了降低一次停顿时间,而将可以和应用程序

并发的部分单独提取出来执行。

并发标记周期可以分为以下几步。

  • 初始标记:

标记从根节点直接可达的对象。这个阶段会伴随一次新生代 GC,它是会产生全局停顿的,

应用程序线程在这个阶段必须停止执行。

  • 根区域扫描:

在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。

这个过程是可以和应用程序并发执行的。

但是根区域扫描不能和新生代GC同时执行(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域)。

  • 并发标记:

和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。

这是一个并发的过程,并且这个过程可以被一次新生代GC打断。

  • 重新标记:

和CMS一样,重新标记也是会产生应用程序停顿的。

由于在并发标记过程中,应用程序依然在运行,因此标记结果可能需要进行修正,所以在此对上一次的标记结果进行补充。

  • 独占清理:

这个阶段是会引起停顿的。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。

在这个阶段,还会更新记忆集(Remebered Set)。

该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。

  • 并发清理阶段:

这里会识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。

必要时FullGC

和CMS类似,并发收集由于让应用程序和GC线程交替工作,

因此总是不能完全避免在特别繁忙的场合会出现在回收过程中内存不充足的情况。

当遇到这种情况时,G1也会转入一个Full GC进行回收。

G1的参数设置

-XX:+UseG1GC可以使用标记打开G1收集器开关。

-XX:MaxGCPauseMillis它用于指定目标最大停顿时间。

如果任何一次停顿超过这个设置值时,G1就会尝试调整新生代和老年代的比例、调整堆大小、调整晋升年龄等手段,

试图达到预设目标。

对于性能调优来说,有时候,总是鱼和熊掌不可兼得的,

如果停顿时间缩短,对于新生代来说,这意味着很可能要增加新生代GC的次数,GC反而会变得更加频繁。

对于老年代区域来说,为了获得更短的停顿时间,那么在混合GC收集时,一次收集的区域数量也会变少,这样无疑增加了进行Full GC的

可能性。

-XX:ParallelGCThreads,它用于设置并行回收时,GC的工作线程数量。

-XX:InitiatingHeapOccupancyPercent参数可以指定当整个堆使用率达到多少时,触发并发标记周期的执行。

默认值是45,即当整个堆占用率达到45%时,执行并发标记周期。

InitiatingHeapOccupancyPercent 一旦设置,始终都不会被G1收集器修改,

这意味着G1收集器不会试图改变这个值,来满足MaxGCPauseMillis的目标。

如果InitiatingHeapOccupancyPercent值设置偏大,会导致并发周期迟迟得不到启动,那么引起Full GC的可能性也大大增加,

反之,一个过小的InitiatingHeapOccupancyPercent 值,会使得并发周期非常频繁,大量GC线程抢占CPU,会导致应用程序的性能有所

下降。

对象何时进入老年代

老年对象进入老年代

那eden区中的对象何时能进入老年代呢?

一般来说, 当对象的年龄达到一定的大小,就自然可以离开年轻代,进入老年代,

一般可以把对 象进入老年代的事件,称为"晋升"。

对象的年龄是由对象经历过的GC次数决定的。

在新生代中的对象每经历一次GC,如果它没有被回收,它的年龄就加1。

虚拟机提供了一个参数来控制新生代对象的最大年龄: MaxTenuringThreshold。

默认情况下,这个参数为15。

也就是说,在新生代的对象最多经历15次GC,就可以晋升到老年代。

-XX:MaxTenuringThreshold作用:在可以自动调节对象晋升(Promote)到老年代阈值的GC中,设置该阈值的最大值。

该参数的默认值是15

使用:-Xmx1024M -Xms1024M -XX:+PrintGCDetails -XX:MaxTenuringThreshold=5 -XX:+PrintHeapAtGC

大对象进入老年代

除了年龄外,对象的体积也会影响对象的晋升。

试想,如果对象体积很大,新生代无论eden区或者survivor区无法容纳这个对象,自然这个对象无法存放在新生代,

另外一个有趣的参数是PretenureSizeThreshold,它用来设置对象直接晋升到老年代的阈值,单位是字节。

只要对象的大于指定值,就会绕过新生代,直接在老年代分配。

这个参数只对串行回收器和ParNew有效,对于ParallelGC 无效。

默认情况下该值为0,也就是不指定最大的晋升大小,一切由运行情况决定。

使用参数:-Xmx32m -Xms32m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000

虚拟机在为线程分配空间时,会优先使用一块叫作TLAB的区域,对于体积不大的对象,

很有可能会在TLAB上先行分配,因此,就失去了在老年代分配的机会。

因此,这里简单地禁用TLAB即可。

使用参数:

-Xmx32m -Xms32m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB

方法finalize对垃圾回收器的影响

该函数允许在子类中被重载,用于在对象被回收时进行资源地释放。

目前,普遍的认识是,尽量不要使用finalize()函数进行资源释放,

原因主要有以下几点:

在finalize()时可能会导致对象复活;

finalize()函数的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()将没有机会执行;

一个糟糕的 finalize() 会严重影响GC的性能。

函数finalize()是由FinalizerThread 线程处理的。

每一个即将被回收的并且包含有finalize()方法的对象都会在正式回收前加入FinalizerThread 的执行队列,

该队列为java.lang.ref.ReferenceQueue引用队列,内部实现为链表结构,

队列中每一项为 java.lang.ref.Finalizer引用对象,它本质为一个引用,如图所示,这和虚引用、弱引用等如出一辙。

由于在引用队列中的元素排队执行finalize()方法,

一旦出现性能问题,将导致这些垃圾对象长时间堆积在内存中,可能会导致OOM异常。

还有一个糟糕的finalize()可能会使对象长时间被Finalizer引用,而得不到释放,因此,这会进一步增加GC的压力,因此,finalize()应该是

尽量少地被使用。

5. 知识小结

与串行回收器相关的参数

  • -XX:+UseSerialGC:在新生代和老年代使用串行收集器。
  • -XX:SurvivorRatio: 设置eden区大小和survivior区大小的比例。
  • -XX:PretenureSizeThreshold: 设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。
  • -XX:MaxTenuringThreshold: 设置对象进入老年代的年龄的最大值。每一次Minor GC后,对象年龄就加1。任何大于这个年龄的对象,一定会进入老年代。

与并行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和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞量和停顿时间之间的平衡点。

与CMS回收器相关的参数

  • -XX:+UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用CMS+串行收集器。
  • -XX:ParallelCMSThreads: 设定CMS的线程数量。
  • -XX:CMSInitiatingOccupancyFraction: 设置CMS收集器在老年代空间被使用多少后触发,默认为68%。
  • -XX:+UseCMSCompactAtFullCollection: 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理。
  • -XX:CMSFullGCsBeforeCompaction: 设定进行多少次CMS垃圾回收后,进行一次内存压缩。
  • -XX:+CMSClassUnloadingEnabled:允许对类元数据区进行回收。
  • -XX:CMSInitiatingPermOccupancyFraction: 当永久区占用率达到这- - -百分比时,启动CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)。

与G1回收器相关的参数

  • -XX:+UseG1GC: 使用G1回收器。
  • -XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间。
  • -XX:GCPauseIntervalMillis: 设置停顿间隔时间。

八、性能监控工具

1. java 进程 Jps

jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。

通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。

如果在 linux 中想查看 java 的进程,一般我们都需要 ps -ef | grep java 来获取进程 ID。

如果只想获取 Java 程序的进程,可以直接使用 jps 命令来直接查看。

参数说明

  • -q:只输出进程 ID
  • -m:输出传入 main 方法的参数
  • -l:输出完全的包名,应用主类名,jar的完全路径名
  • -v:输出jvm

从这个输出中可以看到,当前系统中共存2个Java 应用程序,

其中第一个输出Jps就是ips命令本身,这更加证明此命令的本质也是一个Java程序。

此外,jps还提供了一系列参数来控制它的输出内容。

参数 -q 可以指定jps只输出进程ID,而不输出类的短名称:

参数 -m 可以用于输出传递给Java进程(主函数)的参数:

参数 -l 可以用于输出主函数的完整路径:

参数 -v 表示传递给jvm的参数

2. jstat 查看虚拟机运行时信息

Jstat是JDK自带的一个轻量级小工具。

全称"Java Virtual Machine statistics monitoring tool",它位于java的bin目录下,

主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收

状况的监控。

option:参数选项

  • -t:可以在打印的列加上Timestamp列,用于显示系统运行的时间
  • -h:可以在周期性数据数据的时候,可以在指定输出多少行以后输出一次表头
  • vmid:Virtual Machine ID( 进程的 pid)
  • interval:执行每次的间隔时间,单位为毫秒
  • count:用于指定输出多少次记录,缺省则会一直打印

选项 option可以由以下值构成。

-class 显示ClassLoad的相关信息;

-compiler 显示JIT编译的相关信息;

-gc 显示和gc相关的堆信息;

-gccapacity 显示各个代的容量以及使用情况;

-gcmetacapacity 显示metaspace的大小

-gcnew 显示新生代信息;

-gcnewcapacity 显示新生代大小和使用情况;

-gcold 显示老年代和永久代的信息;

-gcoldcapacity 显示老年代的大小;

-gcutil显示垃圾收集信息;

-gccause 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;

-printcompilation 输出JIT编译的方法信息;

-class

如下示例输出Java进程38604的ClassLoader相关信息。每秒钟统计一次信息,一共输出两次。

Loaded: 已经装载的类的数量

  • Bytes : 装载类所占用的字节数
  • Unloaded:已经卸载类的数量
  • Bytes:卸载类的字节数
  • Time:装载和卸载类所花费的时间

-gc

显示gc相关的堆信息,查看gc的次数,及时间。

S0C:年轻代中第一个survivor(幸存区)的容量 (字节)

S1C:年轻代中第二个survivor(幸存区)的容量 (字节)

S0U :年轻代中第一个survivor(幸存区)目前已使用空间 (字节)

S1U :年轻代中第二个survivor(幸存区)目前已使用空间 (字节)

EC :年轻代中Eden(伊甸园)的容量 (字节)

EU :年轻代中Eden(伊甸园)目前已使用空间 (字节)

OC:Old代的容量 (字节)

OU:Old代目前已使用空间 (字节)

MC:metaspace(元空间)的容量 (字节)

MU:metaspace(元空间)目前已使用空间 (字节)

YGC :从应用程序启动到采样时年轻代中gc次数

YGCT :从应用程序启动到采样时年轻代中gc所用时间(s)

FGC :从应用程序启动到采样时old代(全gc)gc次数

FGCT :从应用程序启动到采样时old代(全gc)gc所用时间(s)

GCT:从应用程序启动到采样时gc用的总时间(s)

-gccapacity

可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小

参数省略

-gcmetacapacity

metaspace 中对象的信息及其占用量。

参数省略

-gcnew

年轻代对象的信息。

参数省略

-gcnewcapacity

年轻代对象的信息及其占用量

参数省略

-gcold

old代对象的信息

参数省略

-gcoldcapacity

old代对象的信息及其占用量

参数省略

-gccause

显示最近一次GC的原因,以及当前GC的原因:

参数省略

3. 导出堆到文件 jmap

命令jmap是一个多功能的命令。

它可以生成Java程序的堆Dump文件,也可以查看堆内对象实例的统计信息、查看ClassIoader的信息以及finalizer队列。

如使用jmap生成PID为38604的Java程序的对象统计信息,并输出到s.txt文件中。

jmap -histo 38604 >d:\s.txt

jmap另一个更为重要的功能是得到Java程序的当前堆快照:

jmap -dump:format=b,file=d:\heap.hprof 38604

可以通过多种工具分析该堆文件。

比如jhat工具,或者Visual VM、MAT等工具。

4. JDK自带堆分析工具 jhat

使用jhat工具可以用于分析Java应用程序的堆快照内容。

jhat d:\heap.hprof

jhat在分析完成后,使用HTTP服务器展示其分析结果。

例如:在浏览器中访问http://127.0.0.1:7000

通过这些链接,开发者可以进一步查看所有类信息(包括Java平台的类)、所有类的实例数量以及实例的具体信息。

5. 可视化性能监控工具 Visual VM

Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具,它集成了多种性能统计工具的功能,

使用Visual VM可以代替jstat、 jmap、 jhat、 jstack, 甚至代替JConsole。

在JDK 6 Update 7以后,Visual VM便作为JDK的一部分发布,即:它完全免费。

打开jdk的安装目录双击

发现Visual VM除了本地连接外,也支持远程JMX连接。

Java 应用程序可以通过以下参数打开JMX端口:

找到远程的tomcat的bin下的catalina.sh,加入下面的启动参数

JAVA_OPTS="-Djava.rmi.server.hostname=192.168.0.108
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false"

第一步添加远程主机

输入对应的ip和端口就可以连接远程的java程序

1> 监控概况

通过Visual VM,可以查看应用程序的基本情况,比如进程ID、Main Class、启动参数等,

单击Tab页面上的监视页面,即可监控应用程序CPU、堆、永久区、类加载和线程数的总体情况。

通过页面上的"执行垃圾回收"和"堆Dump"按钮还可以手工执行Full GC和生成堆快照。

2> ThreadDump分析

Visual VM的线程页面( 如图所示)可以提供详细的线程信息。

单击右上角的" 线程Dump"按钮可以导出当前所有线程的堆栈信息。

如果Visual VM在当前程序中找到死锁,则会以十分显眼的方式在线程页面给予提示,如死锁程序

检测到死锁:会生成一个线程 Dump 以获取更多信息

3> 性能分析

Visual VM有两个采样器,在"Sampler"页面下,显示了CPU和内存两个性能采样器,用于实时地监控程序信息。

CPU采样器可以将CPU占用时间定位到方法,内存采样器可以查看当前程序的堆信息。

4> 内存快照分析

通过右键菜单中的"堆Dump"选项,可以立即获得当前应用程序的内存快照

内存快照分析功能,提供了4个基本功能页:概要、类、实例和OQL控制台。

  1. 概要页面展示了当前内存的整体信息,包括内存大小、实例总数、类总数等。
  2. 在类页面中,以类为索引,显示了每个类的实例数占用空间。
  3. 在实例页面中,将显示指定类的所有实例。开发者便可以查看当前内存中,内存数据的实际内容。
  4. OQL控制台提供了更为强大的对象查询功能。有关VisualVM的OQL支持。

九、堆分析

1. 堆溢出 OOM

堆是Java程序中最为重要的内存空间,由于大量的对象都直接分配在堆上,因此它也成为最有可能发生溢出的区

间。一般来说,绝大部分Java的内存溢出都属于这种情况。

其原因是因为大量对象占据了堆空间,而这些对象都持有强引用,导致无法回收,当对象大小之和大于由Xmx参

数指定的堆空间大小时,溢出错误就自然而然地发生了。

为了缓解堆溢出错误,一方面可以使用-Xmx参数指定一个更大的堆空间,另一方面,由于堆空间不可能无限增

长,通过下文提到的MAT或者Visual VM等工具,分析找到大量占用堆空间的对象,并在应用程序上做出合理的优

化也是十分必要的。

2. 直接内存溢出

在Java的NIO(New IO)中,支持直接内存的使用,也就是通过Java代码,获得一块堆外的内存空间,这块空间是

直接向操作系统申请的。

直接内存的申请速度一般要比堆内存慢,但是其访问速度要快于堆内存。

因此,对于那些可复用的,并且会被经常访问的空间,使用直接内存是可以提高系统性能的。

但由于直接内存没有被Java虚拟机完全托管,若使用不当,也容易触发直接内存溢出,导致宕机。

运行参数:-Xmx1g -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m

由于内存紧张,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发

生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最

大的堆外内存(直接内存)大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,为避免直接内存溢

出,在确保空间不浪费的基础上,可以降低直接内存溢出的概率,设置合理的-XX:MaxDirectMemorySize也可以

避免意外的内存溢出发生。

3. 过多的线程导致OOM

jvm的线程栈申请的内存空间属于堆外内存,是向操作系统申请的,也不是JVM直接内存,虽然类似。

JVM能创建的线程数需要的内存,不是JVM运行内存,堆内存,直接内存,而是操作系统剩余的可用内存,这个

也决定了能创建的线程数,如果内存不够用,创建线程的时候便会出现内存溢出的错误。

在操作系统的可用内存不足的情况下,想要创建更多的线程,可以考虑减少线程栈的空间大小(-Xss),但是不

建议过小,栈尝试减小容易栈溢出错误。

4. 元数据溢出

元数据区(Perm) 是存放类元数据的区域。

如果一个系统定了太多的类型,那么永久区是有可能溢出的。

在JDK 1.8中,永久区被-块称为元数据的区域替代,但是它们的功能是类似的,都是为了保存类的元信息。

解决方案:

① 增加MaxMateDataSize的值。

② 减少系统需要的类的数量。

③ 使用ClassLoader合理地装载各个类,并定期进行回收。

5. String字符串的优化

1> 不变性

不变性是指String 对象一旦生成,则不能再对它进行改变。

String 的这个特性可以泛化成不变(immutable)模式,即一个对象的状态在对象被创建之后就不再发生变化。

不变模式的主要作用在于,当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,

从而大幅提高系统性能。

2> 字符串常量池的优化

针对常量池的优化指当两个String 对象拥有相同的值时,它们只引用常量池中的同一个拷贝。

当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

public class StringConstants {

    public static void main(String[] args) {
        String str1 = new String("abc");
        String str2 = new String("abc");
        System.out.println(str1 == str2);   //false
        System.out.println(str1 == str2.intern());  //false
        System.out.println("abc" == str2.intern()); //true
        System.out.println(str1.intern() == str2.intern()); //true
    }
}

String.interm()返回字符串在常量池中的引用,显然它和strl也是不同的,

String.intern()始终和常量字符串相等,str1.intern() 与str2.intern()也相等。

3> 字符串常量的位置

jdk1.6之前是在方法区,我们可以看到字符串常量的位置在堆中

6. 使用MAT分析java堆

1> 初始MAT

  1. 解压压缩包
  2. 启动MAT

2> 浅堆和深堆

  1. 浅堆(ShallowHeap)是指一个对象所消耗的内存。
  2. 深堆(Retained Heap)要理解深堆,首先需要了解保留集( Retained Set)。对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。深堆是指对象的保留集中所有的对象的浅堆大小之和。

3> 支配树

MAT提供了一个称为支配树( Dominator Tree)的对象图。

支配树体现了对象实例间的支配关系。

在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。

如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。

支配树是基于对象间的引用图所建立的,它有以下基本性质:

如图所示,左图表示对象引用图,右图表示左图所对应的支配树。

对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B,因此对象C的直接支配者也是根对象。

对象F与对象D相互引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。

而到对象D的所有路径中,必然经过对象C,即使是从对象F到对象D的引用,从根节点出发,也是经过对象C的,所以,对象D的直接支配者

为对象C。

7. tomcat 溢出分析

Tomcat是最常用的JavaServlet容器之一,同时也可以当做单独的Web服务器使用.

Tomcat本身使用Java实现,并运行于Java虚拟机之上。

在大规模请求时,Tomcat 有可能会因为无法承受压力而发生内存溢出错误。

1> 准备tomcat

准备一个 tomcat 设置JAVA_OPT, 打开期catalina.bat

set JAVA_OPTS=-server -Xms100m -Xmx100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/tomcat.hprof

2> 准备Jemeter测试工具进行测试

相关推荐
山居秋暝LS26 分钟前
目标跟踪之sort算法(3)
python·算法·目标跟踪·sort
siy23335 小时前
[c语言日寄]assert函数功能详解
c语言·开发语言·笔记·学习·算法
Tester_孙大壮7 小时前
第26章 测试驱动开发(TDD)模式详解与 Python 实践
驱动开发·python·tdd
youcans_7 小时前
2025年数学建模美赛:A题分析(1)Testing Time: The Constant Wear On Stairs
python·数学建模·模型·频率·可靠性
Channing Lewis7 小时前
如何使用 Flask-Caching 提高性能?
后端·python·flask
大哥喝阔落7 小时前
GRAPHARG——学习
python·学习·flask
洒脱的六边形战士加辣8 小时前
Python Flask教程
开发语言·python·flask
Jelena157795857928 小时前
Python爬虫获取item_search_img-按图搜索淘宝商品(拍立淘)接口
爬虫·python·图搜索算法
小菜鸟博士9 小时前
手撕Diffusion系列 - 第九期 - 改进为Stable Diffusion(原理介绍)
人工智能·深度学习·学习·算法·面试
学编程的闹钟9 小时前
9【如何面对他人学习和生活中的刁难】
学习