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

时间:2025-01-27 07:14:47

目录

学习前言

一、虚拟机的结构

1. Java虚拟机参数设置

2. java 堆

3. 出入栈

4. 局部变量表

1> 局部变量的剖析

2> 局部变量的回收

5. 操作数栈

1> 常量入栈指令

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

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

6. 帧数据区

7. 栈上分配

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

二、常用的 java 虚拟机参数

1. 跟踪调试参数

2. 系统参数查看

三、堆内存的参数配置

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

2. 新生代的配置

-XX: SurvivorRatio

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

3. 堆溢出参数

四、非堆内存的参数配置

1. 方法区配置

2. 栈配置

3. 直接内存配置

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

五、垃圾回收算法

1. 垃圾回收的思想

2. 引用计数法

3. 标记清除法

4. 复制算法

5. 标记压缩算法

6. 分代算法

7. 分区算法

六、谁是真正的垃圾

1. 对象复活

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

强引用

软引用

弱引用

虚引用

七、垃圾回收器

1. 串行回收器

垃圾回收的停顿

新生代的串行

优点

缺点

应用场景

老年代的串行

2. 并行回收器

新生代并行回收器ParNew

新生代并行垃圾回收器ParallelGC

老年代并行垃圾回收器ParallelOldGC

3. CMS 垃圾回收器

CMS 参数设置

CMS 日志

4. G1回收器

新生代GC

回收过程

必要时FullGC

G1的参数设置

对象何时进入老年代

老年对象进入老年代

大对象进入老年代

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

5. 知识小结

与串行回收器相关的参数

与并行GC相关的参数

与CMS回收器相关的参数

与G1回收器相关的参数

八、性能监控工具

1. java 进程 Jps

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

-class

-gc

-gccapacity

-gcmetacapacity

-gcnew

-gcnewcapacity

-gcold

-gcoldcapacity

-gccause

3. 导出堆到文件 jmap

4. JDK自带堆分析工具 jhat

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

1> 监控概况

2> ThreadDump分析

3> 性能分析

4> 内存快照分析

九、堆分析

1. 堆溢出 OOM

2. 直接内存溢出

3. 过多的线程导致OOM

4. 元数据溢出

5. String字符串的优化

1> 不变性

2> 字符串常量池的优化

3> 字符串常量的位置

6. 使用MAT分析java堆

1> 初始MAT

2> 浅堆和深堆

3> 支配树

7. tomcat 溢出分析

1> 准备tomcat

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指令;另外一种是抛出异常。

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

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

注意:

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

法继续进行下去。

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

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,并将