一篇学习JVM

基础入门

1.JDK、JRE、JVM三者间的联系与区别

在这里插入图片描述

JDK

JDK(Java SE Development Kit),Java标准开发包,它提供了编译运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等。

下图是JDK的安装目录:
在这里插入图片描述

JRE

JRE( Java Runtime Environment) Java运行环境,用于解释执行Java的字节码文件。普通用户而只需要安装 JRE(Java Runtime Environment)来运行 Java 程序。而程序开发者必须安装JDK来编译、调试程序。

下图是JRE的安装目录:里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。 
在这里插入图片描述

JVM

在这里插入图片描述

定义:java virtual meachine -java运行时环境(java二进制字节码的运行环境)。JVM是运行在操作系统之上的,它与硬件没有直接的交互。Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果

好处:

一次编写到处运行  .class
自动内存管理,垃圾回收  GC    对象:  new Object()
数组下标越界检查
多态、多线程

在这里插入图片描述

我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

区别与联系

JDK 用于开发,JRE 用于运行java程序 ;如果只是运行Java程序,可以只安装JRE,无序安装JDK。
JDk包含JRE,JDK 和 JRE 中都包含 JVM。
JVM 是 java 编程语言的核心并且具有平台独立性。

JDK,JRE,JVM的区别

JDK :Java Development Kit,Java 语言的软件开发工具包(SDK)。在JDK的安装目录下有一个jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib合起来就称为jre;
JRE:Java Runtime Environment,Java运行环境,包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,没有包含任何开发工具;
JVM:Java虚拟机,有多个版本,用来支持Java跨平台的

在这里插å
¥å›¾ç‰‡æè¿°

2.JVM的位置

JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,可操作系统可以帮我们完成和硬件进行交互的工作。
在这里插入图片描述

3.JVM体系概览

在这里插入图片描述

橙色区域:所有线程共享,存在GC(垃圾回收)
灰色区域:线程私有

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

运行时常量池:是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。

堆内存(线程共享):所有线程共享的一块区域,垃圾收集器管理的主要区域。目前主要的垃圾回收算法都是分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等,默认情况下新生代按照8:1:1的比例来分配。根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘一样。

程序计数器: Java 线程私有,类似于操作系统里的 PC 计数器,它可以看做是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

虚拟机栈(栈内存):Java线程私有,虚拟机展描述的是Java方法执行的内存模型:每个方法在执行的时候,都会创建一个栈帧用于存储局部变量、操作数、动态链接、方法出口等信息;每个方法调用都意味着一个栈帧在虚拟机栈中入栈到出栈的过程;

本地方法栈 :和Java虚拟机栈的作用类似,区别是该区域为 JVM 提供使用 native 方法的服务

4.Java文件是如何被运行的

比如我们现在写了一个 HelloWorld.java 好了,那这个 HelloWorld.java 抛开所有东西不谈,那是不是就类似于一个文本文件,只是这个文本文件它写的都是英文,而且有一定的缩进而已。

那我们的 JVM 是不认识文本文件的,所以它需要一个 编译 ,让其成为一个它会读二进制文件的 HelloWorld.class

HelloWorld.java-------->编译---------->HelloWorld.class------->JVM编译解释-------->机器代码

① 类加载器
如果 JVM 想要执行这个 .class 文件,我们需要将其装进一个 类加载器 中,它就像一个搬运工一样,会把所有的 .class 文件全部搬进JVM里面来。
在这里插入图片描述
② 方法区
方法区 是用于存放类似于元数据信息方面的数据的,比如类信息,常量,静态变量,编译后代码···

类加载器将 .class 文件搬过来就是先丢到这一块上

③ 堆
堆 主要放了一些存储的数据,比如对象实例数组···等,它和方法区都同属于 线程共享区域 。也就是说它们都是 线程不安全 的

④ 栈
栈 这是我们的代码运行空间。我们编写的每一个方法都会放到 栈 里面运行。

我们会听说过 本地方法栈 或者 本地方法接口 这两个名词,不过我们基本不会涉及这两块的内容,它俩底层是使用C来进行工作的,和Java没有太大的关系。

⑤ 程序计数器
主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是 线程独享 的,就是说每一个线程都会有自己对应的一块区域而不会存在并发和多线程的问题。
在这里插入图片描述

小总结

Java文件经过编译后变成 .class 字节码文件
字节码文件通过类加载器被搬运到 JVM 虚拟机中
虚拟机主要的5大块:方法区,堆都为线程共享区域,有线程安全问题,栈和本地方法栈和计数器都是独享区域,不存在线程安全问题,而 JVM 的调优主要就是围绕堆,栈两大块进行

5.Java运行小例子

学生类
在这里插入图片描述
一个main方法
在这里插入图片描述

执行main方法的步骤如下:

1. 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载
2. JVM 找到 App 的主程序入口,执行main方法
3. 这个main中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个Student对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中
 4. 加载完 Student 类后,JVM 在**堆中**为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 指向方法区中的 Student 类的类型信息 的引用执行student.sayName();时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
5. 执行sayName()

组件剖析

1.类加载器 ClassLoader

在这里插入图片描述

它是负责加载.class文件的,它们在文件开头会有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine 来决定!!!

1.1.类的生命周期

在这里插入图片描述

