jvm相关知识整理

jvm相关相关知识点,主要分为以下四大块:

  • jvm内存结构

  • java类加载机制

  • 垃圾回收算法及垃圾回收器

  • 常用GC分析和调优命令

1.jvm内存结构

以jdk1.7中的jvm为例,上图为jvm内存结构布局。jvm内存结构主要有三大块:堆内存、方法区和栈。

堆内存是jvm中最大的一块,它由年轻代和老年代组成,而年轻代内存又被分为三部分,Eden、From Survivor空间和To Survivor空间。默认情况下,三者按照8:1:1的比例进行分配。

 方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为了与java堆区分,方法区还有一个别名Non-Heap。

 栈又分为java虚拟机栈和本地方法栈,栈主要用于方法的执行。

 通过一张图来了解如何通过jvm参数来控制各区域的内存大小。

 控制参数

  • -Xms设置堆的最小空间大小。

  • -Xmx设置堆的最大空间大小。

  • -XX:NewSize设置新生代最小空间大小。

  • -XX:MaxNewSize设置新生代最大空间大小。

  • -XX:PermSize设置永久代最小空间大小。

  • -XX:MaxPermSize设置永久代最大空间大小。

  • -Xss设置每个线程的堆栈大小。

 没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。

  老年代大小=堆内存大小-年轻代大小

 上图为java运行时数据区示意图。可以看出,堆内存和方法区为所有线程共享,而java栈、本地方法栈和程序计数器为线程私有。

下面一一介绍这几个区域的作用。

1.1 java堆(Heap)

 对于大多数应用来说,java堆(Heap)是所有java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。

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

1.2 方法区(Method Area)

 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

 对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。

 Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。

 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。方法区有时被称为持久代(PermGen)。

 所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:伊甸区(Eden),幸存者区域(Survivor Sapce),老年代(Old Generation Space)。

 方法的执行都是伴随着线程的。原始类型的本地变量以及引用都存放在线程栈中。而引用关联的对象比如String,都存在在堆中。

1.3 程序计数器(Program Counter Register)

 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

1.4 JVM栈(JVM Stacks)

 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

1.5 本地方法栈(Native Method Stacks)

 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

2.java类的加载机制

2.1 类加载机制

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

 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

加载.class文件的方式

  • 从本地系统中直接加载

  • 通过网络下载.class文件

  • 从zip,jar等归档文件中加载.class文件

  • 从专有数据库中提取.class文件

  • 将Java源文件动态编译为.class文件

2.2 类的生命周期

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

2.2.1 加载

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

  • 通过一个类的全限定名来获取其定义的二进制字节流。

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

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

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

2.2.2 连接

2.2.2.1 验证

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

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  • 符号引用验证:确保解析动作能正确执行。

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

2.2.2.2 准备

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

  • 1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

  • 2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

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

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

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。

  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。

  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。

  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

  • 3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

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

2.2.2.3 解析

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

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

