Java基础 之 JVM

JVM基础

 

JVM

JVM是一种规范,基于这套规范的jvm平台可以通过字节码指令集及内存管理来虚构出一台计算机,任何语言符合JVM规范并编译成class文件,即可以在JVM虚拟机上运行。目前常见的JVM实现,常用的有Hotspot,也有TaobaoVM,J9,LiquidVM,Jrockit,Microsoft VM,azul zing等,通过java -version 即可查看当前的虚拟机平台。

JVM虚拟机可以加载运行应用程序,在JVM基础上,附加核心库(core lib) ,即构成JRE运行时环境,在JRE之上封装了开发包(development kit)即构成JDK,本文分析的是已发行的JDK8版本,其包容关系如下:

Java基础 之 JVM_第1张图片

JVM的三个组成部分,包括:

  • 类加载系统:装载class文件数据到元空间(方法区)
  • 运行时数据区:运行时数据区保存运行数据
  • 执行引擎:执行具体的指令(代码)

三个部分组成结构如下图:

Java基础 之 JVM_第2张图片

JVM通过这三个组件完成程序运行,三部件执行的整体流程是:类加载器加载编译的class字节码文件到运行时数据区,执行引擎执行具体的指令。更具体的JVM架构如下图:

Java基础 之 JVM_第3张图片

class文件结构

通过官网可知,class文件结构包含了以下内容:

Java基础 之 JVM_第4张图片

依次对应如下:

  • 魔数

  • 次版本,主版本号

  • 常量池大小,常量池表[池大小-1]常量池中包含了以下字段、方法、接口、属性等的名称信息、索引信息、长度信息、权限信息、数据类型信息等

  • class的access权限,当前class索引,父类class索引

  • 接口池大小,接口表[池大小-1]

  • 字段池大小,字段表定义的常量[池大小-1]

  • 方法池大小,方法表[池大小-1]

  • 其他属性池大小,其他属性表[池大小-1],例如内部类等

不管是scala,或者java等其他语言编译后形成class文件,可通过二进制编辑工具,可直接查看编译后的class文件数据,以下是通过IDEA插件BinED进行查看的情况:

Java基础 之 JVM_第5张图片

二进制文件中按照class文件结构和步长进行16进制读取,可以获取到常量池、属性、方法、java汇编指令等信息。为了将class二进制可视化,可以使用javap命令来分析文件结构,这里使用IDEA的插件jclasslib来查看,可参考上述官网的class文件结构进行查看分析:

Java基础 之 JVM_第6张图片

类加载系统

class二进制文件要想运行,需要加载到内存使得执行引擎可以运行。class文件加载到内存的流程如下:

Java基础 之 JVM_第7张图片

这个流程大体分为3个流程,即加载、链接和初始化,其中链接流程又分为验证、准备和解析的过程,各个流程简单概括如下:

  • 加载:类加载器加载class文件到内存

  • 链接:元空间符号引用转换成直接引用。执行代码放在原空间常量池,具体执行的指令与元空间常量进行关联。

    • 验证:class文件合法性

    • 准备:静态变量赋默认值

    • 解析:常量池引用转换为内存地址的引用

  • 初始化:静态变量赋初始值

类加载器

Launcher是java程序启动入口类,在启动java应用的时候会首先创建Launcher类,并准备应用程序运行中需要的类加载器。jvm的类加载有三个基本类型和特殊的线程上下文加载器。这三个基本的类加载器就是引导类加载器(Bootstrap)、扩展类加载器(Extension)以及系统程序类加载器(Application)。

类加载可看作是类的命名空间,同一个类由不同的类加载器加载得到对象不相同

双亲委派机制

加载某个类的class文件时,这些类加载器有加载顺序,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,具体的流程如下图:

Java基础 之 JVM_第8张图片

简单描述,就是当 JVM 接收到需要加载类的请求时,先自下而上从各自的类加载器缓存中判断该类是否已经加载,如果加载则返回,如果未加载则委托给父加载器判断,一直到引导类加载器(Boostrap)为止,如果所有 ClassLoader 都未加载,则自上向下委托加载该类,直到加载成功或报出异常

需要留意的是,父加载器,并非是被继承的加载器,也不是加载器的加载器,而是一个引用组合的关系。最顶层的加载器是Bootstrap类加载器,由于它是用c++写的,并不继承于java.lang.ClassLoader,所以在返回该ClassLoader时就会返回null,意味着当某个类的加载器为空时,意味着该类的类加载器是引导类加载器

