admin管理员组

文章数量:1664854

Java 虚拟机[JVM]原理介绍

  • 1、概述
  • 2、Java类的加载原理机制
    • 2.1 、Java类的加载过程
    • 2.2 、Class loader (类加载器)
      • 2.2.1 类的生命周期
        • 2.2.1.1 加载
        • 2.2.1.2 连接
        • 2.2.1.3 初始化
        • 2.2.1.4 使用
        • 2.2.1.5 卸载
        • 2.2.1.6 结束生命周期
      • 2.2.2 类加载器
      • 2.2.2 JVM类加载机制
      • 2.2.3 双亲委派机制
    • 2.3 、Runtime Data Area (运行时数据区)
    • 2.3 、Execution Engine (执行引擎)
    • 2.3 、Native Interface (本地接口)
    • 2.4 、Native Libraies (本地方法库)
  • 3、内存模型(Runtime Data Area <运行时数据区> 结构)
    • 3.1 程序计数器
    • 3.2 Java 虚拟机栈
    • 3.3 本地方法栈
    • 3.4 Java 堆
    • 3.5 方法区(元空间/永久代)
    • 3.6 运行时常量池
    • 3.7 直接内存
  • 4、垃圾回收机制 (GC)
    • 4.1. 概述
      • 4.1.1 什么时候进行垃圾回收
      • 4.1.2 如何判断对象已死(或能够被回收)`
        • 4.1.2.1 引用计数法
          • 4.1.2.1.1 算法分析
          • 4.1.2.1.2 优缺点
        • 4.1.2.2 可达性分析算法(根搜索算法)
          • 4.1.2.2.1 算法分析
        • 4.1.2.3 Java中的引用
    • 4.2.垃圾回收算法的种类
      • 4.2.1 按照方法分类:
        • 4.2.1.1 标记-清理算法
        • 4.2.1.2 复制算法
        • 4.2.1.3 标记-整理算法
      • 4.2.2 按照回收策略分类
        • 4.2.2.1 分代收集算法
      • 4.3 常见的垃圾收集器
      • 4.3.1 Serial收集器(复制算法)
          • 4.3.1.1 特点
          • 4.3.1.2 应用场景
          • 4.3.1.3 参数
        • 4.3.2 ParNew收集器(标记-复制算法)
          • 4.3.2.1 特点
          • 4.3.2.2 应用场景
          • 4.3.2.3 参数
          • 4.3.2.4 为什么只有ParNew能与CMS收集器配合
        • 4.3.3 Parallel Scavenge收集器(标记-复制算法)
          • 4.3.3.1 特点
          • 4.3.3.2 应用场景
          • 4.3.3.3 参数
          • 4.3.3.4 Parallel Scavenge收集器 VS CMS等收集器:
          • 4.3.3.5 Parallel Scavenge收集器 VS ParNew收集器:
        • 4.3.4 Serial Old收集器(标记-整理算法)
          • 4.3.4.1 特性
          • 4.3.4.2 应用场景
        • 4.3.5 Parallel Old收集器(标记-复制算法)
          • 4.3.5.1 特点
          • 4.3.5.2 使用场景
          • 4.3.5.3 参数
        • 4.3.6 CMS(Concurrent Mark Sweep)收集器
          • 4.3.6.1 特点
          • 4.3.6.2 应用场景
          • 4.3.6.3 设置参数
          • 4.3.6.4 CMS收集器运作过程
            • 4.3.6.4.1 初始标记(initial mark)
            • 4.3.6.4.2 并发标记(concurrent mark)
            • 4.3.6.4.3 重新标记(remark)
            • 4.3.6.4.4 并发清除(concurrent sweep)
          • 4.3.6.5 缺点
            • 4.3.6.5.1 对CPU资源非常敏感
            • 4.3.6.5.2 浮动垃圾(Floating Garbage)
            • 4.3.6.5.3 "Concurrent Mode Failure"失败
            • 4.3.6.5.4 产生大量内存碎片
            • 4.3.6.5.5 碎片解决方法:
        • 4.3.7 G1收集器
          • 4.3.7.1 特点
            • 4.3.7.1.1 Region概念
            • 4.3.7.1.2 可并行,可并发
            • 4.3.7.1.3 分代收集,收集范围包括新生代和老年代
            • 4.3.7.1.4 空间整合,不产生碎片
          • 4.3.7.2 应用场景
          • 4.3.7.3 参数
          • 4.3.7.4 为什么G1收集器可以实现可预测的停顿?
          • 4.3.7.5 G1收集器运作过程
            • 4.3.7.5.1 初始标记(Initial Marking)
            • 4.3.7.5.2 并发标记(Concurrent Marking)
          • 4.3.7.5.3 最终标记(Final Marking)
          • 4.3.7.5.4 筛选回收(Live Data Counting and Evacuation)
        • 4.3.8 总结
          • 4.3.8.1 收集器总结
          • 4.3.8.2 参数总结
          • 4.3.8.3 可预测的停顿:低停顿的同时实现高吞吐量
        • 4.4 GC什么时候触发的
  • 5. jvm 调优
    • 5.1性能监控工具概述
    • 5.2 工具
      • 5.2.1 jps:虚拟机进程状况工具
      • 5.2.2 jstack:堆栈跟踪工具
      • 5.2.3 jstat:虚拟机统计信息监控工具
      • 5.2.4 jinfo 实时地查看和调整虚拟机各项参数
      • 5.2.5 jmap 生成虚拟机的内存转储快照(heapdump文件)
      • 5.2.6 jcmd
      • 5.2.7 JConsole:JMX的可视化管理工具
      • 5.2.8 VisualVM:多合一故障管理工具
      • 5.2.9 JMC (Java Mission Control)
    • 5.2 JVM 参数设置
      • 5.2.1 堆大小设置
        • 5.2.1.1 典型设置:`
      • 5.2.2 回收器选择器
        • 5.2.2.1 典型设置:
      • 5.2.3 响应时间优先 的并发收集器
        • 5.2.3.1 典型设置:
      • 5.2.4 辅助信息
      • 5.2.6 常见配置汇总
        • 5.2.6.1 常用参数设置
        • 5.2.6.2 收集器设置
        • 5.2.6.3 垃圾回收统计信息
        • 5.2.6.3 并行收集器设置
        • 5.2.6.3并发收集器设置

1、概述

JVM 即 Java Virtual Machine(Java虚拟机)的缩写,JVM 本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。Java虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。

2、Java类的加载原理机制

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

2.1 、Java类的加载过程

如图所示
首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),
然后由JVM中的类加载器加载各个类的字节码文件,
加载完毕之后,交由JVM执行引擎执行。
Java内存模型指的就是Runtime Data Area(运行时数据区),即程序执行期间用到的数据和相关信息保存区。

2.2 、Class loader (类加载器)

2.2.1 类的生命周期

类加载的过程包括了加载、连接、初始化三个阶段。连接又分为验证、准备、解析,在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

2.2.1.1 加载

查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

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

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个 java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

2.2.1.2 连接

验证:确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

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

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备:为类的 静态变量 分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  2. .这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
  3. 如果类字段的字段属性表中存在 ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

假设一个类变量的定义为: public static int value = 3;

那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的 public static 指令是在程序编译后,存放于类构造器 <clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

这里还需要注意如下几点:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

假设上面的类变量value被定义为: public static final int value = 3;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据 ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

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

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

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

2.2.1.3 初始化

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • ①声明类变量是指定初始值
  • ②使用静态代码块为类变量指定初始值

JVM初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName("com.hobart.Test")
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类
2.2.1.4 使用

主动引用:

  • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
  • 通过反射方式执行以上三种行为。
  • 初始化子类的时候,会触发父类的初始化。
  • 作为程序入口直接运行时(也就是直接调用main方法)。
    被动引用:
  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
  • 定义类数组,不会引起类的初始化。
  • 引用类的常量,不会引起类的初始化。
2.2.1.5 卸载

从内存中释放类,我们在垃圾回收机制(GC)讨论,方法区内存回收中对类的回收条件如下:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2.2.1.6 结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  • 执行了 System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

2.2.2 类加载器

类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。