2.2.3 初始化

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

  • ①声明类变量是指定初始值

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

 JVM初始化步骤

  • 1、假如这个类还没有被加载和连接,则程序先加载并连接该类

  • 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

  • 3、假如类中有初始化语句,则系统依次执行这些初始化语句

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

  • 创建类的实例,也就是new的方式

  • 访问某个类或接口的静态变量,或者对该静态变量赋值

  • 调用类的静态方法

  • 反射(如Class.forName(“com.shengsiyuan.Test”)

  • 初始化某个类的子类,则其父类也会被初始化

  • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

2.2.4 结束生命周期

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

  • 执行了System.exit()方法

  • 程序正常执行结束

  • 程序在执行过程中遇到了异常或错误而异常终止

  • 由于操作系统出现错误而导致Java虚拟机进程终止

2.3 类加载器

 类加载器只用于实现类的加载动作,但它在java程序中起到的作用却远不限于类加载阶段。先看看java中共有哪些类加载器吧。

public class ClassLoaderTest {
   public static void main(String[] args) {
       ClassLoader loader=ClassLoaderTest.class.getClassLoader();
       System.out.println(loader);
       System.out.println(loader.getParent());
       System.out.println(loader.getParent().getParent());
  }
}

运行后,输出结果:

sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null

从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

这几种类加载器的层次关系如下图所示:

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

 站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其它的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

 站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  • 启动类加载器Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

  • *扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。**

  • 应用程序类加载器Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

 应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

  • 1、在执行非置信代码之前,自动验证数字签名。

  • 2、动态地创建符合用户特定需要的定制化构建类。

  • 3、从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

2.4 类的加载

类加载有三种方式:

  • 1、命令行启动应用时候由JVM初始化加载

  • 2、通过Class.forName()方法动态加载

  • 3、通过ClassLoader.loadClass()方法动态加载

例子:

LoaderTest类

public class LoaderTest {

   public static void main(String[] args) throws ClassNotFoundException {
      ClassLoader loader= Test.class.getClassLoader();
       //使用ClassLoader.loadClass()来加载类,不会执行初始化块
       loader.loadClass("com.jvm.loader.Test");
       System.out.println(loader);
       System.out.println("****************");
     
       //使用Class.forName()来加载类,默认执行初始化块
     ClassLoader loader1=  Class.forName("com.jvm.loader.Test").getClassLoader();
       System.out.println(loader1);
       System.out.println("================");

//Class.forName()來加载类,并指定ClassLoader,初始化时不执行静态块
       ClassLoader loader2=Class.forName("com.jvm.loader.Test",false,loader).getClassLoader();
       System.out.println(loader2);
       System.out.println("----------------");
  }
}

Test类

public class Test { 
       static {
               System.out.println("静态初始化块执行了!");
      }
}

分别切换加载方式,会有不同的输出结果。

Class.forName()和ClassLoader.loadClass()区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

2.5 双亲委派模型

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

双亲委派机制:

  • 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

  • 2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。

  • 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

  • 4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

ClassLoader源码分析:

public Class<?> loadClass(String name)throws ClassNotFoundException {
       return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
       // 首先判断该类型是否已经被加载
       Class c = findLoadedClass(name);
       if (c == null) {
           //双亲委派的体现
           //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
           try {
               if (parent != null) {
                    //如果存在父类加载器,就委派给父类加载器加载
                   c = parent.loadClass(name, false);
              } else {
               //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                   c = findBootstrapClass0(name);
              }
          } catch (ClassNotFoundException e) {
            // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
               c = findClass(name);
          }
      }
       if (resolve) {
           resolveClass(c);
      }
       return c;
  }

双亲委派模型意义:

  • 系统类防止内存中出现多份同样的字节码

  • 保证Java程序安全稳定运行

2.6 自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

public class MyClassLoader extends ClassLoader {

   private String root;

   @Override
   protected Class<?> findClass(String name) throws ClassNotFoundException {
       byte[] classData=loadClassData(name);
       if (classData==null){
           throw new ClassNotFoundException();
      }else {
           return defineClass(name,classData,0,classData.length);
      }
  }

   private byte[] loadClassData(String className) {
       String fileName=root+ File.separatorChar+className.replace(',',File.separatorChar)+".class";

       try {
           InputStream in=new FileInputStream(fileName);
           ByteArrayOutputStream baos=new ByteArrayOutputStream();
           int bufferSize=1024;
           byte[] buffer=new byte[bufferSize];
           int length=0;
           while ((length=in.read(buffer))!=-1){
               baos.write(buffer,0,length);
          }
           return baos.toByteArray();
      }catch (IOException e){
           e.printStackTrace();
      }
      return null;
  }

   public String getRoot() {
       return root;
  }

   public void setRoot(String root) {
       this.root = root;
  }

   public static void main(String[] args) {
       MyClassLoader classLoader=new MyClassLoader();
       classLoader.setRoot("F:\\Work_Space\\jvm\\src");
      try {
          Class<?> testClass=null;
          testClass=classLoader.loadClass("com.jvm.loader.Test");
          Object object=testClass.newInstance();
          System.out.println(object.getClass().getClassLoader());
      }catch (ClassNotFoundException e){
          e.printStackTrace();
      }catch (IllegalAccessException e){
          e.printStackTrace();
      }catch (InstantiationException e){
          e.printStackTrace();
      }
  }
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

  • 1、这里传递的文件名需要是类的全限定性名称,即com.jvm.loader.Test格式的,因为 defineClass 方法是按这种格式进行处理的。

  • 2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。

  • 3、这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把com/paddx/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。

如何破坏双亲委派模式?

 继承ClassLoader类并重写loadClass()方法,重新定义新的加载方式,则双亲委派模式就会被打破。

如下所示:

public class MyClassLoaderTest2 extends ClassLoader {

   public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

       ClassLoader loader=new ClassLoader() {
           @Override
           public Class<?> loadClass(String name) {
              String fileName=name.substring(name.lastIndexOf(".")+1)+".class";
               InputStream in;
               try {
                   in=getClass().getResourceAsStream(fileName);
                   if (in==null){
                       return super.loadClass(fileName);
                  }
                   byte[] bytes=new byte[in.available()];
                   in.read(bytes);
                   in.close();
              } catch (Exception e) {
                   e.printStackTrace();
              }
               return null;
          }
      };



       System.out.println(loader);
       loader.loadClass("com.jvm.loader.Test").newInstance();

  }
}

控制台输出:

com.jvm.loader.MyClassLoaderTest2$1@4554617c
Exception in thread "main" java.lang.NullPointerException
at com.jvm.loader.MyClassLoaderTest2.main(MyClassLoaderTest2.java:36)

 出现报错,这是由于测试类Test,继承Object,因为破坏了双亲加载模型,Object类也会使用这个加载器加载,从Classpath下找这个类,必然会出错。

3.GC算法

 垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了。 jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的.

3.1 对象存活判断

 判断对象是否存活一般有两种方式:

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

 在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象。

  • 方法区中类静态属性实体引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI引用的对象。

3.2 GC算法

3.2.1 标记 -清除算法

 “标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

 它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

3.2.2 复制算法

 “复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

 这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

3.2.3 标记-压缩算法

 复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

 根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

3.2.4 分代收集算法

 GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

 分代收集(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

4.垃圾收集器

垃圾收集器就是垃圾回收算法的具体实现。

4.1 Serial收集器

 串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)

 参数控制:-XX:+UseSerialGC 串行收集器

 

 ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

 参数控制:

 -XX:+UseParNewGC ParNew收集器  -XX:ParallelGCThreads 限制线程数量

4.2 Parallel收集器

 Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩

 参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行

4.3 Parallel Old 收集器

 Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供

 参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

4.4 CMS收集器

 CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

 从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)

  • 并发标记(CMS concurrent mark)

  • 重新标记(CMS remark)

  • 并发清除(CMS concurrent sweep)

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

 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

 优点: 并发收集、低停顿  缺点: 产生大量空间碎片、并发阶段会降低吞吐量

 参数控制:

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