从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载验证准备解析初始化使用卸载。其中验证,准备,解析三个部分统称为连接.

加载

1.将class文件加载到内存   Student.java---->Student.class---->加载到方法区
2.将静态数据结构转化成方法区中运行时的数据结构

3.在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口 java.lang.Class对象反射的入口
   
   反射,获取Class的对象:
      > 1. Class.forName("java.lang.String");
      > 2. String.class
      > 3. new String().getClass();
      
      ---------------------------------------------------------------------------------------
      > 4. Integer.TYPE  (包装类)  --- 基本数据类型的class  int.class
      > 5. Class c5= int.class;  // int.class
     
      
 ------------------------------------------------------------------------------------------------   
 "Hello","你好","中国人" -----> String类
 8848   250  8080 -----> Integer类
 刘德华   张学友  黎明 ------>  Person类
 String.class   Integer.class   Person.class -----> java.lang.Class类 反射的入口

链接

1.验证:确保加载的类符合JVM规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查

2.准备:为static变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)

static int a = 3;    静态变量 类变量 ,使用方式 类.变量名;
int a=10;    实例变量 

3.解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在import static java.lang.Math.*;这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)

初始化

初始化其实就是一个赋值的操作,它会执行一个类构造器(构造方法)的<clinit>()方法。由编译器自动收集类中所有变量的赋值动作,此时准备阶段时的那个 static int a = 3 的例子,在这个时候就正式赋值为3

-----------------------------------------------------------------------------------
Student()
Student(int age,String name)

卸载

GC将无用对象从内存中卸载   垃圾回收算法!

1.2.类加载器的加载顺序

加载一个Class类的顺序也是有优先级的,类加载器从最底层开始往上的顺序是这样的

虚拟机自带的加载器
   BootStrap ClassLoader:rt.jar
   
   Extention ClassLoader: 加载扩展的jar包
   
   App ClassLoader:指定的classpath下面的jar包中class(加载的时候我的类 或者导入的类)


用户自定义加载器 
     Custom ClassLoader:自定义的类加载器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。

类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法访问的,故而该引用为NULL

对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
在这里插入图片描述

2.什么是沙箱(sandbox)

Java安全模型的核心就是Java沙箱(sandbox),沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

3.双亲委派机制

当一个类收到了加载请求时,它是不会先自己去尝试加载的,而是委派给父类去完成,比如我现在要new一个Person,这个Person是我们自定义的类,如果我们要加载它,就会先委派App ClassLoader,只有当父类加载器都反馈自己无法完成这个请求(也就是父类加载器都没有找到加载所需的Class)时,子类加载器才会自行尝试加载

这样做的好处是,加载位于rt.jar包中的类时不管是哪个加载器加载,最终都会委托到BootStrap ClassLoader进行加载,这样保证了使用不同的类加载器得到的都是同一个结果。

其实这个也是一个隔离的作用,避免了我们的代码影响了JDK的代码,比如我现在要来一个

public class String(){
    public static void main(){sout;}
}

这种时候,我们的代码肯定会报错,因为在加载的时候其实是找到了rt.jar中的String.class,然后发现这也没有main方法

双亲委派模型的工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该首先传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
在这里插入图片描述

4.类加载器加载流程

在这里插入图片描述

5.执行引擎

Execution Engine执行引擎负责解释命令,提交操作系统执行。

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM锁识别的字节码指令、符号表和其他辅助信息,那么,如果想让一个Java程序运行起来、执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,

JVM中的执行引擎充当了将高级语言翻译为机器语言的译者.执行引擎的工作过程。

Engine执行代码时一般分为两种类型:

1.解释执行 传统方式
2.编译执行(e.g JIT),产生本地机器码,编译花费时间多,但是执行时效率和速度更高

6.1.什么是解释器( Interpreter),什么是JIT编译器?

解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

JIT (Just In Time Compiler)编译器(即时编译器):就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

6.2.为什么说Java是半编译半解释型语言?

JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
在这里插入图片描述

我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

7.Native Interface本地接口

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介绍。

使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。

这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

1.native 是用做java 和其他语言(如c++)进行协作时用的,也就是native 后的函数的实现不是用java写的
2.既然都不是java,那就别管它的源代码了,呵呵

8.本地方法栈

native关键字主要用于方法上

它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

一个native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。

在定义一个native方法时,并不提供实现体(比较像定义一个Java Interface),因为其实现体是由非Java语言在外面实现的

本地方法栈的特点

1.Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
2.本地方法栈,也是线程私有的。

允许被实现成固定或者是可动态扩展的内存大小(在内存溢出方面和虚拟机栈相同)
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。

3.如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
4.本地方法一般是使用C语言实现的。
5.它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

在这里插入图片描述

9. PC寄存器

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

10.方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。
静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中

实例变量存在堆内存中,和方法区无关

方法区就用来存储了每一个类的结构信息,不同的虚拟机实现是不一样的,有些叫永久代,有些称为元空间

方法区中的内容:

类型信息

类型的全限定名
超类的全限定名
直接超接口的全限定名
类型标志(该类是类类型还是接口类型)
类的访问描述符(public、private、default、abstract、final、static)

类型常量池
Jvm为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string, integer, 和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。 因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java程序的动态链接中起了核心的作用。

字段信息

 字段修饰符(public、protect、private、default) 
 字段的类型 
 字段名称