双亲委派机制主要是涉及到安全稳定问题,如果类被一个加载器加载,就不用再从父类加载器加载,如果父类加载器未加载交由后续加载器加载,也能防止父类加载器的类被破坏。

自定义类加载器

自定义加载器,就是定义如何读取字节码文件和加载链接初始化的过程。在ClassLoader源码中的loadClass()方法递归实现了双亲委派机制,因此覆盖这个方法就可以破坏掉双亲委派机制,如果不想破坏双亲委派机制,则重写findClass()方法即可,最终通过defineClass()方法来实现类的链接初始化。

Java基础 之 JVM_第9张图片

这里以一个加密的class文件作为示例:

首先正常编译class文件,然后通过IO读取文件字节并对每个字节异或,具体异或值即为加密的密文,然后保存为新的class文件,这样class文件即加密了,可以进行共享分发了。

为了使用加密类的方法,需要构建一个解密加载器,再重写findClass()方法时,添加解密逻辑(反加密,这里使用异或操作),然后实例化这个解密加载器并按照加密类的类名进行加载,这样就能实例化加密的类了。

下面就行简单的自定义加载操作,不做加解密操作,只有一个正常类和一个加载器类,不使用正常类的new方法实例化,而是用类加载器加载并实例化,可观察到最终实例化的加载器是应用加载器(AppClassLoader),名称上也不是custome之类的其他类加载器。

Java基础 之 JVM_第10张图片

运行时内存结构

JVM加载.class字节码文件后产生数据存放的区域就是JVM内存区域,这个区域的结构如下:

Java基础 之 JVM_第11张图片

可以看到运行时数据区主要分为以下几个部分:

  • 元空间:也叫方法区,线程共享,存放类的信息引用、常量池等;
  • 堆空间:线程共享,存放实际创建的对象,动态分配内存,不需要的对象会由JVM进行回收,JVM性能调优的一部分就是对堆空间对象内存的调优
  • 线程栈:线程独享,是方法的栈帧,
  • 本地方法区:运行native方法产生的数据会保存在本地方法区,
  • 程序计数器:存储下一条将要执行的指令地址;

内存区域按线程共享与否分为两部分,其中蓝绿色部分是线程共享的,浅黄色部分是线程私有的。栈、本地方法栈、程序计数器是每个线程私有的,因此不会有线程安全问题,而原空间(方法区)、堆是线程共享的,因此会有线程安全问题其运行时内存的分配过程简述如下:

  • 类加载器加载类并将类的元信息存入元空间,这个元空间包括了类的执行指令
  • 类加载器通知执行引擎执行指令,执行引擎从元空间获取执行指令并开始执行
  • 执行指令时,如果创建了对象则放在中(但不是全部);如果创建了线程就创建线程栈中,如果执行本地native方法产生了数据则放入本法方法栈

元空间

存储类的元信息,类的常量池、静态部分等。

存放new出来的对象,当然并不是所有的对象都存放在堆空间中,也可能存在于线程栈中。在对象的逃逸分析章节中有描述。

线程之:线程栈

线程栈中存储的是栈帧。栈帧存储了方法的变量表、操作数栈、动态链接和方法返回等信息,方法从调用开始到执行结束的整个过程,就对应着一个栈帧的入栈出栈过程

变量表存储了了方法参数和内部的局部变量,操作数栈存放了,动态链接指向了栈帧方法的引用,方法返地址则用于恢复调用状态。以一个springboot引用程序的入口程序为例:

该springboot启动类只有一个主线程并有一个main方法,并且只调用了run方法,因此这里有两个栈帧,分别是main和run的栈帧。可以通过javap指令来打印类的栈结构:

Java基础 之 JVM_第12张图片

Java基础 之 JVM_第13张图片

Java基础 之 JVM_第14张图片

通过javap -v StudyApplication.class编译后的指令可知,大体流程如下:

  1. 通过new 等指令创建对象并存入堆中,对象地址加载到局部变量表中;
  2. 通过ldc、aload_x、iload_x等指令将对象地址存入操作数栈并进行计算并将结果引用放入变量表中;
  3. 通过invokexxxx等指令压入其他栈并进行初始化后进行计算,当调用的栈执行完成会通过pop指令使得当前栈帧弹出线程栈并返回结果引用;
  4. 通过pop从线程栈中弹出当前栈帧;
  5. 通过return返回结果引用;