4.5 G1收集器

 G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

  2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

 G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

 收集步骤:

  • 1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)

  • 2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。

  • 3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

  • 4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

  • 5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

  • 6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

4.6 常用的收集器组合

 

服务器31 新生代GC策略 老年老代GC策略 说明
组合1 Serial Serial Old Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
组合2 Serial CMS+Serial Old CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。
组合3 ParNew CMS 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
组合4 ParNew Serial Old 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
组合5 Parallel Scavenge Serial Old Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
组合6 Parallel Scavenge Parallel Old Parallel Old是Serial Old的并行版本
组合7 G1GC G1GC -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启;-XX:MaxGCPauseMillis =50 #暂停时间目标;-XX:GCPauseIntervalMillis =200 #暂停间隔目标;-XX:+G1YoungGenSize=512m #年轻代大小;-XX:SurvivorRatio=6 #幸存区比例

5. jdk1.8中的MetaSpace

5.1 Metaspace

 在jdk1.8之前,在 HotSpot JVM 中,永久代中用于存放类和方法的元数据以及常量池,比如ClassMethod。每当一个类初次被加载的时候,它的元数据都会放到永久代中。而永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即常见的java.lang.OutOfMemoryError: PermGen ,因而不得不对虚拟机做调优。因而在jdk1.8中MetaSpace出现,取代了之前的PerGen。

 那么,Metaspace(元空间)是哪一块区域?官方的解释是:

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.

 即JDK 8 开始把类的元数据放到本地堆内存(native heap)中,这一块区域就叫 Metaspace,中文名叫元空间。

 使用本地内存有什么好处呢?最直接的表现就是OOM问题将不复存在,因为默认的类的元数据分配只受本地内存大小的限制,也就是说本地内存剩余多少,理论上Metaspace就可以有多大(貌似容量还与操作系统的虚拟内存有关?这里不太清楚),这解决了空间不足的问题。不过,让 Metaspace 变得无限大显然是不现实的,因此我们也要限制 Metaspace 的大小:使用 -XX:MaxMetaspaceSize 参数来指定 Metaspace 区域的大小。JVM 默认在运行时根据需要动态地设置 MaxMetaspaceSize 的大小。

 而在垃圾回收方面,如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。根据JDK 8的特性,G1和CMS都会很好地收集Metaspace区(一般都伴随着Full GC)。因而为了减少垃圾回收的频率及时间,控制吞吐量,对Metaspace进行适当的监控和调优是非常有必要的。如果在Metaspace区发生了频繁的Full GC,那么可能表示存在内存泄露或Metaspace区的空间太小了。