方法信息

方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表

类变量(静态变量)
就是类的静态变量,它只与类相关,所以称为类变量 。在jvm使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。

指向类加载器的引用
jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。

类加载器加载完某个类后,将这个类的一些信息保存在方法区,并将这个类加载器的一个引用作为类型信息的一部分保存在方法区中

指向Class实例的引用

jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。

例如,假如你有一个java.lang.Integer的对象引用,可以激活getClass()得到对应的类引用类加载的过程中(通过new 或者Class.forName方式加载某个类),虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象

方法表
运行时常量池

11.Stack栈是什么

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈存储什么?

11.1.栈帧中主要保存3类数据:

本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等

11.2.栈运行原理:

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……

遵循“先进后出”/“后进先出”原则。

在这里插入图片描述
图示在一个栈中有两个栈帧:

栈帧 2是最先被调用的方法,先入栈,然后方法 2 又调用了方法1,栈帧 1处于栈顶的位置,栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1和栈帧 2,线程结束,栈释放。

每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕 后会自动将此栈帧出栈。

11.3.虚拟机栈有什么作用

1.主管Java程序的运行 程序中的方法与局部变量 部分结果
2. 参与方法的调用与返回

在这里插入图片描述

11.4.栈的优点

1.栈是一种快速有效的分配存储方式 访问速度仅次于程序计数器
2.JVM直接对栈的操作只有两个每个方法的执行会伴随进栈(入栈 压栈),执行结束后会出栈(弹栈)
3.对于栈来说没有垃圾回收的问题(不存在GC有可能存在OOM)

11.5.虚拟机常见异常

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。

如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个StackOverflowError异常;

如果Java虚拟机栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常;

package com.it.test;

/**
 * @BelongsProject: Tmall
 * @BelongsPackage: com.it.test
 * @CreateTime: 2020-11-15 17:27
 * @Description: TODO
 */
public class TestStack {

    public static int i=1;
    
    public static void main(String[] args) {
        //我的电脑默认测试JVM栈的大小为9750左右
        //添加JVM命令行参数:-Xss1024k 之后为9777左右
        //添加JVM命令行参数:-Xss1m 之后为9789左右
        System.out.println(i++);
        main(args);
    }
}

在这里插入图片描述

11.6.设置栈内存大小

我们可以使用虚拟机参数-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度;

-Xss size

设置线程堆栈大小(以字节为单位)。附加字母k或K表示KB,m或M表示MB,和g或G表示GB。默认值取决于平台:

Linux / x64(64位):1024 KB
macOS(64位):1024 KB
Oracle Solaris / x64(64位):1024 KB
Windows:默认值取决于虚拟内存

下面的示例以不同的单位将线程堆栈大小设置为1024 KB:

-Xss1m (1mb)
-Xss1024k  (1024kb)
-Xss1048576

设置方式如下图所示:
在这里插入图片描述

深度调用后,撑爆了栈,是一个错误,不是异常
Exception in thread java.lang.StackOverflowError

java.lang.Object
	——java.lang.Throwable
		——java.lang.Error
			——java.lang.VirtualMachineError
				——java.lang.StackOverflowError

11.7.栈+堆+方法区的交互关系

在这里插入图片描述
HotSpot是使用指针的方式来访问对象:Java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址

12.常见的JVM种类

HotSpot VM(Sun公司)

HotSpot VM是绝对的主流。大家用它的时候很可能就没想过还有别的选择,或者是为了迁就依赖了Oracle/Sun JDK某些具体实现的烂代码而选择用HotSpot VM省点心。Oracle / Sun JDK、OpenJDK的各种变种(例如IcedTea、Zulu),用的都是相同核心的HotSpot VM。
当大家说起“Java性能如何如何”、“Java有多少种GC”、“JVM如何调优”云云,经常默认说的就是特指HotSpot VM。可见其“主流性”。

JDK8的HotSpot VM已经是以前的HotSpot VM与JRockit VM的合并版,也就是传说中的“HotRockit”,只是产品里名字还是叫HotSpot VM。

这个合并并不是要把JRockit的部分代码插进HotSpot里,而是把前者一些有价值的功能在后者里重新实现一遍。移除PermGen、Java Flight Recorder、jcmd等都属于合并项目的一部分不过要留意的是,这里的HotSpot VM特指“正常配置”版,而不包括“Zero / Shark”版。

Wikipedia那个页面上把后者称为“Zero Port”。用这个版本的人应该相当少,很多时候它的release版都build不成功。