不太准确但直观的理解:局部变量表用于对象或值的存储;操作数栈用于值的计算;动态链接用于将方法的符号引用转化为执行指令的引用(指令代码);方法返回用于指示结果存储的寄存器地址。

线程之:本地方法栈

线程私有的,存储运行本地方法(native)产生的数据。

线程之:程序计数器

线程私有的,用于存储当前线程正在执行的Java方法的JVM指令地址,配合执行引擎来执行命令。

对象创建

对象的创建分为以下几个步骤:

  • 类加载校验:校验类的加载
  • 分配堆内存:为对象分配内存
  • 初始化:根据数据类型分配初始值
  • 设置对象头:设置对象信息,包括hash码、锁状态、对象年龄、类的元信息等;
  • 执行init方法 :

如下图所示:

Java基础 之 JVM_第15张图片

分配内存

为对象分配内存时,按照内存空间地址连续性与否的分配策略如下:

  • 指针碰撞:Bump the Pointer,内存地址连续规整,指针会作为已分配和未分配内存的分界线,给对象 分配内存的过程,就是分界线指针移动下一个对象大小的过程
  • 空闲列表:Free List ,内存空间不规整,JVM会维护一个可用空间的列表用以分配,类似于一个内存空间的LinkedList链表,分配对象就是在空闲空间插入对象数据的过程,空闲列表记录数据地址并动态变化;

内存分配产生的并发问题:

  • 自旋分配:CAS,并发分配失败则进行重试分配;
  • TLAB:JVM为每个线程分配一块空间,每个线程在自己的空间创建对象;

JVM优先分配到Eden区,如果空间足够则申请结束;如果Eden区空间不足,会发生Minor GC去清除不活跃的对象,如果Minor GC 后空间不足,JVM会试图把Eden区对象转移一部分到Survivor区;如果Survivor区对象晋升老年代则会分配到Old老年区中,如果老年代区满了也无法分配对象,则会发生Full  GC,如果Full GC后Eden和Old区都无法分配对象时,则会出现out of memory的情况。

设置初始值

根据对象的数据类型,为对象的属性进行属性初始化。

设置对象头

对象在内存中的布局如下:

Java基础 之 JVM_第16张图片

可以看出一个对象内存包括:对象头、实例数据以及对齐填充,非常细致的解读请参考一文聊透对象在JVM中的内存布局,以及内存对齐和压缩指针的原理及应用。

其中,对象头信息由MarkWord和KlassPointer组成。

Mark Word

mark word包含了对象的锁状态、hashcode、对象年龄等信息,下表是对象在不同锁状态下其MakrWord字段的情况,java开发中synchronized就是读取对象的锁状态来判断并发性。

Java基础 之 JVM_第17张图片

Klass Pointer

Klass Pointer指类型指针,指向了元空间当前类的类元信息。当调用对象的方法时,其实是读取元空间类信息中的方法。

指针压缩

对象头信息根据对象是否为数组类型而有所不同,如果为数组类型,对象头信息会多出4字节的数组长度信息,否则就没有;对象头的Mark部分固定长度8字节,未压缩的指针部分为8字节长度,压缩过的为4字节长度,而JVM要求对象内存占用为8的倍数,因此减少的4字节作为空闲gap进行补齐

指针数字指向元空间的类元信息,对象头的压缩可以减小对象总的空间占用,减少GC。

Java基础 之 JVM_第18张图片

JVM参数UseCompressedOops可用来设置是否开启指针压缩,jdk8已默认开启。写一段程序为例,在IDEA的VM参数中填入-XX:-UseCompressedOops或-XX:-UseCompressedOops,分别打印关闭和打开对象压缩后的对象头信息,注意关注Klass Point即(object header:class)的部分。

import org.openjdk.jol.info.ClassLayout;

public class KlassPointer {

    public static void main(String[] args) {
        KlassPointer klassPointer = new KlassPointer();
        System.out.println(ClassLayout.parseInstance(klassPointer).toPrintable());
        System.out.println("========================================");
        System.out.println(ClassLayout.parseInstance(new KlassPointer[]{klassPointer}).toPrintable());
    }
}

Java基础 之 JVM_第19张图片

Java基础 之 JVM_第20张图片

通过程序测试,开启压缩后,klass pinter部分原先占用的8字节空间变成4字节空间,为满足8的倍数关系需要对齐压缩掉的4字节。

对象数据

java代码中定义的属性和值;

对齐填充