5.2 新增的 JVM 参数

  • -XX:MetaspaceSize 是分配给类元数据空间(以字节计)的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark ),此值为估计值。MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。

  • -XX:MaxMetaspaceSize 是分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。

  • -XX:MinMetaspaceFreeRatio 表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。

  • -XX:MaxMetaspaceFreeRatio 表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。

5.3 总结

1、持久代场景 • 这块内存区域被完全移除。 • PermSize和MaxPermSize JVM 参数会被忽略,并且在启动的时候会给出警告信息。

2、Metaspace 内存分配模型 • 对于类元数据的大多数内存分配都不会发生在本地内存。 • 被用于描述类元数据的类对象被移除。

3、Metaspace 容量 • 默认的,类元数据分配限制于可用的本地内存 (容量大小依赖于你用32位jvm或者64位jvm的操作系统可用虚拟内存)。 • 新的标记已经可以使用 (MaxMetaspaceSize),它允许你限制用于类元数据的本地内存大小。如果你没有指定这个标记,Metaspace会根据运行时应用程序的需求来动态的控制大小。

4、Metaspace 垃圾收集 • 一旦类元数据的使用量达到了“MaxMetaspaceSize”指定的值,对于无用的类和类加载器,垃圾收集此时会触发。 • 为了控制这种垃圾收集的频率和延迟,合适的监控和调整Metaspace非常有必要。过于频繁的Metaspace垃圾收集是类和类加载器发生内存泄露的征兆,同时也说明你的应用程序内存大小不合适,需要调整。

5、Java 堆空间影响 •一些杂项数据被移到了Java堆空间。这意味着当你更新到JDK8后会观察到Java堆空间的增长。

6、Metaspace 监控 • Metaspace 的使用可以通过HotSpot 1.8的详细的GC日志输出观察到。 • 在基于b75上测试的时候Jstat 和 JVisualVM 还没有更新,旧的持久代空间引用依然存在。

 

6 常用jdk监控命令

6.1 jps

 JSP:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。

命令格式

jps

示例

F:\Work_Space\jvm>jps
10272 Launcher
6240 Jps
6432
15076 HeapOOM
1000 KotlinCompileDaemon

