性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优

目录导航

    • 前言
    • JVM相关工具总结
      • JVM参数
        • 标准参数
        • -X参数
        • -XX参数
        • 其他参数
        • 说明
      • JVM常用命令
        • jps
        • jinfo
        • jstat
        • jstack
        • jmap
      • JVM常用工具
        • JConsole
        • jvisualvm
        • 内存分析MAT
        • GC日志分析工具
    • JVM调优实战
      • 重新认知JVM
      • Java源码和反编译文件
      • 重新认知类加载机制
        • 装载
        • 链接
          • 验证
          • 准备
          • 解析
        • 初始化
      • 堆内存溢出
        • 测试用例
        • 运行结果
        • 优化分析
      • 方法区内存溢出
        • 测试用例
        • 运行结果
      • 虚拟机栈
        • 测试用例
        • 运行结果
        • 说明
      • 线程死锁
        • 测试用例
        • 运行结果
        • 优化分析
      • 垃圾回收
        • 垃圾收集发生的时机
        • 实验环境准备
        • GC日志文件
          • Parallel GC日志
          • CMS 日志
          • G1 日志
        • GC日志文件分析工具
          • gceasy
          • GCViewer
        • G1调优
        • G1调优的最佳实践
    • 写在最后

前言

性能优化专题共计四个部分,分别是:

  • Tomcat 性能优化
  • MySql 性能优化
  • JVM 性能优化
  • 性能测试

本节是性能优化专题第二部分 —— JVM 性能优化篇,共计六个小节,分别是:

  1. JVM介绍与入门
  2. 类文件讲解
  3. 字节码执行引擎
  4. GC算法与调优
  5. Java内存模型与锁优化
  6. Linux性能监控与调优

通过这六节的学习,你将学到:

➢ 了解JVM内存模型以及每个分区详解。
➢ 熟悉运行时数据区,特别是堆内存结构和特点。
➢ 熟悉GC三种收集方法的原理和特点。
➢ 熟练使用GC调优工具,快速诊断线上问题。
➢ 生产环境CPU负载升高怎么处理?
➢ 生产环境给应用分配多少线程合适?
➢ JVM字节码是什么东西?

JVM相关工具总结

工欲善其事,必先利其器!在Linux上调优之前,我们看看需要掌握哪些常见的JVM相关的工具以及命令的使用呢?

JVM参数

经过前面的各种分析学习,我们知道了关于JVM很多的知识,比如版本信息,类加载,堆,方法区,垃圾回
收等,但是总觉得心里不踏实,原因是没看到实际的一些东西。
所以这一章节,咱们就好好来聊一聊关于怎么将这些内容进行直观地展示在我们面前,包括怎么进行相应的
一些设置。OK,let’s go!

标准参数

-version

-help

-server

-cp

在这里插入图片描述

-X参数

非标准参数,也就是在JDK各个版本中可能会变动

-Xint	
#解释执行

-Xcomp	
#第一次使用就编译成本地代码

-Xmixed	
#混合模式,JVM自己来决定

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第1张图片

-XX参数

使用得最多的参数类型

非标准化参数,相对不稳定,主要用于JVM调优和Debug

  • Boolean类型

格式:-XX:[+-] +或-表示启用或者禁用name属性

比如:-XX:+UseConcMarkSweepGC 表示启用CMS类型的垃圾回收器

-XX:+UseG1GC 表示启用G1类型的垃圾回收器

  • 非Boolean类型

格式:-XX=表示name属性的值是value

比如:-XX:MaxGCPauseMillis=500

想要设置的话,得先知道默认JVM中参数相关的信息,下面这个针对的是java这个进程

java -XX:+PrintFlagsFinal -version > flags.txt

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第2张图片
如果要查看一个运行中的JVM相关参数的信息,可以使用jinfo,不过要先知道Java进程的ID。

比如启动一个tomcat,它的PID为2908,如下图过程所示:
性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第3张图片

如果想通过jinfo查看更多
性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第4张图片
设置参数的常见方式:

  • 开发工具中设置比如IDEA,eclipse

  • 运行jar包的时候:java -XX:+UseG1GC xxx.jar

  • web容器比如tomcat,可以在脚本中的进行设置

  • 通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)

其他参数

-Xms1000
# 等价于-XX:InitialHeapSize=1000

-Xmx1000
# 等价于-XX:MaxHeapSize=1000

-Xss100
# 等价于-XX:ThreadStackSize=100

所以这块也相当于是-XX类型的参数

说明