JVM要求java对象占用内存大小为8 bit的倍数,因此对齐填充就是把不是8bit倍数字节数的补齐为8的倍数。

执行init方法

默认class类实例化对象时都会执行的默认方法,这个在class字节码反编译后可查看到,例如通过jclasslib插件或者javap查看以上程序,可看到JVM中默认的init方法,不过这个方法无法被重写。

Java基础 之 JVM_第21张图片

垃圾回收

垃圾回收是JVM提供的空闲时间回收垃圾对象的机制。回收的时间有三个时机:

  • cpu空闲;
  • 对象存储空间满;
  • system.gc()主动调用垃圾收集器进行回收,但不保证回收成功;

垃圾对象的判定

垃圾对象的判定算法有两个:

  • 引用计数法
  • 可达性分析

引用计数法:当对象被引用了,则对象计数+1,当计数为0则对象被判定为垃圾,缺点是无法解决循环依赖对象问题,Hotspot未采用这种方法;

可达性分析

    JVM运行时内存结构中,对象的引用可能在元空间,可能在线程栈的变量表中,也可能在本地方法栈中,就把这些引用作为GC Root 根节点,其他节点挂载到根节点上,垃圾回收会从 GC Roots 这个集合的引用链去寻找回收对象[深度优先bi],如下图所示

Java基础 之 JVM_第22张图片

当对象不在GC Root引用链中则该对象就应该被回收。

对象的finalize方法

object类中有一个finalize方法,因此任何对象都有finalize方法,这个方法是对象被回收前的最后一根救命稻草。

  • GC回收对象前会标记垃圾对象,回收是对象的finalize方法会被调用;
  • 垃圾对象的finalize方式调用时,如果对象没被引用,则会被回收;
  • 垃圾对象的finalize方法调用时,如果对象被引用则对象被二次标记并被移除回收列表,对象存活;
  • finalize方法只会被调用一次

以一段程序为例子,注意在JVM参数中设置NewSize参数,即-Xmn2m,然后执行:

public class Finalize {
    public static void main(String[] args) {
        List userList = new ArrayList<>();
        int i = 0, j = 1000;
        for (int k = 0; k < 500; k++) {
            userList.add(new User(i++));
            new User(userList, j++);
        }
    }

    @Data
    static class User {
        private List userList;
        private int id;

        public User(List userList, int id) {
            this.userList = userList;
            this.id = id;
        }

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

        @Override
        protected void finalize() throws Throwable {
            if (id % 10 == 0) {
                userList.add(this);
                System.out.println("不回收对象:" + id);
            } else {
                System.out.println("对象被回收:" + id);
            }
        }
    }
}

其运行结果如图:

Java基础 之 JVM_第23张图片

 这段程序的userList引用存在于线程栈的局部变量表中,是在GC Root根节点中,然后循环体添加User对象到GC Root引用链中,因此这些对象都不会进行GC操作;循环体中新建的User对象没有指向其他引用,因此都是垃圾,但是在垃圾回收时执行finalize方法发现,id是10倍数的对象会添加到userList这个GC Root引用链中,因此不会被垃圾回收,非10倍数的对象依然会被垃圾回收。这些对象在JVM内存中的对象分配情况如下:

Java基础 之 JVM_第24张图片

对象的逃逸分析

通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上。如果对象都放在堆上,则GC压力大并且会产生更多的内存碎片,因此jdk1.7以后默认增加了逃逸分析,如果JVM发现对象没有逃逸出方法则优先分配到线程栈中,以一段程序为例:

​
public class EscapeAnalysis {

    // 堆上分配对象
    public MyUser test1() {
        return new MyUser();
    }

    // 线程栈上分配对象
    public void test2() {
        MyUser user = new MyUser();
    }
}

通过Return可知,test1()方法中发生了对象逃逸,对象很可能会分配到堆中,test2()则很可能分配到栈中,分配到栈中的好处是对象随栈销毁而不会发生GC。

JVM逃逸分析的规则是:

  • 对象有被引用,则对象分配到堆上;
  • 对象未被引用,则对象分配到栈上,如果对象过大仍可能会分配到堆中。

垃圾回收算法

垃圾回收算法常见四种:标记清除、复制算法、标记整理、分代回收。

标记清除算法

通过GC Root可达性分析,标记可达性对象,GC时不可达对象将被清除,动图如下:

算法产生的问题:

  • 标记和清除效率低,需要从GC Root根节点遍历所有堆内对象
  • GC产生的STW影响吞吐率
  • 清理之后产生内存碎片