6.2 jstat

 jstat:(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

命令格式

jstat [option] LVMID [interval] [count]

参数

  • [option] : 操作参数

  • LVMID : 本地虚拟机进程ID

  • [interval] : 连续输出的时间间隔

  • [count] : 连续输出的次数

option 参数总览

 

Option Displays…
class class loader的行为统计。Statistics on the behavior of the class loader.
compiler HotSpt JIT编译器行为统计。Statistics of the behavior of the HotSpot Just-in-Time compiler.
gc 垃圾回收堆的行为统计。Statistics of the behavior of the garbage collected heap.
gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计。Statistics of the capacities of the generations and their corresponding spaces.
gcutil 垃圾回收统计概述。Summary of garbage collection statistics.
gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因。Summary of garbage collection statistics (same as -gcutil), with the cause of the last and
gcnew 新生代行为统计。Statistics of the behavior of the new generation.
gcnewcapacity 新生代与其相应的内存空间的统计。Statistics of the sizes of the new generations and its corresponding spaces.
gcold 年老代和永生代行为统计。Statistics of the behavior of the old and permanent generations.
gcoldcapacity 年老代行为统计。Statistics of the sizes of the old generation.
gcpermcapacity 永生代行为统计。Statistics of the sizes of the permanent generation.
printcompilation HotSpot编译方法统计。HotSpot compilation method statistics.

 

option 参数详解

-class

监视类装载、卸载数量、总空间以及耗费的时间

F:\Work_Space\jvm>jstat -class 15076
Loaded  Bytes  Unloaded  Bytes     Time
  611  1230.1        0     0.0       0.07
  • Loaded : 加载class的数量

  • Bytes : class字节大小

  • Unloaded : 未加载class的数量

  • Bytes : 未加载class的字节大小

  • Time : 加载时间

-compiler

输出JIT编译过的方法数量耗时等

F:\Work_Space\jvm>jstat -compiler 15076
Compiled Failed Invalid   Time   FailedType FailedMethod
     78      0       0     0.02          0
  • Compiled : 编译数量

  • Failed : 编译失败数量

  • Invalid : 无效数量

  • Time : 编译耗时

  • FailedType : 失败类型

  • FailedMethod : 失败方法的全限定名

-gc

垃圾回收堆的行为统计,常用命令

F:\Work_Space\jvm>jstat -gc 15076
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
459776.0 459776.0  0.0    0.0   338432.0 321787.3 2759680.0  2759325.8  4864.0 3718.0 512.0  409.6      13   11.420   9    105.459  116.878

C即Capacity 总容量,U即Used 已使用的容量

  • S0C : survivor0区的总容量

  • S1C : survivor1区的总容量

  • S0U : survivor0区已使用的容量

  • S1C : survivor1区已使用的容量

  • EC : Eden区的总容量

  • EU : Eden区已使用的容量

  • OC : Old区的总容量

  • OU : Old区已使用的容量

  • PC 当前perm的容量 (KB)

  • PU perm的使用 (KB)

  • YGC : 新生代垃圾回收次数

  • YGCT : 新生代垃圾回收时间

  • FGC : 老年代垃圾回收次数

  • FGCT : 老年代垃圾回收时间

  • GCT : 垃圾回收总消耗时间

F:\Work_Space\jvm>jstat -gc 15076 2000

 这个命令意思就是每隔2000ms输出15076的gc情况,也可以在后面再加一个次数参数,如

F:\Work_Space\jvm>jstat -gc 15076 2000 20

 表示每隔2000ms输出一次,共输出20次

-gccapacity

 同-gc,不过还会输出Java堆各区域使用到的最大、最小空间

F:\Work_Space\jvm>jstat -gccapacity 15076
NGCMN   NGCMX     NGC     S0C   S1C       EC     OGCMN     OGCMX       OGC         OC       MCMN     MCMX     MC     CCSMN   CCSMX     CCSC   YGC   FGC
86528.0 1379328.0 1001472.0 351232.0 287744.0 204288.0   173568.0 2759680.0   956416.0   956416.0     0.0 1056768.0   4864.0     0.0 1048576.0   512.0     7     4
  • NGCMN : 新生代占用的最小空间

  • NGCMX : 新生代占用的最大空间

  • OGCMN : 老年代占用的最小空间

  • OGCMX : 老年代占用的最大空间

  • OGC:当前年老代的容量 (KB)

  • OC:当前年老代的空间 (KB)

  • PGCMN : perm占用的最小空间

  • PGCMX : perm占用的最大空间

-gcutil

 同-gc,不过输出的是已使用空间占总空间的百分比

F:\Work_Space\jvm>jstat -gcutil 15076
 S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
 0.00  54.46  21.99  99.99  76.50  80.00     13   11.651     8   83.381   95.032
-gccause

 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因

F:\Work_Space\jvm>jstat -gccause 15076
 S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC
 0.00   0.00  95.37  99.99  76.50  80.00     13   11.651     9  104.577  116.227 Ergonomics           Allocation Failure
  • LGCC:最近垃圾回收的原因

  • GCC:当前垃圾回收的原因

-gcnew

 统计新生代的行为

F:\Work_Space\jvm>jstat -gcnew 15076
S0C   S1C   S0U   S1U   TT MTT DSS     EC       EU     YGC     YGCT
351232.0 287744.0   0.0 287744.0 4 15 351232.0 204288.0 204288.0     7   2.280
  • TT:Tenuring threshold(提升阈值)

  • MTT:最大的tenuring threshold

  • DSS:survivor区域大小 (KB)

-gcnewcapacity

新生代与其相应的内存空间的统计

F:\Work_Space\jvm>jstat -gcnewcapacity 15076
NGCMN     NGCMX       NGC     S0CMX     S0C     S1CMX     S1C       ECMX       EC     YGC   FGC
  86528.0 1379328.0 1370624.0 459776.0 459776.0 459776.0 459776.0 1378304.0   339968.0   13     7
  • NGC:当前年轻代的容量 (KB)

  • S0CMX:最大的S0空间 (KB)

  • S0C:当前S0空间 (KB)

  • ECMX:最大eden空间 (KB)

  • EC:当前eden空间 (KB)

-gcold

统计老生代的行为

F:\Work_Space\jvm>jstat -gcold 15076
  MC       MU      CCSC     CCSU       OC          OU       YGC    FGC    FGCT     GCT
 4864.0   3718.0    512.0    409.6   2759680.0   2759327.0     13     8   84.327   96.474
-gcoldcapacity

统计老生代的大小和空间

F:\Work_Space\jvm>jstat -gcoldcapacity 15076
  OGCMN       OGCMX        OGC         OC       YGC   FGC    FGCT     GCT
  173568.0   2759680.0   1687552.0   1687552.0     8     4   19.705   21.842
-printcompilation

hotspot编译方法统计

F:\Work_Space\jvm>jstat -printcompilation 15076
Compiled Size Type Method
    81   138   1 java/lang/StringBuffer append
  • Compiled:被执行的编译任务的数量

  • Size:方法字节码的字节数

  • Type:编译类型

  • Method:编译方法的类名和方法名。类名使用"/" 代替 "." 作为空间分隔符. 方法名是给出类的方法名. 格式是一致于HotSpot - XX:+PrintComplation 选项

6.3 jmap

 jmap:(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还阔以使用 -XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。 jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。

命令格式

jmap [option] LVMID

option参数

  • dump : 生成堆转储快照

  • finalizerinfo : 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象

  • heap : 显示Java堆详细信息

  • histo : 显示堆中对象的统计信息

  • permstat : to print permanent generation statistics

  • F : 当-dump没有响应时,强制生成dump快照

option 参数详解

-dump

常用格式

-dump::live,format=b,file=<filename> pid 

dump堆到文件,format指定输出格式,live指明是活着的对象,file指定文件名

F:\Work_Space\jvm>jmap -dump:format=b,file=dump.hpro 15076
Dumping heap to F:\Work_Space\jvm\dump.hpro ...
Heap dump file created

dump.hprof这个后缀是为了后续可以直接用MAT(Memory Anlysis Tool)打开。

-finalizerinfo

打印等待回收对象的信息

F:\Work_Space\jvm>jmap -finalizerinfo 15076
Attaching to process ID 15076, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.192-b12
Number of objects pending for finalization: 0

可以看到当前F-QUEUE队列中并没有等待Finalizer线程执行finalizer方法的对象。

-heap

打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况,可以用此来判断内存目前的使用情况以及垃圾回收情况

F:\Work_Space\jvm>jmap -heap 15076
Attaching to process ID 15076, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.192-b12

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
  MinHeapFreeRatio         = 0
  MaxHeapFreeRatio         = 100
  MaxHeapSize             = 734003200 (700.0MB)
  NewSize                 = 88604672 (84.5MB)
  MaxNewSize               = 244318208 (233.0MB)
  OldSize                 = 177733632 (169.5MB)
  NewRatio                 = 2
  SurvivorRatio           = 8
  MetaspaceSize           = 21807104 (20.796875MB)
  CompressedClassSpaceSize = 1073741824 (1024.0MB)
  MaxMetaspaceSize         = 17592186044415 MB
  G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
  capacity = 66584576 (63.5MB)
  used     = 20812416 (19.8482666015625MB)
  free     = 45772160 (43.6517333984375MB)
  31.25711275836614% used
From Space:
  capacity = 11010048 (10.5MB)
  used     = 0 (0.0MB)
  free     = 11010048 (10.5MB)
  0.0% used
To Space:
  capacity = 11010048 (10.5MB)
  used     = 0 (0.0MB)
  free     = 11010048 (10.5MB)
  0.0% used
PS Old Generation
  capacity = 106430464 (101.5MB)
  used     = 6112296 (5.829139709472656MB)
  free     = 100318168 (95.67086029052734MB)
  5.742994787657789% used

5187 interned Strings occupying 445296 bytes.

可以很清楚的看到Java堆中各个区域目前的情况。

猜你喜欢

转载自www.cnblogs.com/reecelin/p/11922284.html