一般要设置参数,可以先查看一下当前参数是什么,值得注意的是"=“表示默认值,”:="表示被用户或JVM修改后的值。

JVM常用命令

官网

jps

The jps command lists the instrumented Java HotSpot VMs on the target system. The command is limited to reporting information on JVMs for which it has the access permissions.

jps命令列出了目标系统上已检测到的Java HotSpot VM。该命令仅限于报告有关其具有访问权限的JVM的信息。
性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第5张图片

jinfo

实时查看和调整JVM配置参数

The jinfo command prints Java configuration information for a specified Java process or core file or a remote debug server. The configuration information includes Java system properties and Java Virtual Machine (JVM) command-line flags.

jinfo命令显示指定的Java进程或核心文件或远程调试服务器的Java配置信息。配置信息包括Java系统属性和Java虚拟机(JVM)命令行标志。

查看用法:

jinfo -flag name PID 
# 查看某个java进程的name属性的值

比如:

jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第6张图片

调整用法:
参数只有被标记为manageable的flags可以被实时修改

jinfo -flag [+|-] PID jinfo -flag <name>=<value> PID

查看曾经赋过值的一些参数:

jinfo -flags PID

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第7张图片

jstat

查看虚拟机性能统计信息

The jstat command displays performance statistics for an instrumented Java HotSpot VM. The target JVM is identified by its virtual machine identifier, or vmid option.

jstat命令显示已检测的Java HotSpot VM的性能统计信息。目标JVM由其虚拟机标识符或vmid选项标识。

查看类装载信息

jstat -class PID 1000 10
# 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次

比如:

jstat -class PID 1000 10

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第8张图片
查看垃圾收集信息

jstat -gc PID 1000 10

在这里插入图片描述

jstack

查看线程堆栈信息

The jstack command prints Java stack traces of Java threads for a specified Java process, core file, or remote debug server.

jstack命令为指定的Java进程,核心文件或远程调试服务器打印Java线程的Java堆栈跟踪。

用法:

jstack PID

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第9张图片

jmap

生成堆转储快照

The jmap command prints shared object memory maps or heap memory details of a specified process, core file, or remote debug server.

jmap命令打印指定进程,核心文件或远程调试服务器的共享对象内存映射或堆内存详细信息。

打印出堆内存相关信息

jmap -heap PID

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第10张图片

dump出堆内存相关信息:

jmap -dump:format=b,file=heap.hprof PID

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第11张图片
关于dump下来的文件
一般dump下来的文件直接看有些费力,可以结合MAT工具来分析。
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件

-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=heapdump.hprof

JVM常用工具

JConsole

JConsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用情况、类加
载情况等。

jvisualvm

可以监控本地的java进程的CPU,类,线程等

内存分析MAT

https://help.eclipse.org/2020-12/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html

GC日志分析工具

要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,根据前面参数的学习,下面的配置很
容易看懂

-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDateStamps 
-Xloggc:$CATALINA_HOME/logs/gc.log
  • 在线分析
    http://gceasy.io
  • 离线分析

GCViewer

JVM调优实战

重新认知JVM

通过前面的知识铺垫,我们基本了解了JVM整体的工作流程。现在我们整理一下整体架构:
性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第12张图片
除此之外,从Class文件往前推的话,肯定是Java源码文件,经过编译之后生成Class文件。
所以接下来咱们就从Java源码文件开始慢慢聊。

Java源码和反编译文件

public class Person{
    
    private String name="Jack"; 
   
    private int age; 
    
    private final double salary=100; 
    
    private static String address; 
    
    private final static String hobby="Programming"; 
    
    private Object obj=new Object(); 
    
    public void say(){ 
        
        System.out.println("person say..."); 
    
    }
    public static int calc(int op1,int op2){ 
        op1=3; 
        int result=op1+op2; 
        Object o=obj; 
        return result; 
    }
    
    public static void main(String[] args){
        System.out.println(calc(1,2)); 
    } 
}

此时你需要一个能够看懂反编译指令的宝典
首先我们获取到字节码文件:

javac Person.java

得到Person.class,因为都是二进制的数字嘛,我们当然可以根据Oracle官方文档自己去破译字节码文件,这里我们则使用JVM自带工具,javap进行反编译:

javap -v Person.class
  Compiled from "Person.java"