J9 VM(IBM)
J9是IBM开发的一个高度模块化的JVM。在许多平台上,IBM J9 VM都只能跟IBM产品一起使用。这不是技术限制,而是许可证限制。
例如说在Windows上IBM JDK不是免费公开的,而是要跟IBM其它产品一起捆绑发布的;
使用IBM Rational、IBM WebSphere的话都有机会用到J9 VM(也可以自己选择配置使用别的Java SE JVM)。根据许可证,这种捆绑在产品里的J9 VM不应该用于运行别的Java程序…大家有没有自己“偷偷的”拿来跑别的程序IBM也没力气管
(咳咳而在一些IBM的硬件平台上,很少客户是只买硬件不买配套软件的,IBM给一整套解决方案,里面可能就包括了IBM JDK。
这样自然而然就用上了J9 VM。
所以J9 VM得算在主流里,虽然很少是大家主动选择的首选。
J9 VM的性能水平大致跟HotSpot VM是一个档次的。有时HotSpot快些,有时J9快些。
不过J9 VM有一些HotSpot VM在JDK8还不支持的功能,最显著的一个就是J9支持AOT编译和更强大的class data sharing

JRockit(BEA公司的)
JRockit以前Java SE的主流JVM中还有JRockit,跟HotSpot与J9一起并称三大主流JVM。
这三家的性能水平基本都在一个水平上,竞争很激烈。
自从Oracle把BEA和Sun都收购了之后,Java SE JVM只能二选一,JRockit就炮灰了。
JRockit最后发布的大版本是R28,只到JDK6;原本在开发中的R29及JDK7的对应功能都没来得及完成项目就被终止了。

垃圾回收机制

1.前言

理解Java虚拟机垃圾回收机制的底层原理,是系统调优与线上问题排查的基础,也是一个高级Java程序员的基本功,本文就针对Java垃圾回收这一主题做一些整理与记录。Java垃圾回收器的种类繁多,它们的设计要在吞吐量(内存空间)与实时性(用户线程中断)方面进行权衡,各个垃圾回收器的适应场景也不尽相同(如:桌面应用,web应用),因此,这里我们只讨论JDK8下的默认垃圾回收器,毕竟目前JDK8版本是业界的主流(占80%),并且我们只讨论堆内存空间的垃圾回收。

JDK8下的默认垃圾回收器:UseParallelGC : Parallel (新生代)+ (老年代)堆内存回收机制

2.什么是垃圾?

在 JVM 进行垃圾回收之前,首先就是判断哪些对象是垃圾,也就是说,要判断哪些对象是可以被销毁的,其占有的空间是可以被回收的。根据 JVM 的架构划分,我们知道, 在 Java 世界中,几乎所有的对象实例都在堆中存放,所以垃圾回收也主要是针对堆来进行的。

在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?因此对于垃圾回收,判断并标识对象是否可回收是第一步。从理论层面来说,判断对象是否可回收一般两种方法。这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。

第一种引用计数器算法:每当对象被引用一次计数器加1,对象失去引用计数器减1,计数器为0是就可以判断对象死亡了。这种算法简单高效,但是对于循环引用或其他复杂情况,需要更多额外的开销,因此Java几乎不使用该算法。

第二种跟搜索算法-可达性分析算法:所谓可达性分析是指,顺着GCRoots根一直向下搜索(用一个成语概括就是“顺藤摸瓜”),整个搜索的过程就构成了一条“引用链”,只要在引用链上的对象叫做可达,在引用链之外的(说明跟GCRoots没有任何关系)叫不可达,不可达的对象就可以判断为可回收的对象。 哪些对象可作为GCRoots对象呢? 包括如下:

  • 虚拟机栈帧上本地变量表中的引用对象(方法参数、局部变量、临时变量)

  • 方法区中的静态属性引用类型对象、常量引用对象

  • 本地方法栈中的引用对象(Native方法的引用对象)

  • Java虚拟机内部的引用对象,如异常对象、系统类加载器等

  • 所有被同步锁(synchronize)持有的对象

  • Java虚拟机内部情况的注册回调、本地缓存等

如果对虚拟机的内存布局与运行流程有所了解的话,这些作为GCRoots都很好理解,它们是程序运行时的源头,程序的正常运行必须依赖它们,而与这些源头没有任何关系的对象,即可视为可回收对象。就好比“瓜从藤上掉下来了, 那这瓜肯定也没有用了”

在这里插入图片描述

GCRoots可达性分析 不可达对象

在这里插入图片描述

可达性分析

可达性分析从理论上很好理解,但在垃圾收集器具体运行时,要考虑的问题不知道要复杂多少倍,因为在可达性分析的同时,程序也是在并行运行着,整个内存堆的状态随着程序的运行是实时变化的,要实现分析结果与内存状态的一致性,就必须要暂停用户线程,在一个快照去进行分析。

3.垃圾回收算法

可达性分析解决了判断对象是否可回收的问题,那么在垃圾回收时内存空间会发生哪些变化呢?这就是垃圾回收算法要讨论的问题,我们根据算法对内存采取的不同操作,可将垃圾回收算法分为3种,标记-清除算法、标记-复制算法、标记-整理算法。

3.1 标记-清除算法

根据名称就可以理解改算法分为两个阶段:首先标记出所有需要被回收的对象,然后对标记的对象进行统一清除,清空对象所占用的内存区域,下图展示了回收前与回收后内存区域的对比,红色的表示可回收对象,橙色表示不可回收对象,白色表示内存空白区域。
在这里插入图片描述

标记-清除算法 垃圾回收前后内存区域对比

标记-清除缺点

  • 第一个:是执行效率不可控,试想一下如果堆中大部分的对象都可回收的,收集器要执行大量的标记、收集操作。

  • 第二个:产生了许多内存碎片,通过回收后的内存状态图可以知道,被回收后的区域内存并不是连续的,当有大对象要分配而找不到满足大小的空间时,要触发下一次垃圾收集。

3.2 标记-复制算法

对标记-清除算法执行效率与内存碎片的缺点,计算机科学家又提出了一种“半复制区域”的算法。

标记-复制算法将内存分为大小相同的两个区域,运行区域,预留区域,所有创建的新对象都分配到运行区域,当运行区域内存不够时,将运作区域中存活对象全部复制到预留区域,然后再清空整个运行区域内存,这时两块区域的角色也发生了变化,每次存活的对象就像皮球一下在运行区域与预留区域踢来踢出,而垃圾对象会随着整个区域内存的清空而释放掉,内存前后的状态参考下图:

在这里插入图片描述

标记-复制算法回收前后内存对比

3.3 标记-整理算法

标记-复制算法要浪费一半内存空间,且在大多数状态为存活状态时使用效率会很低,针对这一情况计算机科学家又提出了一种新的算法“标记-整理算法”,标记整理算法的标记阶段与其他算法一样,但是在整理阶段,算法将存活的对象向内存空间的一端移动,然后将存活对象边界以外的空间全部清空,如下图所示:

在这里插入图片描述

标记-整理算法回收前后内存对比

标记-整理算法

标记整理算法解决了内存碎片问题,也不存在空间的浪费问题,看上去挺美好的。但是,当内存中存活对象多,并且都是一些微小对象,而垃圾对象少时,要移动大量的存活对象才能换取少量的内存空间。

不同的垃圾回收算法都有各自的优缺点,适应于不同的垃圾回收场景

4.新生代、老年代堆内存结构

Java 堆内存空间新生代、老年代是如何划分的?对象创建后是如何分配到不同的区域的?结合下图可以知道,整个堆内存被分为了2个大的区域,新生代,老年代,默认情况下新生代占1/3的空间,老年代占2/3的空间,新生代又分为两个区 Eden区Survial区,Survial又分为S0、S1区 默认各占8/10与1/10,1/10的空间
在这里插入图片描述

年轻代 老年代 堆空间结构

为什么要这么设计呢?为什么要分那么多不同的内存区域干嘛?这是由对象的生命周期特征、与各类垃圾回收算法的优缺点所决定的,这正式垃圾回收器设计的理论基础。经过统计分析,大多数应用程序对象生命周期符合两个特征:

垃圾回收的理论基础:

  • 绝大多数的对象都是“朝生夕灭”的,既创建不久即可消亡。
  • 熬过越多此垃圾回收过程的对象就越难以消亡。

一块独立的内存区域只能使用一种回收算法,根据对象生命周期特征,将其划分到不同的区域,再对特定区域使用特定的垃圾回收算法,只有这样才能将垃圾算法的优点发挥到极致,这种组合的垃圾回收算法叫:分代垃圾算法。比如:在新生代使用标记-复制算法,在老年代使用标记-整理算法。

5.堆内存回收过程详解

我们分析了如何判断对象是否可回收,还有3中基础的垃圾回收算法,以及年轻代、老年代的内存区域划分与原因。接下来我们就一步一步来分析堆内存的回收流程。

5.1 内存初始状态

假设在第一垃圾回收之前,内存中的状态如图所示,Eden区有2个存活对象3个垃圾对象,内存的可用区域已经所剩无几,Survivor区因为还没有进行任何MinorGC所以是空的,有1个大对象直接分配到了老年代,
在这里插入图片描述

垃圾回收初始状态

5.2.第1次执行MinorGC(轻GC)后状态

当新的对象分配到Eden区,发现内存空间不够,于是触发第一次MinorGC,垃圾回收器首先将Edne区中的两个存活对象复制到S0区,然后在清空Eden区的空间,如下图:
在这里插入图片描述

第一次MinorGC内存状态

5.3.程序运行一段时间后状态

经过第1次MinorGC程序再运行一段时间后,堆内存状态如下:Eden区又产生了大量的对象,并且大部分对象都可回收状态,这也符合对象“朝生夕灭”的特征,S0区中也有1个对象可以回收,S1与老年代没有变化,在这种状态下,如果新对象分配再次触发MinorGC会发生什么呢?
在这里插入图片描述

程序运行一度时间后的状态

5.4.执行第2次MinorGC后状态

新对象分配Eden区空间不足,又触发了第二次MinorGC,第二次MinorGC与第一次GC时在Eden区的操作是一样的:将Eden区存活的对象复制到S1区,然后在清空整个Eden区,同时也将S0区存活的对象复制到S1区并将对象的年龄加1,再清空S0区,GC后的状态如下图所示:
在这里插入图片描述

执行第二次MinorGC后状态

5.5.第2MinorGC程序运行一段时间后状态

经过第二MinorGC后程序又运行了一段时间,Eden区中有生成了很多对象,S1区也有一个对象可回收。
在这里插入图片描述

第二MinorGC程序运行一段时间后状态

5.6.第15MinorGC后内存状态

在接下来的每次MinorGC时,都是第二次一样,从Eden区和survivor非空白区移动存活对象到survivor区中空白区域,并清空这两个区域内存空间,存活对象每此从survivor两个区域移动一次,对象年龄加1,下图表示经过了15次MinorGC后的堆内存状态。
在这里插入图片描述

经过15次MinorGC后的内存状态

对于年轻代区域的内存收集,使用的是标记-复制算法,只是为了减少复制算法空白区域的内存浪费,并不是将内存一份为二,而是巧妙的将内存分为三个区域,预留的空白区域只占整个年轻代区域的1/10。

5.7.对象如何进入老年代

以上是年轻代的分配与回收问题,那对象如何进入老年代呢?个人认为对象进入老年代,可以分为2种类型6种情况。

在这里插入图片描述

对象晋升入老年代

第一种类型–直接分配:对象创建时直接分配到老年代具体分为3种情况。

  • 超过虚拟机PretenureSizeThreshold参数设置大小的对象,该参数的默认值是0,也就是说任何大小的对象都会先分配到Eden区。

  • 超过Eden大小的对象

  • 如果新生代分配失败,一个大数组或者大字符串

第二种类型–从年轻代晋升:从年轻代空间晋升到老年代也可分为3种情况。

  • 新生代分配担保,在执行MinorGC时要将Eden区存活的对象复制到Survivor区,但是Survivor区默认空间是只有新生代的2/10,实际使用的只有1/10,当Survivor区内存不够所有存活对象分配时,就需要将Survivor无法容纳的对象分配到老年代去,这种机制就叫分配担保。

  • 对象年龄超过虚拟机MaxTenuringThreshold的设置值,最大为15,

  • Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半(TargetSurvivorRatio),年龄大于或等于该年龄的对象直接进入老年代。

5.8.内存分配担保机制

在执行MinorGC时要将Eden区存活的对象复制到Survivor区,但是Survivor区默认空间是只有新生代的2/10,实际使用的只有1/10,当Survivor区内存不够所有存活对象分配时,就需要Survivor无法容纳将对象分配到老年代空间中,这种机制就叫分配担保,但是,老年代的空间也是有限的,如果老年代中空间也不够的话,那只能乖乖的执行一次FullGC了。

5.9.老年代回收算法-FullGC

当有对象要进入老年代,而老年代空间又不足时就会触发FullGC,当然,反过来说触发FullGC的条件不仅仅只是老年代空间不足,FullGC使用的算法是上面说的标记-整理算法。
在这里插入图片描述

完整堆内存回收过程

6.总结

  • 判断对象是否可以回收是垃圾回收的基础与前提,通过可达性分析从GCRoots开始进行"顺藤摸瓜"找到不可达对象(可回收)

  • 对象生命周期的特征"朝生夕灭"与"越战越强"是垃圾回收算法的理论基础

  • 基础的垃圾回收算法有3种分别是 标记-清除算法、标记-负责算法、标记整理算法,都有各自的适应场合与优缺点

  • 分代垃圾算法根据对象生命周期的特征,将其划分到不同的区域,从而使用最适合的垃圾算法来进行优化

  • 在JDK8默认的配置下使用 新生代,老年代的垃圾回收策略,新生代区域使用标记-复制算法,老年代区域使用标记-整理算法

JVM调优工具

1.jps

jps 命令用于查看正在运行的Java进程,必须保证jdk正确的安装及环境变量的配置

2.jinfo

# 查看jvm的参数
jinfo -flags 进程号
# 查看java系统参数
jinfo -sysprops 进程号

3.jstat

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。

3.1.类加载统计

jstat -class 进程号

Loaded:加载class的数量

Bytes:所占用空间大小

Unloaded:未加载数量

Bytes:未加载占用空间

Time:时间

3.2.垃圾回收统计

jstat -gc 进程号

S0C:第一个幸存区的大小

S1C:第二个幸存区的大小

S0U:第一个幸存区的使用大小

S1U:第二个幸存区的使用大小

EC:伊甸园区的大小

EU:伊甸园区的使用大小

OC:老年代大小

OU:老年代使用大小

MC:方法区大小(元空间)

MU:方法区使用大小

CCSC:压缩类空间大小

CCSU:压缩类空间使用大小

YGC:年轻代垃圾回收次数

YGCT:年轻代垃圾回收消耗时间

FGC:老年代垃圾回收次数

FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间

3.3.堆内存统计

jstat -gccapacity 进程号

NGCMN:新生代最小容量(New Generation Capacity Min)

NGCMX:新生代最大容量

NGC:当前新生代容量

S0C:第一个幸存区大小

S1C:第二个幸存区的大小

EC:伊甸园区的大小

OGCMN:老年代最小容量

OGCMX:老年代最大容量

OGC:当前老年代大小

OC:当前老年代大小

MCMN:最小元数据容量

MCMX:最大元数据容量

MC:当前元数据空间大小

CCSMN:最小压缩类空间大小

CCSMX:最大压缩类空间大小

CCSC:当前压缩类空间大小

YGC:年轻代gc次数

FGC:老年代GC次数

3.4.新生代垃圾回收统计

jstat -gcnew 进程号

S0C:第一个幸存区的大小

S1C:第二个幸存区的大小

S0U:第一个幸存区的使用大小

S1U:第二个幸存区的使用大小

TT:对象在新生代存活的次数

MTT:对象在新生代存活的最大次数

DSS:期望的幸存区大小

EC:伊甸园区的大小

EU:伊甸园区的使用大小

YGC:年轻代垃圾回收次数

YGCT:年轻代垃圾回收消耗时间

3.5.新生代内存统计

jstat -gcnewcapacity 进程号

NGCMN:新生代最小容量

NGCMX:新生代最大容量

NGC:当前新生代容量

S0CMX:最大幸存1区大小

S0C:当前幸存1区大小

S1CMX:最大幸存2区大小

S1C:当前幸存2区大小

ECMX:最大伊甸园区大小

EC:当前伊甸园区大小

YGC:年轻代垃圾回收次数

FGC:老年代回收次数

3.6.老年代垃圾回收统计

jstat -gcold 进程号

MC:方法区大小

MU:方法区使用大小

CCSC:压缩类空间大小

CCSU:压缩类空间使用大小

OC:老年代大小

OU:老年代使用大小

YGC:年轻代垃圾回收次数

FGC:老年代垃圾回收次数

FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间

3.7.老年代内存统计

jstat -gcoldcapacity 进程号

OGCMN:老年代最小容量

OGCMX:老年代最大容量

OGC:当前老年代大小

OC:老年代大小

YGC:年轻代垃圾回收次数

FGC:老年代垃圾回收次数

FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间

3.8.元空间统计

jstat -gcmetacapacity 进程号

MCMN:最小元数据容量

MCMX:最大元数据容量

MC:当前元数据空间大小

CCSMN:最小压缩类空间大小

CCSMX:最大压缩类空间大小

CCSC:当前压缩类空间大小

YGC:年轻代垃圾回收次数

FGC:老年代垃圾回收次数

FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间

3.9.堆内存统计(百分比)

jstat -gcutil 进程号

S0:幸存1区当前使用比例

S1:幸存2区当前使用比例

E:伊甸园区使用比例

O:老年代使用比例

M:元数据区使用比例

CCS:压缩使用比例

YGC:年轻代垃圾回收次数

FGC:老年代垃圾回收次数

FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间

4.jmap

jmap可以用来查看内存信息

4.1.查看实例个数及占用内存大小
jmap -histo 进程号 > 保存的文件路径
#jmap -histo 2334 > d:/log.txt

num:序号

instances:实例数量

bytes:占用空间大小

class name:类名称

4.2.查看堆信息
jmap -heap 进程号
4.3.堆内存dump
# live表示活着的对象  format表示输出格式 file指定文件路径
jmap -dump:live,format=b,file=保存路径 进程号
# jmap -dump:format=b,file=d:/test.hprof 2345

也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=./ (路径)

可以用jvisualvm命令工具导入该dump文件分析

5.jstack

jstack可以查找死锁

jstack 进程号

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。

1 Serial收集器

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 单线程的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EFKUzALa-1677078460754)(F:\阿里云下载文件\2022-10-14 jvm调优\1.JVM调优\课堂内容\imgs\JVM13.jpg)]

虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。

2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。

新生代采用复制算法,老年代采用标记-整理算法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybNnZc1R-1677078460755)(F:\阿里云下载文件\2022-10-14 jvm调优\1.JVM调优\课堂内容\imgs\JVM14.jpg)]

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

并行和并发概念补充:

并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适合科学计算、后台处理等弱交互场景。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。适合Web应用。

3 Parallel Scavenge收集器

Parallel Scavenge 收集器类似于ParNew 收集器,是Server 模式(内存大于2G,2个cpu)下的默认收集器,那么它有什么特别之处呢?

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用复制算法,老年代采用标记-整理算法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGtsUQOp-1677078460756)(F:\阿里云下载文件\2022-10-14 jvm调优\1.JVM调优\课堂内容\imgs\JVM15.jpg)]

4.Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

6 CMS收集器

(-XX:+UseConcMarkSweepGC(主要是old区使用))

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

初始标记: 暂停所有的其他线程(STW),并记录下直接与root相连的对象,速度很快 ;

并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

并发清除: 开启用户线程,同时GC线程开始对未标记的区域做清扫。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5f2rHlEw-1677078460756)(F:\阿里云下载文件\2022-10-14 jvm调优\1.JVM调优\课堂内容\imgs\JVM16.jpg)]

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

对CPU资源敏感(会和服务抢资源);