Bootstrap ClassLoader
    -- Extension ClassLoader
        -- System ClassLoader
            -- User-Defined ClassLoader

1)根类加载器(bootstrap class loader): 这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME$/jre/lib/rt.jar(Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME$/jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。Bootstrap类加载器是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果你调用String.class.getClassLoader(),会返回null,任何基于此的代码会抛出NullPointerException异常。Bootstrap加载器被称为初始类加载器。

2)扩展类加载器(extensions class loader): 它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。Extensions将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader实现。

3)系统类加载器(system class loader): 被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath-cp选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。通过sun.misc.Launcher$AppClassLoader实现。

3) 用户自定义类加载器 (user custom class loader) User Custom ClassLoaderr是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录

2.2.2 JVM类加载机制

全盘负责: 当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
父类委托: 先让父类加载器驶入加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
缓存机制: 保证所有加载过的类都会被缓存,当需要使用某个类时,先从缓存区寻找该Class,只有缓存区不存在该类时,才会去加载此类。

2.2.3 双亲委派机制

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

意义:系统类防止内存出出现多分同样的字节码;保证Java程序安全稳定运行

(1)当AppClassLoader去加载一个class时,它首先不会自己去加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
(2)当ExtClassLoader去加载一个class时,它首先也不会自己去加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
(3)如果BootStrapClassLoader加载失败,会使用ExtClassLoader来尝试加载。
(4)如果ExtClassLoader加载失败,会使用AppClassLoader来加载
(5)如果AppClassLoader也加载失败,则会爆出异常ClassNotFoundException

2.3 、Runtime Data Area (运行时数据区)

运行数据区是整个JVM 的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java 生态系统如此的繁荣,得益于该区域的优良自治。

整个JVM 框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行,瞧,一个完整的系统诞生了!

2.3 、Execution Engine (执行引擎)

执行引擎也叫做解释器(Interpreter) ,负责解释命令,提交操作系统执行。

方法调用会导致栈帧的入栈,会确定调用哪一个方法。
(1)栈帧。程序的执行对应着栈帧的入栈和出栈,栈帧主要包括:局部变量表、操作数栈、动态连接、方法返回地址等。

(2)方法调用。

解析调用: 类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析的前提是方法在程序真正运行之前就有一个可确定的调用版本。编译期可确定调用方法的版本:静态方法、私有方法、实例构造器、父类方法。
分派调用:
a. 静态分派:发生在编译阶段。所有依赖于静态类型来定位方法执行版本的分派动作成为静态分派,典型方法是重载。javac编译器根据参数的静态类型决定使用哪个重载版本。
b. 动态分派:运行期根据实际类型确定方法执行版本。与方法重写有密切关系。
c. 单分派和多分派:单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

(3)执行引擎需将字节码转换成可以直接被JVM执行的语言,可通过以下两种方式转换:

a. 解释器:一条一条的读取,解释并且执行字节码指令
b. 即时编译器:执行引擎首先按照解释执行的方式来执行,在合适的时候,即时编译器把整段字节码编译成本地代码。内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。

2.3 、Native Interface (本地接口)

本地接口的作用是融合不同的编程语言为Java 所用,它的初衷是融合C/C++ 程序,Java 诞生的时候是C/C++ 横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++ 程序,于是就在内存中专门开辟了一块区域处理标记为native 的代码,它的具体做法是Native Method Stack 中登记native 方法,在Execution Engine 执行时加载Native Libraies 。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java 程序驱动打印机,或者Java 系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket 通信,也可以使用Web Service 等等,不多做介绍。

2.4 、Native Libraies (本地方法库)

  • 与Java环境外交互:
    有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。 你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。

  • 与操作系统交互:
    JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。 还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

  • Sun’ s Java
    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。 jre大部分是用Java实现的,它也通过一些本地方法与外界交互。 例如:类java.lang. Thread的setPriority() 方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用c实现的,并被植入JVM内部,在windows 95 的平台上,这个本地方法最终将调用win32 SetPriority() API。 这是一个本地方
    法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库,
    (external dynamic link library) 提供,然后被JVM调用。

3、内存模型(Runtime Data Area <运行时数据区> 结构)

3.1 程序计数器

程序计数器是一个记录着当前线程所执行的字节码的行号指示器

JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。

从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。

首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。

程序计数器的特点

  1. 线程隔离性,每个线程工作时都有属于自己的独立计数器。
  2. 执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址。
  3. 执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
  4. 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
  5. 程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

3.2 Java 虚拟机栈

每个线程有一个私有的栈,随着线程的创建而创建,生命周期和线程一致。它是描述 Java 方法执行的内存模型:栈里面存着的是一种叫“栈帧”的东西,每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。栈的大小可以固定也可以扩展,当栈调用深度大于JVM所允许的范围,会抛出StackOverFlowError的错误,不过这个深度范围不是一个恒定的值。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用。

OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。即:OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。

我们通过下面这段程序可以测试一下StackOverflowError这个结果:

/**
  * 栈溢出测试源码 StackOverFlowError
  * JVM参数:-Xss128k 设置单个线程栈的大小
  * Created by hobart on 2020/4/25.
  */
package com.hobart.jvm.memory;

public class StackErrorMock {
	private static int index = 1;
	 
    public void call(){
        index++;
        call();
    }
 
    public static void main(String[] args) {
        StackErrorMock mock = new StackErrorMock();
        try {
            mock.call();
        }catch (Throwable e){
            System.out.println("Stack deep : "+index);
            e.printStackTrace();
        }
    }
}

运行三次,可以看出每次栈的深度都是不一样的,输出结果如下:



我们在测试一下OutOfMemoryError

下面在测试之前,先特别提示一下,如果想测试栈的OOM异常,记得先保存当前的工作。由于Windows平台的虚拟机中,Java的线程是映射到操作, 系统的内核上的,因此以下代码可能会导致操作系统假死

我的系统就是卡死的

/**
  * 测试栈溢出测试源码OutOfMemoryError
  * Created by hobart on 2020/4/25.
  */
package com.hobart.jvm.memory;

public class MemoryErrorMock {
	private void dontStop() {
        while (true) {
        }
    }

    //通过不断的创建新的线程使Stack内存耗尽
    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(() -> dontStop());
            thread.start();
        }
    }

    public static void main(String[] args) {
        MemoryErrorMock oom = new MemoryErrorMock();
        oom.stackLeakByThread();
    }

}

3.3 本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

3.4 Java 堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

下面我们简单的模拟一个堆内存溢出的情况:

package com.hobart.jvm.memory;

import java.util.ArrayList;
import java.util.List;

/**
  * OutOfMemoryError: Java heap space
  * -Xmx10m
  * Created by hobart on 2020/4/25.
  */
public class HeapOomMock {
	public static void main(String[] args) {
        List<byte[]> list = new ArrayList();
        int i = 0;
        boolean flag = true;
        while (flag){
            try {
                i++;
                list.add(new byte[1024 * 1024]);//每次增加一个1M大小的数组对象
            }catch (Throwable e){
                e.printStackTrace();
                flag = false;
                System.out.println("count="+i);//记录运行的次数
            }
        }
    }
}

运行上述代码,输出结果如下:

jvm中分为堆和方法区,堆又进一步分为新生代和老年代,方法区又称为永久代。

堆中区分的新生代和老年代是为了垃圾回收,新生代中的对象存活期一般不长,而老年代中的对象存活期较长,所以当垃圾回收器回收内存时,新生代中垃圾回收效果较好,会回收大量的内存,而老年代中回收效果较差,内存回收不会太多。

基于以上特性,新生代中一般采用复制算法,因为存活下来的对象是少数,所需要复制的对象少,而老年代对象存活多,不适合采用复制算法,一般是标记整理和标记清除算法。

因为复制算法需要留出一块单独的内存空间来以备垃圾回收时复制对象使用,所以将新生代分为eden区和两个survivor区,每次使用eden和一个survivor区,另一个survivor作为备用的对象复制内存区。

一个JVM实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:

Young Generation Space 新生代

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。

Tenure Generation space老年代

老年代用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。 三个区的示意图如下:

Permanent Generation space 永久存储区

绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟 “PermGen space”的内存溢出:

package com.hobart.jvm.memory;

import java.util.HashSet;
import java.util.Set;
/**
  * OutOfMemoryError:  PermGen space
  *  -XX:PermSize=2m -XX:MaxPermSize=2m 
  * Created by hobart on 2020/4/25.
  */
public class PermGenOomMock {
	public static void main(String[] args) {
        // 使用Set保持着常量池引用,避免Full GC 回收常量池的行为
		Set<String> set = new HashSet<String>();
		// 在short范围内足以让2M的PermSize产生OOM了
		short i = 0;
		while(true) {
			set.add(String.valueOf(i++).intern());
		}
    }
}

运行结果如下:

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

3.5 方法区(元空间/永久代)

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

永久代(Permanent Generation)
永久代是 HotSpot 虚拟机在 JDK7 及以前对方法区的具体实现,而永久代也在 heap 中(但注意,其它虚拟机并没有永久代,这是 HotSpot 虚拟机特有的)。

在 JDK1.7 中,移除永久代的工作就已经开始了,原先存储在永久代的部分数据转移到了Java heap 或者 native memory,但永久代仍存在于 JDK1.7 中,并没完全移除。移除工作主要包括下面三点:

  • 符号引用(Symbolic Reference)转移到了native memory
  • Interned Strings转移到了heap
  • 静态变量从instanceKlass对象(PermGen内)末尾转移到了java.lang.Class对象(heap内)的末尾

元空间(metaspace)
从 JDK 8 开始的 HotSpot 虚拟机完全移除了 PermGen,改为在 native memory 里存放这些元数据。新的用于存放元数据的内存空间叫做 Metaspace。

取消永久代主要有以下几点原因:

  • 字符串存在永久代中,容易出现性能问题和内存溢出
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
  • 便于将 HotSpot 与 JRockit 合二为一(JRockit 中并没有永久代)

在 Java 8 中抛出堆溢出的示例:

package com.hobart.jvm.memory;

import java.util.ArrayList;
import java.util.List;
/**
  * OutOfMemoryError:  PermGen space
  *  -XX:PermSize=2m -XX:MaxPermSize=2m 
  * Created by hobart on 2020/4/25.
  */
public class StringHeapError {
	
	public static void main(String[] args) {
		StringHeapError mock = new StringHeapError();
        try {
            mock.call();
        }catch (Throwable e){
            e.printStackTrace();
        }
	}

	private void call() {
		String temp = "world";
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String str = temp + temp;
            temp = str;
            str.intern();
        }
	}
	
}


JDK1.6输出结果:

JDK1.7 输出结果:

JDK1.8 输出结果:

3.6 运行时常量池

属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

我们先研究一下String常量

package com.hobart.jvm.memory;

public class Demo {
	public static String a = "JVM原理";
    public static void main(String[] args) {
        String b = "常量池";
    }
}

使用Java自带的反编译工具反编译一下,编译后输入javap -verbose Demo


从结果上可以看到常量池第#11 个常量池项,值为JVM原理;第#24个常量池项,值为常量池

我们在看一个


从以上可以看出

  • 对于直接做+运算的两个字符串(字面量)常量,并不会放入String常量池中,而是直接把运算后的结果放入常量池中
  • 对于先声明的字符串字面量常量,会放入常量池,但是若使用字面量的引用进行运算就不会把运算后的结果放入常量池中了
  • 总结一下就是JVM会对String常量的运算进行优化,未声明的,只放结果;已经声明的,只放声明

3.7 直接内存

非虚拟机运行时数据区的部分

在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

package com.hobart.jvm.memory;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class Test1 {
    static int _100Mb=1024*1024*100;

    public static void main(String[] args) {
        List<ByteBuffer> list=new ArrayList();
        int i=0;
        try {
           while (true){
               ByteBuffer byteBuffer=ByteBuffer.allocateDirect(_100Mb);
               list.add(byteBuffer);
               i++;
           }
        }finally {
            System.out.println(i);
        }
    }
}

4、垃圾回收机制 (GC)

4.1. 概述

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

4.1.1 什么时候进行垃圾回收

①会在cpu空闲的时候自动进行回收

②在堆内存存储满了之后

③主动调用System.gc()后尝试进行回收

补充:System.gc()用于调用垃圾收集器,在调用时,垃圾收集器将运行以回收未使用的内存空间。它将尝试释放被丢弃对象占用的内存。 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。  所以System.gc()并不能说是完美主动进了垃圾回收。