复制算法

将内存分为大小相同的两块区域,每次只是用一个区域,在任意时刻所有对象只能分配到其中一个内存区域,另一块区域是空闲的,清理步骤如下:

  • gc线程将存活对象按顺序复制到另外一块空闲区域,分配后的对象依次排列规整,对象引用地址动态更新;
  • 分配后的空闲区域将被清理掉,这样每次回收都会回收一半空闲区域;

复制算法动图如下:

这个算法的问题:

  • 只使用了一半的内存区域,内存利用率低;
  • 如果对象存活数量多,很多对象的复制和地址更新耗费时间;

复制算法主要用于新生代存活率低的对象。

标记整理算法

标记整理于标记清除类似,比标记清理多了一步移动操作,具体步骤如下:

  • 标记:GC Root遍历标记可达对象;
  • 整理:将可达(存活)对象向空闲空间移动,然后清理指针末端的空闲区域;

标记整理节省了空间,使对象地址连续规整,但依然需要gc root遍历标记可达(存活)对象,加上对象移动操作,因此标记整理操作具有更高的使用成本,其回收动图如下:

分代回收

JVM按照对象的不同生命周期来划分不同的内存区域,不同区域采用不同的回收算法,参考下图:

堆内存分类

 JVM的堆内存分为新生代、老年代,默认占比1:2,其中新生代又划分为Eden和S1 From/S2 TO三块内存区域,Eden、S1、S2默认占比8:1:1。

分代回收的大致流程:对象分配到Eden区,当Eden区满发生一次Minor GC,存活的对象会分配到survivor区,当survivor区对象熬过15次Minor GC(15岁),对象被分配到老年区,当老年区满就发生Full GC,此时产生STW现象,线程暂停,如果Full GC后老年区依然没有空间存放对象,则会发生OOM现象。

详细的流程如下:

  • 新对象会分配到Eden区,From、To区是空的;
  • Eden区满发生一次Minor GC,垃圾对象会被清除,存活的对象年龄+1,并被移动到From区;
  • Eden区满再次Minor GC,Eden区和From 区垃圾对象被清除,存活对象年龄+1,并被移动到TO区,From区空间清空
  • Eden区满再次Minor GC,Eden区和To 区垃圾对象被清除,存活对象年龄+1,并被移动到From区,To区空间清空
  • Eden区满再次Minor GC,From或To区的对象该清除的清除,年龄达到15岁则被分配到老年区
  • 多次Minor GC后,当老年区满,发生Full GC,此时发生STW暂停
  • 如果Full GC后堆空间不足会报OOM异常;

注意:From、To区没有必然的先后顺序,地位相同,From区可能存在来自Eden或To区的对象,同样To区可能存在来自Eden或From区的对象,不过转移复制对象时总有一个为空的

在新生代中,采用标记复制算法,在老年代中采用标记整理算法,动图如下

这里写一个循环创建对象的案例来演示OOM,

public class GCTest {

    private byte[] object = new byte[1042*1000];

    public static void main(String[] args) throws InterruptedException {
        ArrayList gcTests = new ArrayList<>();
        while (true) {
            gcTests.add(new GCTest());
            Thread.sleep(3);
        }
    }
}

通过jvisualvm客户端工具,配置visual gc 插件来查看gc过程。

Java基础 之 JVM_第25张图片

Java基础 之 JVM_第26张图片

Java基础 之 JVM_第27张图片

 运行结果如下图所示:

Java基础 之 JVM_第28张图片

 对象进入老年代的条件

  • 当对象年龄大于15岁的时候
  • 当对象很大的时候,在From、TO区复制移动会消耗空间消耗时间
  • 老年代分摊担保机制

垃圾回收器

实现垃圾回收,有很多实现回收的算法,包括Serial收集器、Parallel收集器、ParNew收集器、CMS收集器。

Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

单线程垃圾收集器,在收集过程中会有较长时间的STW,GC执行完成用户线程继续执行,单线程垃圾回收比较简单直接。 

Java基础 之 JVM_第29张图片

Parallel收集器(-XX:+UseParallelGC,-XX:+UseParallelOldGC)

多线程垃圾收集器,充分利用CPU,吞吐量高。

Java基础 之 JVM_第30张图片

ParNew收集器(-XX:+UseParNewGC)

原理与Parallel相同,不过可以配合CMS收集器进行工作。

CMS收集器

使得用户线程和gc线程并发执行,尽量减少STW时间,简单描述就是gc与用户线程并发标记并发清理,过程如下图所示:

Java基础 之 JVM_第31张图片

初始标记:暂停一次所有线程(stw)

并发标记:gc roots遍历所有对象,同时用户线程也在并发执行;

重新标记:修正并发过程中的对象的状态,通过三色标记算法来实现;

并发清理:gc开始清理对象,同时用户线程也在并发执行;

并发重置:重置gc过程中的标记数据; 

三色标记算法

并发过程中对象的状态可能发生改变,因此使用三色标记来重新标识:

黑色:对象及其引用被gc roots遍历,重新标记为黑色,不会被回收;

灰色:对象及部分引用被gc roots遍历,重新标记为灰色;

白色:对象未被gc roots遍历,会被回收;

垃圾收集器组合方案

不同的垃圾收集器在不同的生命周期下组合使用。

年轻代 老年代 特点
Serial Serial Old 简单直接
Serial CMS
ParNew CMS 推荐使用
ParNew Serial Old
Paralle Parallel Old 吞吐量高,jdk8默认使用
Paralle Serial Old

JVM调优

JVM调优

JVM调优到底调的是什么?JVM出现了什么问题而需要调优呢?jvm调优调的是稳定,并不保证带来性能上的极大提升,可参考的需要调优的情况有:

  • Full GC次数频繁
  • OOM
  • 系统吞吐量或性能不高或下降
  • GC停顿时间过长

通过分析GC日志,观察cpu、内存性能等可视化指标,来确定jvm优化目标,例如减少GC次数和停顿时常,gc回收规律干净,降低内存占用增加吞吐量等,然后通过jvm参数来调整对象的堆内存分配和垃圾回收算法等来综合测试调参,来让程序性能更稳定更高效。

JVM调优参数

在官方JDK1.8的JVM文档中,详细描述了GC优化的点,包括了堆大小调优和收集器gc算法调优

Java基础 之 JVM_第32张图片

,当然还有其他方面的优化,实践中常常对堆和GC算法进行优化,实现方式就是向传递JVM参数,JVM参数常用三类参数如下:

参数         标准 描述
- 标准参数 全部的JVM都必须实现,向后兼容
-X 非标准参数        默认实现,不保证全部jvm都满足,不保证向后兼容
-XX 非stable参数 不同jvm实现会不一样,未来可能会取

的发射点

其中通用参数-常用如下:

  • -verbose:gc:程序运行时打印GC信息
  • -version:查看版本信息

堆的调优参数-Xm..

  • -Xmx: maximum size of heap memory,堆内存初始值
  • -Xms: minimum size of heap memory,堆内存最小值
  • -Xmn:memory new,堆内存中新生代内存

线程栈调优参数为-Xs..

  • -Xss:stack size ,线程栈大小

常用的调优参数与作用位置如下图:

Java基础 之 JVM_第33张图片

JVM GC日志

-XX后面的+号表示显式增加参数,-号反之,实践中可以使用-XX:+PrintGCDetails参数来打印查看GC日志,例如:

 对于打印的GC日志格式解读如下图,引用自JVM 实战 | JAVACORE

Java基础 之 JVM_第34张图片

Java基础 之 JVM_第35张图片

可以增加如下参数来生成GC日志文件:

  • -XX:+UseGCLogFileRotation
  • -Xloggc:C:\Users\root\Desktop\log\gc.log

 实际运行情况如下:

Java基础 之 JVM_第36张图片

具体的日志内容如下: 

Java基础 之 JVM_第37张图片

为了对GC日志进行可视化,可以通过在线GC解析服务来读取并分析gc日志并生成分析报告,如下:

Java基础 之 JVM_第38张图片

通过报告内存视图可以看出当前引用堆内存分配情况和峰值,通过参数可以适当调节分配比例和大小,通过GC视图来查看程序GC频率GC时间等,通过参数设置来进行优化并进行优化前后的对比来决定最佳参数,如下:

Java基础 之 JVM_第39张图片

Java基础 之 JVM_第40张图片

Java基础 之 JVM_第41张图片

引用及参考链接:

  • [JVM] JVM调优相关总结 - Vagrant。 - 博客园
  • 20.GC日志详解及日志分析工具 - 盛开的太阳 - 博客园
  • 老大难的 GC 原理及调优,这下全说清楚了 - AIQ
  • 动图图解GC算法 - 让垃圾回收动起来! - 码农参上 - 博客园

你可能感兴趣的:(java)