无法处理浮动垃圾(在java业务程序线程与垃圾收集线程并发执行过程中又产生的垃圾,这种浮动垃圾只能等到下一次gc再清理了);

它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

CMS的相关参数

-XX:+UseConcMarkSweepGC 启用cms

-XX:ConcGCThreads:并发的GC线程数(并非STW时间,而是和服务一起执行的线程数)

-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩(减少碎片)

-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次(因压缩非常的消耗时间,所以不能每次FullGC都做)

-XX:CMSInitiatingOccupancyFraction:触发FulGC条件(默认是92)

-XX:+UseCMSInitiatingOccupancyOnly:是否动态调节

-XX:+CMSScavengeBeforeRemark:FullGC之前先做YGC(一般这个参数是打开的)

-XX:+CMSClassUnloadingEnabled:启用回收Perm区(jdk1.7及以前)

7 G1收集器

(-XX:+UseG1GC)

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L04Ns3W8-1677078460757)(F:\阿里云下载文件\2022-10-14 jvm调优\1.JVM调优\课堂内容\imgs\JVM17.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d8sdz6Zp-1677078460758)(F:\阿里云下载文件\2022-10-14 jvm调优\1.JVM调优\课堂内容\imgs\JVM18.jpg)]

G1将Java堆划分为多个大小相等的独立区域(Region),虽保留新生代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。