4.1.2 如何判断对象已死(或能够被回收)`

4.1.2.1 引用计数法
4.1.2.1.1 算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。 但是这种简单的算法在当前的jvm中并没有采用,原因是他并不能解决对象之间循环引用的问题。 假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种情况下由于他们的相互引用,从而垃圾回收机制无法识别。

4.1.2.1.2 优缺点

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

4.1.2.2 可达性分析算法(根搜索算法)
4.1.2.2.1 算法分析

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

在Java语言中,可作为GC Roots的对象包括下面几种:

a) 虚拟机栈中引用的对象(栈帧中的本地变量表);

b) 方法区中类静态属性引用的对象;

c) 方法区中常量引用的对象;

d) 本地方法栈中JNI(Native方法)引用的对象。

4.1.2.3 Java中的引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。

强引用在程序代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用 也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。

不要被概念吓到,也别担心,还没跑题,再深入,可就不好说了。小编罗列这四个概念的目的是为了说明,无论引用计数算法还是可达性分析算法都是基于强引用而言的。

4.2.垃圾回收算法的种类

4.2.1 按照方法分类:

4.2.1.1 标记-清理算法

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

分为两个步骤:
第一就是标记,也就是标记所有的需要回收的对象;
第二就是清理,标记完成后进行统一的回收带有标记的对象占据的内存空间。

缺点是效率问题,还有一个致命的缺点就是空间问题,标记清除之后会产生大量不连续的内存碎片,当程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而造成内存空间浪费。

回收前:

回收后:

特点:

  • 简单方便
  • 容易产生内存碎片
4.2.1.2 复制算法

复制算法是将内存容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完之后,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次性清理。这样使得每次都对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只是这种算法的代价就是将内存缩小为原来的一半了。

回收前:

回收后:

特点:

  • 简单
  • 不会产生碎片
  • 内存利用率太低,只用了一半
4.2.1.3 标记-整理算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

分为两个步骤:

第一步(标记):利用可达性遍历内存,把“存活”对象和“垃圾”对象进行标记。

第二步(整理):把所有存活对象堆到同一个地方,这样就没有内存碎片了。

回收前:

回收后:

特点:

  • 适合存活对象多,垃圾少的情况
  • 需要整理的过程

4.2.2 按照回收策略分类

4.2.2.1 分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

年轻代(Young Generation)的回收算法

  • 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  • 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(from survivor,to survivor)区。大部分对象在eden区中生成,回收时先将eden区存活对象复制到一个from survivor区,然后清空eden区。当这个from survivor区也存放满了时,则将eden区和from survivor区存活对象复制到另一个to survivor区,然后清空eden和这个from survivor区,此时from survivor区是空的然后将to survivor区和to survivor区交换,即保持to survivor区为空, 如此往复。
  • to survivor区不足以存放 edenfrom survivor的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
  • 新生代发生的GC也叫做Minor GCMinorGC发生频率比较高(不一定等eden区满了才触发)。

年老代(Old Generation)的回收算法

  • 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  • 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GCFull GCFull GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)的回收算法

-用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。

4.3 常见的垃圾收集器

下面一张图是HotSpot虚拟机包含的所有收集器。


图中展示了7种不同分代的收集器:

Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;

两个收集器间有连线,表明它们可以搭配使用:

Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

4.3.1 Serial收集器(复制算法)

Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器,新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式。JDK1.3.1前是HotSpot新生代收集的唯一选择;

Serial/Serial Old收集器的工作过程如下图:

4.3.1.1 特点
  1. 针对新生代
  2. 串行
  3. 复制算法
  4. 单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,
  5. 另一方面也意味着在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束为止,这个过程也称为 Stop The world。
  6. 后者意味着,在用户不可见的情况下要把用户正常工作的线程全部停掉,这显然对很多应用是难以接受的。
4.3.1.2 应用场景

对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。 在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的

Stop the World是在用户不可见的情况下执行的,会造成某些应用响应变慢;
因为新生代的特点是对象存活率低,所以收集算法用的是复制算法,把新生代存活对象复制到老年代,复制的内容不多,性能较好
单线程地好处就是减少上下文切换,减少系统资源的开销。但这种方式的缺点也很明显,在GC的过程中,会暂停程序的执行。若GC不是频繁发生,这或许是一个不错的选择,否则将会影响程序的执行性能。 对于新生代来说,区域比较小,停顿时间短,所以比较使用。

4.3.1.3 参数

-XX:+UseSerialGC:串联收集器

在JDK Client模式,不指定VM参数,默认是串行垃圾回收器

4.3.2 ParNew收集器(标记-复制算法)

ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。

ParNew/Serial Old收集器的工作过程如下图:

ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器,具体内容将在稍后进行介绍。

ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。

4.3.2.1 特点

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括Serial收集器可用的所有控制参数、收集算法、Stop The world、对象分配规则、回收策略等都一样。在实现上也共用了相当多的代码。

  1. 针对新生代
  2. 复制算法
  3. 串行
  4. 多线程
  5. GC时需要暂停所有用户线程,直到GC结束
  6. Serial多线程版本,其他特点与Serial相同
4.3.2.2 应用场景

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。很重要的原因是:除了Serial收集器之外,目前只有它能与CMS收集器配合工作(看图)。在JDK1.5时期,HotSpot推出了一款几乎可以认为具有划时代意义的垃圾收集器-----CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

4.3.2.3 参数

-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器;
-XX:+UseParNewGC:强制指定使用ParNew;
-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

4.3.2.4 为什么只有ParNew能与CMS收集器配合

CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;

因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

4.3.3 Parallel Scavenge收集器(标记-复制算法)

Parallel Scavenge收集器和ParNew类似,新生代的收集器,同样用的是复制算法,也是并行多线程收集。与ParNew最大的不同,它关注的是垃圾回收的吞吐量,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

4.3.3.1 特点

针对新生代
复制算法
并行
多线程
高吞吐量为目标

4.3.3.2 应用场景

Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。

高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;适合那种交互少、运算多的场景

例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;

4.3.3.3 参数

-XX:+MaxGCPauseMillis:控制最大垃圾收集停顿时间,大于0的毫秒数;这个参数设置的越小,停顿时间可能会缩短,但也会导致吞吐量下降,导致垃圾收集发生得更频繁。
-XX:GCTimeRatio:设置垃圾收集时间占总时间的比率,0<n<100的整数,就相当于设置吞吐量的大小。

先垃圾收集执行时间占应用程序执行时间的比例的计算方法是:
1 / (1 + n)
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%=1/(1+19);
默认值是1%–1/(1+99),即n=99;
垃圾收集所花费的时间是年轻一代和老年代收集的总时间;

此外,还有一个值得关注的参数:

-XX:+UseAdptiveSizePolicy 开启这个参数后,就不用手工指定一些细节参数,如:

新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;

JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);

另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。

4.3.3.4 Parallel Scavenge收集器 VS CMS等收集器:

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。

4.3.3.5 Parallel Scavenge收集器 VS ParNew收集器:

Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。

4.3.4 Serial Old收集器(标记-整理算法)

Serial 收集器在新生代和老年代都有对应的版本,Serial Old是Serial收集器的老年代版本,除了收集算法不同,两个版本并没有其他差异。

Serial 新生代收集器采用的是复制算法。
Serial Old 老年代采用的是标记 - 整理算法。

Serial/Serial Old收集器的工作过程如下图:

4.3.4.1 特性
  1. Serial Old是Serial的老年代版本,
  2. 除了采用标记-整理算法(还有压缩,Mark-Sweep-Compact),其他与Serial相同
  3. 单线程收集;
4.3.4.2 应用场景

Client模式 Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
Server模式如果在Server模式下,那么它主要还有两大用途:

一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;
另一种用途就是作为CMS收集器的后备预案,在并发收集发生"Concurrent Mode Failure"时使用。

4.3.5 Parallel Old收集器(标记-复制算法)

Parallel Old是 Parallel Scavenge收集器的老年代版本,除了收集算法不同,两个版本并没有其他差异,并行收集器,吞吐量优先。它也使用多线程和和“标记-整理”算法,高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。

4.3.5.1 特点
  1. Parallel Old是Parallel Scavenge的老年代版本
  2. Parallel Old 老年代采用的是标记 - 整理算法,其他特点与Parallel Scavenge相同
4.3.5.2 使用场景
  1. 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器组合。
  2. JDK1.6及之后用来代替老年代的Serial Old收集器;
  3. 特别是在Server模式,多CPU的情况下;
4.3.5.3 参数

-XX:+UseParallelOldGC:指定使用Parallel Old收集器;

4.3.6 CMS(Concurrent Mark Sweep)收集器

CMS是HotSpot在JDK5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;命名中用的是concurrent,而不是parallel,说明这个收集器是有与工作执行并发的能力的。MS则说明算法用的是Mark Sweep算法。
它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁GC的场景下,是比较适用的。

4.3.6.1 特点
  1. 针对老年代
  2. 标记-清除算法 (不进行压缩操作,产生内存碎片);
  3. 并发
  4. 多线程
  5. 收集过程中不需要暂停用户线程
  6. 以获取最短回收停顿时间为目标
4.3.6.2 应用场景

与用户交互较多的场景。CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上,这类应用尤其注重服务的响应速度,希望系统停顿时间最短,以给用户带来极好的体验。CMS收集器就非常符合这类应用的需求。
CMS是一种以获取最短回收停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多。

4.3.6.3 设置参数

-XX:+UseConcMarkSweepGC:指定使用CMS收集器
-XX:+UseConcMarkSweepGC:使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection:Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction:设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads:设定CMS的线程数量(一般情况约等于可用CPU数量)

4.3.6.4 CMS收集器运作过程

比前面几种收集器更复杂,可以分为4个步骤:

4.3.6.4.1 初始标记(initial mark)

单线程执行
需要“Stop The World”
但仅仅把GC Roots的直接关联可达的对象给标记一下,由于直接关联对象比较小,所以这里的速度非常快

4.3.6.4.2 并发标记(concurrent mark)

对于初始标记过程所标记的初始标记对象,进行并发追踪标记,
此时其他线程仍可以继续工作。
此处时间较长,但不停顿。
并不能保证可以标记出所有的存活对象;

4.3.6.4.3 重新标记(remark)

在并发标记的过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾。
此处执行并行标记,与用户线程不并发,所以依然是“Stop The World”,
且停顿时间比初始标记稍长,但远比并发标记短。

4.3.6.4.4 并发清除(concurrent sweep)

并发清除之前所标记的垃圾。
其他用户线程仍可以工作,不需要停顿

其中,初始标记和并发标记仍然需要Stop the World、初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。

4.3.6.5 缺点

总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。由于CMS以上特性,缺点也是比较明显的

4.3.6.5.1 对CPU资源非常敏感

对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

4.3.6.5.2 浮动垃圾(Floating Garbage)

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

由于在垃圾收集阶段用户线程还需要运行,那就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,也可以热为CMS所需要的空间比其他垃圾收集器大;

  "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;

  JDK1.5默认值为68%;

  JDK1.6变为大约92%;
4.3.6.5.3 "Concurrent Mode Failure"失败

如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样会导致另一次Full GC的产生。这样停顿时间就更长了,代价会更大,所以 "-XX:CMSInitiatingOccupancyFraction"不能设置得太大。

4.3.6.5.4 产生大量内存碎片

这个问题并不是CMS的问题,而是算法的问题。由于CMS基于"标记-清除"算法,清除后不进行压缩操作,所以会产生碎片

"标记-清除"算法介绍时曾说过:

产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。

4.3.6.5.5 碎片解决方法:

(1)、-XX:+UseCMSCompactAtFullCollection

  使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;

  但合并整理过程无法并发,停顿时间会变长;

  默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);

(2)、-XX:+CMSFullGCsBeforeCompaction

  设置执行多少次不压缩的Full GC后,来一次压缩整理;

  为减少合并整理过程的停顿时间;

  默认为0,也就是说每次都执行Full GC,不会进行压缩整理;

  由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;
4.3.7 G1收集器

G1(Garbage - First)名称的由来是G1跟踪各个Region里面的垃圾堆的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
G1(Garbage-First)是JDK7-u4才推出商用的收集器;

G1与前面的垃圾收集器有很大不同,它把新生代、老年代的划分取消了!

这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。

取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。


在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。

4.3.7.1 特点

G1除了降低停顿外,还能建立可预测的停顿时间模型;

4.3.7.1.1 Region概念
  1. 横跨整个堆内存
  2. 在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。
  3. G1在使用时,Java堆的内存布局与其他收集器有很大区别,
  4. 它将整个Java堆划分为多个大小相等的独立区域(Region),
  5. 虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(可以不连续)的集合。
4.3.7.1.2 可并行,可并发
  1. 能充分利用多CPU、多核环境下的硬件优势;
  2. G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间
  3. 并行:使用多个CPU来缩短Stop-The-World停顿的时间,
  4. 并发:也可以并发让垃圾收集与用户程序同时进行
4.3.7.1.3 分代收集,收集范围包括新生代和老年代
  1. 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
  2. 能够采用不同方式处理不同时期的对象;
4.3.7.1.4 空间整合,不产生碎片
  1. 从整体看,是基于标记-整理算法;
  2. 从局部(两个Region间)看,是基于复制算法;
  3. 都不会产生内存碎片,有利于长时间运行;
  4. 这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
4.3.7.2 应用场景

如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。

1.面向服务端应用,针对具有大内存、多处理器的机器;最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
2.用来替换掉JDK1.5的CMS收集器;
(1)、超过50%的Java堆被活动数据占用;
(2)、对象分配频率或年代提升频率变化很大;
(3)、GC停顿时间过长(长与0.5至1秒)。

4.3.7.3 参数

-XX:+UseG1GC:指定使用G1收集器;
-XX:InitiatingHeapOccupancyPercent:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
-XX:MaxGCPauseMillis:为G1设置暂停时间目标,默认值为200毫秒;
-XX:G1HeapRegionSize:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;

4.3.7.4 为什么G1收集器可以实现可预测的停顿?

G1可以建立可预测的停顿时间模型,是因为:

  1. 可以有计划地避免在Java堆的进行全区域的垃圾收集;
  2. G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
  3. 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);

这就保证了在有限的时间内可以获取尽可能高的收集效率;

4.3.7.5 G1收集器运作过程

不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

4.3.7.5.1 初始标记(Initial Marking)
  1. 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,
  2. 速度很快,
  3. 需要“Stop The World”。(OopMap)
4.3.7.5.2 并发标记(Concurrent Marking)
  1. 进行GC Roots Tracing的过程,从刚才产生的集合中标记出存活对象;(也就是从GC Roots 开始对堆进行可达性分析,找出存活对象。)
  2. 耗时较长,但应用程序也在运行;
  3. 并不能保证可以标记出所有的存活对象;
4.3.7.5.3 最终标记(Final Marking)
  1. 最终标记和CMS的重新标记阶段一样,也是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,
  2. 这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,
  3. 也需要“Stop The World”。(修正Remebered Set)
4.3.7.5.4 筛选回收(Live Data Counting and Evacuation)
  1. 首先排序各个Region的回收价值和成本;
  2. 然后根据用户期望的GC停顿时间来制定回收计划;
  3. 最后按计划回收一些价值高的Region中垃圾对象;
  4. 回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
  5. 可以并发进行,降低停顿时间,并增加吞吐量;
4.3.8 总结


图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。

虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

4.3.8.1 收集器总结
收集器串行、并行or并发新生代/老年代算法目标适用场景
Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式
Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式、CMS的后备预案
ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合
Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务
Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务
CMS并发老年代标记-清除响应速度优先集中在互联网站或B/S系统服务端上的Java应用
G1并发/标记-整理+复制算法响应速度优先面向服务端应用,将来替换CMS
4.3.8.2 参数总结
参数MinorGCFull GC描述
-XX:+UseSerialGCSerial收集器串行回收Serial Old收集器串行回收该选项可以手动指定Serial收集器+Serial Old收集器组合执行内存回收
-XX:+UseParNewGCParNew收集器并行回收Serial Old收集器串行回收该选项可以手动指定ParNew收集器+Serilal Old组合执行内存回收
-XX:+UseParallelGCParallel收集器并行回收Serial Old收集器串行回收该选项可以手动指定Parallel收集器+Serial Old收集器组合执行内存回收
-XX:+UseParallelOldGCParallel收集器并行回收Parallel Old收集器并行回收该选项可以手动指定Parallel收集器+Parallel Old收集器组合执行内存回收
-XX:+UseConcMarkSweepGCParNew收集器并行回收缺省使用CMS收集器并发回收,备用采用Serial Old收集器串行回收该选项可以手动指定ParNew收集器+CMS收集器+Serial Old收集器组合执行内存回收。优先使用ParNew收集器+CMS收集器的组合,当出现ConcurrentMode Fail或者Promotion Failed时,则采用ParNew收集器+Serial Old收集器的组合
-XX:+UseConcMarkSweepGC
-XX:-UseParNewGC
Serial收集器串行回收同上同上
-XX:+UseG1GCG1收集器并发、并行执行内存回收暂无
4.3.8.3 可预测的停顿:低停顿的同时实现高吞吐量

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,
每次根据允许的收集时间,优先回收价值最大的Region,这样就保证了在有限的时间内尽可能提高效率。(这也就是Garbage-First名称的来由)。
这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
应用场景
如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。

1.面向服务端应用,针对具有大内存、多处理器的机器;最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
2.用来替换掉JDK1.5的CMS收集器;
(1)、超过50%的Java堆被活动数据占用;
(2)、对象分配频率或年代提升频率变化很大;
(3)、GC停顿时间过长(长与0.5至1秒)。

4.4 GC什么时候触发的

Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:老年代(Tenured)被写满;持久代(Perm)被写满;System.gc()被显示调用;上一次GC之后Heap的各域分配策略动态变化;

5. jvm 调优

5.1性能监控工具概述

工具主要是为了解决问题而生的,就是由于我们的程序存在着一些性能问题,才有了这些工具。其实当我们在下载完成JDK之后,那些工具就被SUN公司随之送给我们了。

我们可以在我们的JDK安装目录,下看看会有很多这样的工具。


我们会发现很多这样的exe文件,这里面有很多都是性能监控工具。我们就抽出来几个进行讲解。

工具名称主要作用
jps(JVM Process Status Tool)显示指定系统中所有的HotSpot虚拟机进程
jstack(Stack Trace for Java)现实虚拟机的线程快照
jstat(JVM Statistics Monitoring)收集HotSpot虚拟机各方面的运行数据
jinfo(Configuration Info for Java)显示虚拟机配置信息
jmap(Memory Map for Java)生成虚拟机的内存转出快照(heapdump文件)
JConsoleJMX的可视化管理工具

5.2 工具

5.2.1 jps:虚拟机进程状况工具

ps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。

如果在 linux 中想查看 java 的进程,一般我们都需要 ps -ef | grep java 来获取进程 ID。
如果只想获取 Java 程序的进程,可以直接使用 jps 命令来直接查看。

jps主要用来输出JVM中运行的进程状态信息。语法格式如下:
jps [options] [hostid]

第一个参数:选项[options]

-q 仅输出VM标识符,不包括class name,jar name,arguments in main method ,不输出类名、Jar名和传入main方法的参数-m 输出传入main方法的参数-l 输出main类或Jar的全限名-v 输出传入JVM的参数
-m 输出main method的参数
-l 输出完全的包名,应用主类名,jar的完全路径名
-v 输出jvm参数
-V 输出通过flag文件传递到JVM中的参数(.hotspotrc文件或-XX:Flags=所指定的文件
-Joption 传递参数到vm,例如:-J-Xms48m

第二个参数:[hostid]

主机或者是服务器的id,如果不指定,就默认为当前的主机或者是服务器。
[protocol:][[//]hostname][:port][/servername]

输出格式
lvmid [ [ classname | JARfilename | “Unknown”] [ arg* ] [ jvmarg* ] ]

样例

(1)jps 仅显示进程id,主类名, 无参数:显示进程的ID 和 类的名称

jps 不带参数,默认显示 进程ID 和 启动类的名称。

(2)jps -q 仅显示进程id

参数 -q 只输出进程ID,而不显示出类的名称

(3)jps -m
参数 -m 可以输出传递给 Java 进程(main 方法)的参数。

(4)jps -l 输出完全的包名,主类名,jar完全路径名

参数 -l 可以输出主函数的完整路径(类的全路径)。

(5)jps -v 显示jvm参数
参数 -v 可以显示传递给 Java 虚拟机的参数。

获取远程服务器 jps 信息
jps 支持查看远程服务上的 jvm 进程信息。如果需要查看其他机器上的 jvm 进程,需要在待查看机器上启动 jstatd 服务。

(6)jps -lv 127.0.0.1 输出127.0.0.1机器上的java进程,显示jvm参数,显示完全的包名,主类名,jar完全路径

注意:127.0.0.1主机要启动jstatd
开启 jstatd 服务
启动 jstatd 服务,需要有足够的权限。 需要使用 Java 的安全策略分配相应的权限。
创建 jstatd.all.policy 策略文件。

grant codebase "file:${java.home}/../lib/tools.jar" {
   permission java.security.AllPermission;
};

启动 jstatd 服务器

jstatd -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname=127.0.0.1

-J 参数是一个公共的参数,如 jps、 jstat 等命令都可以接收这个参数。 由于 jps、 jstat 命令本身也是 Java 应用程序, -J 参数可以为 jps 等命令本身设置 Java 虚拟机参数。
-Djava.security.policy:指定策略文件
-Djava.rmi.server.hostname:指定服务器的ip地址(可忽略)
默认情况下, jstatd 开启在 1099 端口上开启 RMI 服务器。

jps 原理
java程序在启动以后,会在java.io.tmpdir指定的目录下,就是临时文件夹里,生成一个类似于hsperfdata_User的文件夹,这个文件夹里(在Linux中为/tmp/hsperfdata_{userName}/),有几个文件,名字就是java进程的pid,因此列出当前运行的java进程,只是把这个目录里的文件名列一下而已。 至于系统的参数什么,就可以解析这几个文件获得。

5.2.2 jstack:堆栈跟踪工具

jstack用于生成虚拟机当前时刻的线程快照。它是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息。语法格式如下:

jstack [option] vmid

第一个参数:option

-F 当进程挂起,执行jstack 命令没有任何输出后,将强制转储堆内的线程信息
-m 在混合模式下,打印 java 和 native c/c++ 框架的所有栈信息
-l 长列表。打印关于锁的附加信息,例如属于 java.util.concurrent 的 ownable synchronizers 列表

第二个参数:vmid
vmid是Java虚拟机ID,在Linux/Unix系统上一般就是进程ID。

Jstack 使用
通过使用 jps 命令获取需要监控的进程的pid,然后使用 jstack pid 命令查看线程的堆栈信息

通过 jstack 命令可以获取当前进程的所有线程信息。

每个线程堆中信息中,都可以查看到 线程ID、线程的状态(wait、sleep、running 等状态)、是否持有锁信息等。

死锁示例
下面通过一个例子,来演示 jstack 检查死锁的一个例子,代码如下:

package com.hobart.jvm.memory;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeathLock {
	
	static Lock lock1 = new ReentrantLock();
	static Lock lock2 = new ReentrantLock();

	public static void deathLock() {
	    Thread t1 = new Thread() {
	        @Override
	        public void run() {
	            try {
	                lock1.lock();
	                TimeUnit.SECONDS.sleep(1);
	                lock2.lock();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	    };
	    
	    Thread t2 = new Thread() {
	        @Override
	        public void run() {
	            try {
	                lock2.lock();
	                TimeUnit.SECONDS.sleep(1);
	                lock1.lock();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	    };
	    t1.setName("mythread1");
	    t2.setName("mythread2");
	    t1.start();
	    t2.start();
	}
	
	public static void main(String[] args) {
		DeathLock deathLock = new DeathLock();
		deathLock.deathLock();
	}

}

使用jstack -l <pid>查看线程堆栈信息,发现在堆栈信息最后面检查出了一个死锁。如下图

可以清楚的看出 mythread2 等待 这个锁 “0x000000076b5e07e8”,这个锁是由于mythread1线程持有。

mythread1线程等待这个锁“0x000000076b5e0818”,这个锁是由mythread2线程持有。

“mythread1”线程堆栈信息如下:

可以看出当前线程持有“0x000000076b5e07e8”锁,等待“0x000000076b5e0818”的锁

“mythread2”线程堆栈信息如下:


“mythread2”的堆栈信息中可以看出当前线程持有“0x000000076b5e0818”锁,等待“0x000000076b5e07e8”的锁。

5.2.3 jstat:虚拟机统计信息监控工具

Jstat是JDK自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool”,它位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。

jstat监视虚拟机各种运行状态信息,可以显示本地或者是远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。语法格式如下:

jstat 用法

jstat -<option> [-t] [-h<lines>] <vmid> [<intercal> [<count>]]

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编译的方法信息;

(1)jstat -class 显示加载class的数量,及所占空间等信息。

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

(2)jstat -compiler 显示VM实时编译(JIT)的数量等信息。

  • Compiled:编译任务执行数量
  • Failed:编译任务执行失败数量
  • Invalid :编译任务执行失效数量
  • Time :编译任务消耗时间
  • FailedType:最后一个编译失败任务的类型
  • FailedMethod:最后一个编译失败任务所在的类及方法

(3)jstat -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)

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

  • NGCMN :年轻代(young)中初始化(最小)的大小(字节)
  • NGCMX :年轻代(young)的最大容量 (字节)
  • NGC :年轻代(young)中当前的容量 (字节)
  • S0C :年轻代中第一个survivor(幸存区)的容量 (字节)
  • S1C : 年轻代中第二个survivor(幸存区)的容量 (字节)
  • EC :年轻代中Eden(伊甸园)的容量 (字节)
  • OGCMN :old代中初始化(最小)的大小 (字节)
  • OGCMX :old代的最大容量(字节)
  • OGC:old代当前新生成的容量 (字节)
  • OC :Old代的容量 (字节)
  • MCMN:metaspace(元空间)中初始化(最小)的大小 (字节)
  • MCMX :metaspace(元空间)的最大容量 (字节)
  • MC :metaspace(元空间)当前新生成的容量 (字节)
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:当前压缩类空间大小
  • YGC :从应用程序启动到采样时年轻代中gc次数
  • FGC:从应用程序启动到采样时old代(全gc)gc次数

(5)jstat -gcmetacapacity Metaspace 中对象的信息及其占用量。

  • MCMN: 最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:当前压缩类空间大小
  • YGC :从应用程序启动到采样时年轻代中gc次数
  • FGC :从应用程序启动到采样时old代(全gc)gc次数
  • FGCT :从应用程序启动到采样时old代(全gc)gc所用时间(s)
  • GCT:从应用程序启动到采样时gc用的总时间(s)

(6)jstat -gcnew 年轻代对象的信息。

  • S0C :年轻代中第一个survivor(幸存区)的容量 (字节)
  • S1C :年轻代中第二个survivor(幸存区)的容量 (字节)
  • S0U :年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
  • S1U :年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
  • TT:持有次数限制
  • MTT:最大持有次数限制
  • DSS:期望的幸存区大小
  • EC:年轻代中Eden(伊甸园)的容量 (字节)
  • EU :年轻代中Eden(伊甸园)目前已使用空间 (字节)
  • YGC :从应用程序启动到采样时年轻代中gc次数
  • YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)

(7)jstat -gcnewcapacity 年轻代对象的信息及其占用量

  • NGCMN :年轻代(young)中初始化(最小)的大小(字节)
  • NGCMX :年轻代(young)的最大容量 (字节)
  • NGC :年轻代(young)中当前的容量 (字节)
  • S0CMX :年轻代中第一个survivor(幸存区)的最大容量 (字节)
  • S0C :年轻代中第一个survivor(幸存区)的容量 (字节)
  • S1CMX :年轻代中第二个survivor(幸存区)的最大容量 (字节)
  • S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
  • ECMX:年轻代中Eden(伊甸园)的最大容量 (字节)
  • EC:年轻代中Eden(伊甸园)的容量 (字节)
  • YGC:从应用程序启动到采样时年轻代中gc次数
  • FGC:从应用程序启动到采样时old代(全gc)gc次数

(8)jstat -gcold old代对象的信息

  • MC :metaspace(元空间)的容量 (字节)
  • MU:metaspace(元空间)目前已使用空间 (字节)
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • OC:Old代的容量 (字节)
  • OU:Old代目前已使用空间 (字节)
  • YGC:从应用程序启动到采样时年轻代中gc次数
  • FGC:从应用程序启动到采样时old代(全gc)gc次数
  • FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
  • GCT:从应用程序启动到采样时gc用的总时间(s)

(9)jstat -gcoldcapacity old代对象的信息及其占用量

  • OGCMN :old代中初始化(最小)的大小 (字节)
  • OGCMX :old代的最大容量(字节)
  • OGC :old代当前新生成的容量 (字节)
  • OC :Old代的容量 (字节)
  • YGC :从应用程序启动到采样时年轻代中gc次数
  • FGC :从应用程序启动到采样时old代(全gc)gc次数
  • FGCT :从应用程序启动到采样时old代(全gc)gc所用时间(s)
  • GCT:从应用程序启动到采样时gc用的总时间(s)

(10)jstat - gcutil 统计gc信息

  • S0 :年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
  • S1 :年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
  • E :年轻代中Eden(伊甸园)已使用的占当前容量百分比
  • O :old代已使用的占当前容量百分比
  • P :perm代已使用的占当前容量百分比
  • YGC :从应用程序启动到采样时年轻代中gc次数
  • YGCT :从应用程序启动到采样时年轻代中gc所用时间(s)
  • FGC :从应用程序启动到采样时old代(全gc)gc次数
  • FGCT :从应用程序启动到采样时old代(全gc)gc所用时间(s)
  • GCT:从应用程序启动到采样时gc用的总时间(s)

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

  • LGCC:最后一次GC原因
  • GCC:当前GC原因(No GC 为当前没有执行GC)

(12)jstat -printcompilation 当前VM执行的信息。

  • Compiled :编译任务的数目
  • Size :方法生成的字节码的大小
  • Type:编译类型
  • Method:类名和方法名用来标识编译的方法。类名使用/做为一个命名空间分隔符。方法名是给定类中的方法。上述格式是由-XX:+PrintComplation选项进行设置的

5.2.4 jinfo 实时地查看和调整虚拟机各项参数

jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括Java System属性和JVM命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息

jinfo 用法

jinfo [option] <pid>

参数说明

pid 对应jvm的进程id
executable core 产生core dump文件
[server-id@]remote server IP or hostname 远程的ip或者hostname,server-id标记服务的唯一性id

option

no option 输出全部的参数和系统属性
-flag name 输出对应名称的参数
-flag [+|-]name 开启或者关闭对应名称的参数
-flag name=value 设定对应名称的参数
-flags 输出全部的参数
-sysprops 输出系统属性

Javacore 概述
Javacore,也可以称为“threaddump”或是“javadump”,它是 Java 提供的一种诊断特性,能够提供一份可读的当前运行的 JVM 中线程使用情况的快照。即在某个特定时刻,JVM 中有哪些线程在运行,每个线程执行到哪一个类,哪一个方法。
应用程序如果出现不可恢复的错误或是内存泄露,就会自动触发 Javacore 的生成。

(1) no option
命令:jinfo <pid>
描述:输出当前 jvm 进程的全部参数和系统属性

(2) -flag name
命令:jinfo -flag name <pid>
描述:输出对应名称的参数

使用该命令,可以查看指定的 jvm 参数的值。如:查看当前 jvm 进程是否开启打印 GC 日志。

(3) -flag [+|-]name
命令:jinfo -flag [+|-]name <pid>
描述:开启或者关闭对应名称的参数

使用 jinfo 可以在不重启虚拟机的情况下,可以动态的修改 jvm 的参数。尤其在线上的环境特别有用。

使用如下:

(4) -flag name=value
命令:jinfo -flag name=value <pid>
描述:修改指定参数的值。

同示例三,但示例三主要是针对 boolean 值的参数设置的。
如果是设置 value值,则需要使用 name=value 的形式。

使用如下:

注意事项 : jinfo虽然可以在java程序运行时动态地修改虚拟机参数,但并不是所有的参数都支持动态修改

(5) -flags
命令:jinfo -flags <pid>
描述:输出全部的参数

(6) -sysprops
命令:jinfo -sysprops <pid>
描述:输出当前 jvm 进行的全部的系统属性

5.2.5 jmap 生成虚拟机的内存转储快照(heapdump文件)

jmap 即Java Memory Map(内存映射) 命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。

jmap [option] vmid

option: 选项参数。
pid: 需要打印配置信息的进程ID。
executable: 产生核心dump的Java可执行文件。
core: 需要打印配置信息的核心文件。
server-id 可选的唯一id,如果相同的远程主机上运行了多台调试服务器,用此选项参数标识服务器。
remote server IP or hostname 远程调试服务器的IP地址或主机名。

option
no option: 查看进程的内存映像信息,类似 Solaris pmap 命令。
heap: 显示Java堆详细信息
histo[:live]: 显示堆中对象的统计信息
clstats:打印类加载器信息
finalizerinfo: 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
dump:<dump-options>:生成堆转储快照
F: 当-dump没有响应时,使用-dump或者-histo参数. 在这个模式下,live子参数无效.
help:打印帮助信息
J<flag>:指定传递给运行jmap的JVM的参数

(1) no option
命令:jmap <pid>
描述:查看进程的内存映像信息,类似 Solaris pmap 命令。

使用不带选项参数的jmap打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始地址、映射大小以及共享对象文件的路径全称。这与Solaris的pmap工具比较相似。

(2) heap
命令:jmap -heap <pid>
描述:显示Java堆详细信息

打印一个堆的摘要信息,包括使用的GC算法、堆配置信息和各内存区域内存使用信息

(3) histo[:live]
命令:jmap -histo:live <pid>
描述:显示堆中对象的统计信息

其中包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名。打印的虚拟机内部的类名称将会带有一个’*’前缀。如果指定了live子选项,则只计算活动的对象。

(4) clstats
命令:jmap -clstats <pid>
描述:打印类加载器信息

-clstats是-permstat的替代方案,在JDK8之前,-permstat用来打印类加载器的数据
打印Java堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言,它的名称、活跃度、地址、父类加载器、它所加载的类的数量和大小都会被打印。此外,包含的字符串数量和大小也会被打印。

(5) finalizerinfo
命令:jmap -finalizerinfo pid
描述:打印等待终结的对象信息

Number of objects pending for finalization: 0 说明当前F-QUEUE队列中并没有等待Fializer线程执行final

(6) dump:
命令:jmap -dump:format=b,file=heapdump.phrof pid
描述:生成堆转储快照dump文件。

以hprof二进制格式转储Java堆到指定filename的文件中。live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。想要浏览heap dump,你可以使用jhat(Java堆分析工具)读取生成的文件。

注意 这个命令执行,JVM会将整个heap的信息dump写入到一个文件,heap如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证dump的信息是可靠的,所以会暂停应用, 线上系统慎用。

使用JVM自带的JVisualVM工具查看堆快照文件

在%JAVA_HOME%/bin目录下双击jvisualvm.exe打开–>文件–>装入–>选择Dump文件类型–>找到堆快照文件–>打开


在类栏中找到你要查看的类名

双击进入实例数栏–>查看统计出的实例化的对象数目,分析对象的引用找到是谁在实例化此对象,从而找到产生大对象的原因。

5.2.6 jcmd

在JDK1.7以后,新增了一个命令行工具 jcmd。他是一个多功能的工具,可以用它来导出堆、查看Java进程、导出线程信息、执行GC、还可以进行采样分析(jmc 工具的飞行记录器)。

jcmd <pid | main class> <command ... | PerfCounter.print | -f file>

  • pid:接收诊断命令请求的进程ID。
    main class :接收诊断命令请求的进程的main类。匹配进程时,main类名称中包含指定子字符串的任何进程均是匹配的。如果多个正在运行的Java进程共享同一个main类,诊断命令请求将会发送到所有的这些进程中。

  • command: 接收诊断命令请求的进程的main类。匹配进程时,main类名称中包含指定子字符串的任何进程均是匹配的。如果多个正在运行的Java进程共享同一个main类,诊断命令请求将会发送到所有的这些进程中。

注意: 如果任何参数含有空格,你必须使用英文的单引号或双引号将其包围起来。 此外,你必须使用转义字符来转移参数中的单引号或双引号,以阻止操作系统shell处理这些引用标记。当然,你也可以在参数两侧加上单引号,然后在参数内使用双引号(或者,在参数两侧加上双引号,在参数中使用单引号)。

  • Perfcounter.print:打印目标Java进程上可用的性能计数器。性能计数器的列表可能会随着Java进程的不同而产生变化。

  • -f file:从文件file中读取命令,然后在目标Java进程上调用这些命令。在file中,每个命令必须写在单独的一行。以"#"开头的行会被忽略。当所有行的命令被调用完毕后,或者读取到含有stop关键字的命令,将会终止对file的处理。

  • -l:查看所有的进程列表信息。

(1) 查看进程 jcmd -l
命令:jcmd -l
描述:查看 当前机器上所有的 jvm 进程信息

  jcmd 
  jcmd -l
  jps 

这三个命令的效果是一样的

(2) 查看性能统计
命令:jcmd <pid> PerfCounter.print
描述:查看指定进程的性能统计信息。

(3) 列出当前运行的 java 进程可以执行的操作
命令:jcmd <pid> help

(4) 查看具体命令的选项
如果想查看命令的选项,比如想查看 JFR.dump 命令选项,可以通过如下命令:
jcmd 22604 help JFR.dump

(5) JRF 相关命令
JRF 功能跟 jmc.exe 工具的飞行记录器的功能一样的。
要使用 JRF 相关的功能,必须使用 VM.unlock_commercial_features 参数取消锁定商业功能 。

  1. 解锁 商业功能

检查 标志位 jcmd <pid> VM.check_commercial_features

解锁 jcmd <pid> VM.unlock_commercial_features


2. 启动JFR
执行命令:jcmd <pid> JFR.start name=abc duration=120s

3. Dump JFR
等待至少duration(本文设定120s)后,执行命令:jcmd <pid> JFR.dump name=abc filename=abc.jfr

(注意,文件名必须为.jfr后缀)


  1. 检查JFR状态
    执行命令:jcmd <pid> JFR.check name=abc

  2. 停止JFR
    执行命令:jcmd <pid> JFR.stop name=abc

  3. JMC分析
    切回开发机器,下载步骤3中生成的abc.jfr,打开jmc,导入abc.jfr即可进行可视化分析

(6) VM.uptime
命令:jcmd <pid> VM.uptime
描述:查看 JVM 的启动时长:

(7) GC.class_histogram
命令:jcmd <pid> GC.class_histogram
描述:查看系统中类统计信息

这里和jmap -histo pid的效果是一样的
这个可以查看每个类的实例数量和占用空间大小。

(8) Thread.print
命令:jcmd <pid> Thread.print
描述:查看线程堆栈信息。

该命令同 jstack 命令。

(9) GC.heap_dump
命令:jcmd <pid> GC.heap_dump FILE_NAME
描述:查看 JVM 的Heap Dump

跟 jmap命令:jmap -dump:format=b file=heapdump.phrof <pid> 效果一样。
导出的 dump 文件,可以使用MAT 或者 Visual VM 等工具进行分析。

注意:如果只指定文件名,默认会生成在启动 JVM 的目录里。

(10) VM.system_properties
命令:jcmd <pid> VM.system_properties
描述:查看 JVM 的属性信息

(11) VM.flags
命令:jcmd <pid> VM.flags
描述:查看 JVM 的启动参数

(12) VMmand_line
命令:jcmd <pid> VMmand_line
描述:查看 JVM 的启动命令行

(13) GC.run_finalization
命令:jcmd <pid> GC.run_finalization
描述: 对 JVM 执行 java.lang.System.runFinalization()

执行一次finalization操作,相当于执行java.lang.System.runFinalization()

(14) GC.run
命令:jcmd <pid> GC.run
描述:对 JVM 执行 java.lang.System.gc()

告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的。

(15) PerfCounter.print
命令:jcmd <pid> PerfCounter.print
描述:查看 JVM 性能相关的参数

(16) VM.version
命令:jcmd <pid> VM.version
描述:查看目标jvm进程的版本信息

5.2.7 JConsole:JMX的可视化管理工具

这个工具相比较前面几个工具,使用率比较高,很重要。它是一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM。用java写的GUI程序,用来监控VM,并可监控远程的VM,非常易用,而且功能非常强。

在cmd里面输入 jconsole,选则进程就可以了。(前提是在IDE工具先建立一个线程运行着)

然后我们选择了相应的选项之后,进入这个工具就会出现下面这个界面

5.2.8 VisualVM:多合一故障管理工具

这个工具也很牛bility。它同jconsole都是一个基于图形化界面的、可以查看本地及远程的JAVA GUI监控工具,Jvisualvm同jconsole的使用方式一样,直接在命令行打入jvisualvm即可启动,jvisualvm界面更美观一些,数据更实时:

最上面也有菜单,你可以选择不同的选项来展示。自己动手试一遍是最好的。

5.2.9 JMC (Java Mission Control)

JMC打开性能日志后,主要包括7部分性能报告,分别是一般信息、内存、代码、线程、I/O、系统、事件。其中,内存、代码、线程及I/O是系统分析的主要部分,本文会重点进行阐述。

5.2 JVM 参数设置

5.2.1 堆大小设置

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

5.2.1.1 典型设置:`

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
- Xmx3550m :设置JVM最大可用内存为3550M。
-Xms3550m :设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g :设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k :设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4 :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4 :设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m :设置持久代大小为16m。
-XX:MaxTenuringThreshold=0 :设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代 。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间 ,增加在年轻代即被回收的概论。