public class com.testjvm.domain.Person
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #13.#39        // java/lang/Object."":()V
   #2 = String             #40            // Jack
   #3 = Fieldref           #14.#41        // com/testjvm/domain/Person.name:Ljava/lang/String;
   #4 = Double             100.0d
   #6 = Fieldref           #14.#42        // com/testjvm/domain/Person.salary:D
   #7 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;
   #8 = String             #45            // person say...
   #9 = Methodref          #46.#47        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = Fieldref           #14.#48        // com/testjvm/domain/Person.obj:Ljava/lang/Object;
  #11 = Methodref          #14.#49        // com/testjvm/domain/Person.calc:(II)I
  #12 = Methodref          #46.#50        // java/io/PrintStream.println:(I)V
  #13 = Class              #51            // java/lang/Object
  #14 = Class              #52            // com/testjvm/domain/Person
  #15 = Utf8               name
  #16 = Utf8               Ljava/lang/String;
  #17 = Utf8               age
  #18 = Utf8               I
  #19 = Utf8               salary
  #20 = Utf8               D
  #21 = Utf8               ConstantValue
  #22 = Utf8               address
  #23 = Utf8               hobby
  #24 = String             #53            // Programming
  #25 = Utf8               obj
  #26 = Utf8               Ljava/lang/Object;
  #27 = Utf8               
  #28 = Utf8               ()V
{
     
  public com.testjvm.domain.Person();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: ldc           #2                  // String Jack
         7: putfield      #3                  // Field name:Ljava/lang/String;
        10: aload_0
        11: ldc2_w        #4                  // double 100.0d
        14: putfield      #6                  // Field salary:D
        17: return
      LineNumberTable:
        line 3: 0
        line 5: 4
        line 9: 10

  public void say();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #8                  // String person say...
         5: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 19: 0
        line 21: 8

  public static int calc(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_3 
   		//将int类型常量3压入操作数栈 
   		1: istore_0 //将int类型值存入局部变量0 
   		2: iload_0 //从局部变量0中装载int类型值 
   		3: iload_1 //从局部变量1中装载int类型值 
   		4: iadd //执行int类型的加法 
   		5: istore_2 //将int类型值存入局部变量2 
   		6: iload_2 //从局部变量2中装载int类型值 
   		7: ireturn //从方法中返回int类型的数据
      LineNumberTable:
        line 23: 0
        line 24: 2
        line 25: 6
        line 26: 10

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: iconst_2
         5: invokestatic  #11                 // Method calc:(II)I
         8: invokevirtual #12                 // Method java/io/PrintStream.println:(I)V
        11: return

重新认知类加载机制

装载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

值得探讨的两个方向:(1)类的装载方式有哪些?(2)类装载到底做了什么?

  1. 类的装载方式有哪些?
    (1)本地系统加载
    (2)网络下载.class文件
    (3)从zip,jar等归档文件中加载.class文件
    (4)从数据库中提取.class文件
    (5)由java源文件动态编译成.class文件
    (6)Class.forName()加载
    (7)ClassLoader.loadClass()加载

  2. 类装载到底做了什么?
    (1)通过一个类的全限定名获取定义此类的二进制字节流
    这个阶段是可控性比较强的阶段,既可以用系统提供的类加载器进行加载,又可以自定义类加载器进行加
    载。
    (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

类信息:类的版本、字段、方法、构造方法、接口定义等

(3)类加载的最终产品是位于堆区中的Class对象。

Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
Java对象实例以及数组都在堆上分配

public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement {
     

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第13张图片

链接

验证

保证被加载类的正确性

  • 文件格式验证
    验证字节流是否符合Class文件格式规范,比如是否以0xCAFEBABE开头,主次版本号是否在当前虚拟机的处
    理范围之内,常量池中的常量是否有不被支持的类型。
  • 元数据验证
    对字节码描述的信息进行语义分析,保证其符合Java语言规范的要求。
  • 字节码验证
    通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证
    确保解析动作能正确执行。

验证阶段很重要,但不是必须的。若所引用的类经过反复验证没问题,可以使用-Xverifynone参数关闭大部
分类验证措施,从而缩短虚拟机类加载的时间。

准备

为类的静态变量分配内存,并将其初始化为默认值
在方法区中,为类变量分配内容并设置初始值

  • 内存分配仅仅是类变量,也就是static类型的变量。不包含实例变量,实例变量会在对象实例化时随对
    象分配在堆中。
  • 这里的默认值是根据类型赋值,不是在代码中显示赋予的值。
    性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第14张图片
解析

把类中的符号引用转换为直接引用

Run-Time Constant Pool:Class文件中除了有类的版本、字段、方法、接口等描述 信息外,还有一项信息就是常量池,用于存放编译时 期生成的各种字面量和符号引用,这部分内容将在类加载后进 入方法区的运行时常量池中存放。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。 符号引用就是一组符号来描述目标,可以是任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第15张图片

初始化

执行类构造器,为类的静态变量赋予正确的初始值,有两种方式

  • 直接给类变量指定初始值
  • 通过静态代码块为类变量指定初始值

类的初始化步骤
(1)如果这个类还没有被加载和链接,那先进行加载和链接
(2)假如这个类存在直接父类,并且这个类还没有被初始化(在一个类加载器中,类只能初始化一次),那就初始化直接的 父类(不适用于接口) (3)假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。

类什么时候才会被初始化?
(1)创建类的实例
(2)访问某个类或接口的静态变量,或者对该静态变量进行赋值
(3)调用类的静态方法
(4)反射[Class.forName(“com.XXX”)]
(5)初始化一个类的子类(因为会先初始化父类)
(6)JVM启动时表明的启动类

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第16张图片

堆内存溢出

测试用例

@RestController
public class HeapController {
     

    List<Person> list=new ArrayList<Person>();

    /**
     *  -Xmx32M -Xms32M
     * @return
     */
    @GetMapping("/heap")
    public String heap(){
     
        int i = 0;
        while(true){
     
            list.add(new Person(i++, UUID.randomUUID().toString()));
        }
    }
}

VM参数配置:

 -Xmx32M -Xms32M

运行结果

访问->http://localhost:8080/heap

得到OOM报错:

Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded

优化分析

  • jps与jinfo
    性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第17张图片
  • jmap:手动导出和参数自动导出
# jmap手动导
jmap -dump:format=b,file=heap.hprof PID

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第18张图片
性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第19张图片

方法区内存溢出

测试用例

比如向方法区中添加Class的信息

<dependency> 
	<groupId>asmgroupId>
	<artifactId>asmartifactId>
	<version>3.3.1version>
dependency>
@RestController
public class NonHeapController {
     
    List<Class<?>> list=new ArrayList<Class<?>>();
    @GetMapping("/nonheap")
    public String nonheap(){
     
        while(true){
     
            list.addAll(MetaspaceUtil.createClasses());
        }
    }
}
public class MetaspaceUtil extends ClassLoader {
     

    public static List<Class<?>> createClasses() {
     
        List<Class<?>> classes = new ArrayList<Class<?>>();
        for (int i = 0; i < 10000000; ++i) {
     
            ClassWriter cw = new ClassWriter(0);
            cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
                    "java/lang/Object", null);
            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "",
                    "()V", null, null);
            mw.visitVarInsn(Opcodes.ALOAD, 0);
            mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
                    "", "()V");
            mw.visitInsn(Opcodes.RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();
            MetaspaceUtil test = new MetaspaceUtil();
            byte[] code = cw.toByteArray();
            Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
            classes.add(exampleClass);
        }
        return classes;
    }
}

VM参数配置:

# 设置Metaspace的大小
-XX:MetaspaceSize=50M 
-XX:MaxMetaspaceSize=50M

运行结果

访问->http://localhost:8080/nonheap

打印如下:

java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191] at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]