分配大对象(直接进Humongous区,专门存放短期巨型对象,不用直接进老年代,避免Full GC的大量开销)不会因为无法找到连续空间而提前触发下一次GC。

被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备以下特点:

并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内完成垃圾收集。

G1收集器的运作大致分为以下几个步骤:

初始标记(initial mark,STW):在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。

并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。

最终标记(Remark,STW):该阶段是 STW 回收,帮助完成标记周期。

筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6qzaRX2-1677078460759)(F:\阿里云下载文件\2022-10-14 jvm调优\1.JVM调优\课堂内容\imgs\JVM19.jpg)]

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了Gc收集器在有限时间内可以尽可能高的收集效率。

G1垃圾收集分类

YoungGC

新对象进入Eden区

存活对象拷贝到Survivor区

存活时间达到年龄阈值时,对象晋升到Old区

MixedGC

不是FullGC,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)

global concurrent marking (全局并发标记)

Initial marking phase:标记GC Root,STW

Root region scanning phase:标记存活Region

Concurrent marking phase:标记存活的对象

Remark phase :重新标记,STW

Cleanup phase:部分STW

相关参数

G1MixedGCLiveThresholdPercent Old区的region被回收的时候的存活对象占比

G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数

G1OldCSetRegionThresholdPercent 一次Mixed GC中能被选入CSet的最多old区的region数量