5.2.2 回收器选择器

JVM给了三种选择:串行收集器、并行收集器、并发收集器 ,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置 进行判断。
吞吐量优先 的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

5.2.2.1 典型设置:

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC :选择垃圾收集器为并行收集器。 此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20 :配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC :配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100 : 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy :设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

5.2.3 响应时间优先 的并发收集器

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

5.2.3.1 典型设置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC : 设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC :设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction :由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection :打开对年老代的压缩。可能会影响性能,但是可以消除碎片

5.2.4 辅助信息

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
-XX:+PrintGC
输出形式:

[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails
输出形式:

[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps 可与上面两个混合使用
输出形式:
11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
-XX:+PrintGCApplicationConcurrentTime: 打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
输出形式:
Application time: 0.5291524 seconds
-XX:+PrintGCApplicationStoppedTime :打印垃圾回收期间程序暂停的时间。可与上面混合使用
输出形式:
Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC :
打印GC前后的详细堆栈信息
输出形式:

34.702: [GC {Heap before gc invocations=7:
def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)
to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)
tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
}
, 0.0757599 secs]

-Xloggc:filename : 与上面几个配合使用,把相关日志信息记录到文件以便分析。

5.2.6 常见配置汇总

5.2.6.1 常用参数设置

-Xms :初始堆大小
-Xmx :最大堆大小
-XX:NewSize=n :设置年轻代大小
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n :设置持久代大小

5.2.6.2 收集器设置

-XX:+UseSerialGC :设置串行收集器
-XX:+UseParallelGC :设置并行收集器
-XX:+UseParalledlOldGC :设置并行年老代收集器
-XX:+UseConcMarkSweepGC :设置并发收集器

5.2.6.3 垃圾回收统计信息

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

5.2.6.3 并行收集器设置

-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间
-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

5.2.6.3并发收集器设置

-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

本文标签: 虚拟机原理JavaJVM