虚拟机栈

测试用例

public class StackDemo {
     

    public static long count=0;

    public static void method(long i){
     
        System.out.println(count++);
        method(i);
    }

    public static void main(String[] args) {
     
        method(1);
    }
}

运行结果

7252
7253
7254
7255
Exception in thread "main" java.lang.StackOverflowError
	at java.lang.Long.toString(Long.java:396)
	at java.lang.String.valueOf(String.java:3113)

说明

Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。 -Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆 栈大小为1M,以前每个线程堆栈大小为256K。根据应用的 线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还 是有限制的,不能无限生成,经验值在3000~5000左右。 线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性 更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

线程死锁

测试用例

//运行主类
public class DeadLockDemo {
     

    public static void main(String[] args) {
     
        DeadLock d1=new DeadLock(true);
        DeadLock d2=new DeadLock(false);
        Thread t1=new Thread(d1);
        Thread t2=new Thread(d2);
        t1.start();
        t2.start();
    }
}

//定义锁对象
class MyLock{
     
    public static Object obj1= new Object();
    public static Object obj2= new Object();
}

//死锁代码
class DeadLock implements Runnable{
     

    private boolean flag;

    DeadLock(boolean flag){
     
        this.flag=flag;
    }

    public void run() {
     
        if(flag) {
     
            while(true) {
     
                synchronized(MyLock.obj1) {
     
                    System.out.println(Thread.currentThread().getName()+"----if获得obj1锁");
                    synchronized(MyLock.obj2) {
     
                        System.out.println(Thread.currentThread().getName()+"----if获得obj2锁");
                    }
                }
            }
        } else {
     
            while(true){
     
                synchronized(MyLock.obj2) {
     
                    System.out.println(Thread.currentThread().getName()+"----否则获得obj2锁");
                    synchronized(MyLock.obj1) {
     
                        System.out.println(Thread.currentThread().getName()+"----否则获得obj1锁");
                    }
                }
            }
        }
    }
}