触发的时机

InitiatingHeapOccupancyPercent:堆占有率达到这个值则触发global concurrent marking,默认45%

G1HeapWastePercent:在global concurrent marking结束之后,可以知道区有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到了此参数,只有达到了,下次才会发生Mixed GC

JVM实战调优

JVM调优主要就是调整下面两个指标

停顿时间: 垃圾收集器做垃圾回收中断应用执行的时间。-XX:MaxGCPauseMillis

吞吐量:花在垃圾收集的时间和花在应用时间的占比**-XX:GCTimeRatio=,垃圾收集时间占比: 1/(1+n)**

GC调优步骤

打印GC日志

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:d:/gc.log

GCeasy网站分析日志得到关键性指标

分析GC原因,调优JVM参数

1、Parallel Scavenge收集器(默认)

分析parallel-gc.log

第一次调优,设置Metaspace大小:增大元空间大小-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M

第二次调优,添加吞吐量和停顿时间参数:-XX:MaxGCPauseMillis=100 -XX:GCTimeRatio=99

第三次调优,修改动态扩容增量:-XX:YoungGenerationSizeIncrement=30

2、配置CMS收集器

-XX:+UseConcMarkSweepGC

分析cms-gc.log

3、配置G1收集器

-XX:+UseG1GC

分析g1-gc.log

查看发生MixedGC的阈值:jinfo -flag InitiatingHeapOccupancyPercent 进程id

分析工具:gceasy,GCViewer

G1调优相关

常用参数

-XX:+UseG1GC 开启G1

-XX:G1HeapRegionSize=n,region的大小,1-32M,2048个

-XX:MaxGCPauseMillis=200 最大停顿时间

-XX:G1NewSizePercent -XX:G1MaxNewSizePercent

-XX:G1ReservePercent=10 保留防止to space溢出()

-XX:ParallelGCThreads=n SWT线程数(停止应用程序)

-XX:ConcGCThreads=n 并发线程数=1/4*并行

最佳实践

年轻代大小:避免使用-Xmn、-XX:NewRatio等显示设置Young区大小,会覆盖暂停时间目标(常用参数3)

暂停时间目标:暂停时间不要太严苛,其吞吐量目标是90%的应用程序时间和10%的垃圾回收时间,太严苛会直接影响到吞吐量

是否需要切换到G1

50%以上的堆被存活对象占用

对象分配和晋升的速度变化非常大

垃圾回收时间特别长,超过1秒

G1调优目标

6GB以上内存

停顿时间是500ms以内

吞吐量是90%以上

GC常用参数

堆栈设置

-Xss:每个线程的栈大小

-Xms:初始堆大小,默认物理内存的1/64

-Xmx:最大堆大小,默认物理内存的1/4

-Xmn:新生代大小

-XX:NewSize:设置新生代初始大小

-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。

-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。

-XX:MetaspaceSize:设置元空间大小

-XX:MaxMetaspaceSize:设置元空间最大允许大小,默认不受限制,JVM Metaspace会进行动态扩展。

垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

收集器设置

-XX:+UseSerialGC:设置串行收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseParallelOldGC:老年代使用并行回收收集器

-XX:+UseParNewGC:在新生代使用并行收集器

-XX:+UseParalledlOldGC:设置并行老年代收集器

-XX:+UseConcMarkSweepGC:设置CMS并发收集器

-XX:+UseG1GC:设置G1收集器

-XX:ParallelGCThreads:设置用于垃圾回收的线程数

并行收集器设置

-XX:ParallelGCThreads:设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis:设置并行收集最大暂停时间

-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

CMS收集器设置

-XX:+UseConcMarkSweepGC:设置CMS并发收集器

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。

-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩

-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收

-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况

-XX:ParallelCMSThreads:设定CMS的线程数量

-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发

-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

G1收集器设置

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads:指定GC工作的线程数量

-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区

-XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集

-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)

-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)

-XX:G1MaxNewSizePercent:新生代内存最大空间

-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)

-XX:MaxTenuringThreshold:最大任期阈值(默认15)

-XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集

-XX:G1HeapWastePercent:堆废物百分比(默认5%)

-XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值