运行结果

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第20张图片

优化分析

  • jstack分析
    性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第21张图片
    把打印信息拉到最后可以发现
    性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第22张图片
  • jvisualvm分析
    性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第23张图片

将线程信息dump出来

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第24张图片

垃圾回收

内存被使用了之后,难免会有不够用或者达到设定值的时候,就需要对内存空间进行垃圾回收。

垃圾收集发生的时机

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。 当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无 法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。 但是不建议手动调用该方法,因为消耗的资源比较大。

虽然垃圾回收的时机是不确定的,但是可以结合之前一个对象的一辈子案例,文字图解再次梳理一下堆内存
回收的流程。

一个对象的一辈子
我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长 时间。 有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了, 有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该 去社会上闯闯了。 于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了 20年(每次GC加一岁),然后被回收。

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第25张图片

实验环境准备

我的本地机器使用的是jdk1.8和tomcat8.5

GC日志文件

回顾升华一下垃圾收集器图

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第26张图片

要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,之前也看过这些参数。

-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDateStamps 
-Xloggc:$CATALINA_HOME/logs/gc.log

比如打开windows中的catalina.bat,在第一行加上

-Xms300M -Xmx300M
set JAVA_OPTS=%JAVA_OPTS% -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps - Xloggc:gc.log

这样使用startup.bat启动tomcat的时候就能够在当前目录下拿到gc.log文件

可以看到默认使用的是ParallelGC

Parallel GC日志

吞吐量优先

T23:21:53.305+0800: 1.303: [GC (Allocation Failure) [PSYoungGen: 65536K[Young区回 收前]->10748KYoung区回收后] 65536K[整个堆回收前]->15039K[整个堆回收后] (251392K[整个堆总大小]), 0.0113277 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

注意 如果回收的差值中间有出入,说明这部分空间是Old区释放出来的
性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第27张图片

CMS 日志

停顿时间优先
VM参数配置:

-XX:+UseConcMarkSweepGC

重启tomcat获取gc日志,这里的日志格式和上面差不多,不作分析。

G1 日志

停顿时间优先

-XX:+UseG1GC

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第28张图片

GC日志文件分析工具

gceasy

可以比较不同的垃圾收集器的吞吐量和停顿时间
性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第29张图片

GCViewer

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第30张图片

G1调优

是否选用G1垃圾收集器的判断依据
(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长

(1)使用G1GC垃圾收集器: -XX:+UseG1GC
修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count 99.16% 0.00016s 0.0137s 0.00559s 12
(2)调整内存大小再获取gc日志分析

-XX:MetaspaceSize=100M -Xms300M -Xmx300M

比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间
在这里插入图片描述

(3)调整最大停顿时间

-XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标

比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间
在这里插入图片描述
(4)启动并发GC时堆内存占用百分比

-XX:InitiatingHeapOccupancyPercent=45 
# G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的 使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).

比如设置该百分比参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

性能优化专题 - JVM 性能优化 - 06 - Linux性能监控与调优_第31张图片

G1调优的最佳实践

(1)不要手动设置年轻代的大小-Xmn,不然G1的默认行为会被干扰

  • G1在垃圾收集时将不再关心暂停时间指标。 所以从本质上说,设置年轻代的大小将禁用暂停时间目标.

  • G1在必要时也不能够增加或者缩小年轻代的空间, 因为大小是固定的,所以对更改大小无能为力.

    (2)暂停时间不要使用平均响应时间

暂停时间只是一个目标,并不能总是得到满足。

(3)增加对内存大小

(4)使用-XX:ConcGCThreads=n来增加标记线程的数量

(5)其他参数设置

-XX:InitiatingHeapOccupancyPercent

-XX:G1MixedGCLiveThresholdPercent	-XX:G1HeapWastePercent

-XX:G1MixedGCCountTarget	-XX:G1OldGCSetRegionThresholdPercent

写在最后

本节代码下载地址为:https://github.com/harrypottry/jvmDemo

更多架构知识,欢迎关注本套系列文章:Java架构师成长之路

你可能感兴趣的:(性能优化专题,jvm,jc,性能优化,linux,反编译)