JVM(基础篇)

发布于:2025-03-24 ⋅ 阅读:(29) ⋅ 点赞:(0)

一.初识JVM

1.什么是JVM

        JVM全称Java Virtyal Machine,中文译名 Java虚拟机 。JVM本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件(将字节码解释成机器码)

2.JVM的功能
  • 解释和运行:对字节码文件中的指令号,实时的解释成机器码,让计算机执行。

  • 内存管理:自动为对象、方法等分配内存;自动的垃圾回收机制,回收不再使用的对象。

  • 即时编译:对热点代码进行优化,提升执行效率。

2.1即时编译

Java需要实时解释,主要是为了支持跨平台特性。

        由于JVM需要实时解释虚拟机指令,不做任何优化性能不如直接运行机器码的C、C++等语言。所以 JVM提供了即时编译(Just-In-Time 简称JIT) 进行性能的优化,最终能达到接近C、C++语言的运行性能甚至在特定场景下实现了超越。

3.常见的JVM

3.1Java虚拟机规范

《Java虚拟机规范》由Oracle制定,内容主要包含了Java虚拟机在设计和实现时需要遵守的规范,主要包含class字节码文件的定义类和接口的加载和初始化指令集等内容。《Java虚拟机规范》是对虚拟机设计的要求,而不是对Java设计的要求,也就是说虚拟机可以运行在其他的语言比如Groovy、Scala生成的class字节码文件之上。

官网地址:Java SE SpecificationsJava SE Specifications Java SE Specifications

3.2HotSpot的发展历程

二.字节码文件详解

1.JVM的组成

2.字节码文件的组成

2.1基础信息
  1. Magic魔数

        文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错。Java字节码文件中,将文件头称为magic魔数

  1. 主副版本号

主副版本号指的是编译字节码文件的 JDK 版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46。之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容

注意:1.2之后大版本号的计算方法就是:主版本号 - 44。比如主版本号是52就是JDK8

主版本号不兼容导致的错误:

两种解决方案:

1.升级JDK版本(容易引发其他的兼容性问题,并且需要大量测试)

2.将第三方依赖的版本号降低或者更换依赖,以满足JDK版本需要(建议采用)

2.2常量池

                常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速找到对应的数据。字节码指令中通过编号引用到常量池的过程称之为符号引用

2.3方法

        字节码中的方法区是存放字节码指令的核心位置,字节码指令的内容存放在方法的code属性中。

操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置。

案例:

1.

2.

3.

2.4常用工具
  1. javap -v/verbose命令
    a. javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。
    b. 直接输入javap查看所有参数
    c. 输入javap -v 字节码文件名称() 查看具体的字节码文件。(如果jar包需要先使用jar -xvf 命令)

javap -v D:\_MyFile\进阶\JVM\code\jvm\target\classes\com\gty\jvm\part1\MethodDemo.class

2. jclasslib插件 jclasslib也有Idea插件版本,建议开发时使用Idea插件版本,可以在代码编译之后实时看到字节码文件内容
a. 鼠标点击要查看的class文件

b. 点击View中的 Show Bytecode With Jclasslib

3. 阿里arthas

        Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。

官网:简介 | arthas

a. dump 类的全限定名:dump已加载类的字节码文件到特定目录。

b. jad类的全限定名:反编译已加载类的源码。

使用方法:

 curl -O https://arthas.aliyun.com/arthas-boot.jar    --->  首次需执行
 java -jar arthas-boot.jar
 ​
 然后选择要选择的进程号等待arhas项目启动,就可以进行操作(classloader等等)

三.类的生命周期

1.生命周期概述

类的生命周期

2.加载阶段
2.1加载阶段
  1. 加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。程序员可以使用Java代码拓展的不同的渠道。(本地文件:磁盘上的字节码文件、动态代理生成:程序运行时使用动态代理生成、通过网络传输的类:早期的Applet技术使用)

  2. 类加载器获取不同渠道的信息

  3. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中。生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。

  4. 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)。

    注:对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。这样Java虚拟机就能很好地控制开发者访问数据的范围。

    2.2查看内存中的对象

    使用 JDK自带的hsdb工具查看Java虚拟机内存信息。工具位于JDK安装目录下lib文件夹中的sa-jdi.jar中。 启动命令:java -cp sa-jdi.jar sun.jvm.hotspot.HSDB

    查看java程序进程号:

    3.连接阶段

    3.1验证

    验证的主要目的是检测Java字节码文件是否遵守《Java虚拟机规范》中的约束,这个阶段一般不需要程序员参与。主要包含以下四个部分,

    1. 文件格式验证,比如文件是否以0xCAFEBABE(魔数)开头,主次版本号是否满足当前Java虚拟机版本要求

    2. 元信息验证,例如类必须有父类(super不能为空)。

    3. 验证程序执行指令的语义,比如方法内的指令执行中跳转到不正确的位置。

    4. 符号引用验证,例如是否访问了其他类中private的方法等。

    案例:版本号的检测

    结论:主版本号不能高于运行环境主版本号,如果主版本号相等,副版本号也不能超过。

    3.2准备
    1. 准备阶段为静态变量(static)分配内存并设置初始值。 (1) 准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。

      (2) final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。

             

    3.3解析
    1. 解析阶段主要是将常量池中的符号引用替换为直接引用。

    2. 符号引用就是在字节码文件中使用编号来访问常量池中的内容。

    3. 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。

    4.初始化阶段

    初始化阶段会执行静态代码块中的代码,并为静态变量赋值。在字节码文件中即为执行clinit部分的字节码指令。

    源码:                                                                                   字节码文件中的方法信息:

    字节码指令:clinit方法中的指令执行顺序与java中的编写顺序是一致的。

    • 以下几种方式会导致类的初始化:

      1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。

      2. 调用Class.forName(String className)。

      3. new一个该类的对象时。

      4. 执行Main方法的当前类。

      添加 -XX:+TraceClassLoading 参数可以打印出加载并初始化的类

    Test1:

    • clinit指令在特定情况下不会出现:

      1. 无静态代码块且无静态变量赋值语句

      2. 有静态变量的声明,但是没有赋值语句

      3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化

    • 继承关系中 Demo02.java

      1. 直接访问父类的静态变量,不会触发子类的初始化。

      2. 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

    四.类加载器

    1. 定义:类加载器(ClassLoader)是 Java 虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。类加载器只参与加载过程中的字节码获取并加载到内存这一部分。

    2. 类加载器的应用场景:

      • 企业级应用:SPI机制、类的热部署、Tomcat类的隔离

      • 大量的面试题:什么是类的双亲委派机制、打破类的双亲委派机制、自定义类加载器

      • 解决线上问题:使用 Arthas 不停机解决线上故障

    1.类加载器的分类

    类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。

    1. Java:

      • JDK中默认提供或者自定义:JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求定制。

      • 继承抽象类ClassLoader:所有Java中实现的类加载器都需要继承ClassLoader这个抽象类。

    2. 虚拟机底层实现:

      • 虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言与虚拟机底层语言一致,比如 Hotspot 使用C++。

      • 加载程序运行时的基础类:保证Java程序运行中基础类被正确地加载,比如 java.lang.String,确保其可靠性。

      类加载器的设计JDK8和8之后的版本差别较大,JDK8之后的版本中默认的类加载器有如下几种:

      1. 虚拟机底层实现:启动类加载器(Bootstrap),加载Java中最核心的类。

      2. Java:扩展类加载器(Extension),允许扩展Java中比较通用的类;应用程序类加载器(Application),加载应用使用的类。

      Arthas中类加载器的相关功能:类加载器的详细信息可以通过 classloader 命令查看

      1. 启动类加载器

      启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。默认加载Java安装目录/jre/lib下的类文件,比如rt.jartools.jarrescources.jar等。

      通过启动类加载器去加载用户jar包:

      • 放入jre/lib下进行扩展:不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即使放进去,但可能由于文件名不匹配的问题也不会正常地被加载。

      • 使用参数进行扩展:推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展。

      1. Java默认的类加载器:

      扩展类加载器应用程序加载器都是JDK提供的,使用Java编写的类加载器。他们的源码都位于sun.misc.Launcher中,是一个静态内部类,继承自URLClassLoader。具备通过目录或指定jar包将字节码文件加载到内存中。

      • 扩展类加载器:默认加载Java安装目录下/jre/lib/ext下的类文件。不推荐放入 /jre/lib/ext下进行扩展,尽可能不要去更改JDK安装目录中的内容。推荐使用 -Djava.ext.dirs=jar包目录进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录。 如果目录中有特殊字符比如空格这种,需要将所有的目录用用引号引起来。

        类加载器的加载路径可通过classloader -c hash值 查看:

      • 应用程序加载器

    2.双亲委派机制
    1. 定义:当加载一个类时,他总是先让他的父级类加载器去加载,确保系统中的类优先加载。直到父类加载器找不到类时,在逐级向下让子级类加载器加载,如果找不到,最终会抛异常 ClassNotFoundException。这样做是为了防止。我们自己定义的类替换了系统中的类。

      总结来说就是:自底向上查找是否加载过,再由顶向下进行加载。

    2. 类加载器的层级关系:

      每个Java实现的类加载器中保存了一个成员变量叫 “父”(Parent)类加载器,可以理解为它的上级,并不是继承关系。

      应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是null。这是因为启动类加载器使用C++编写,不属于Java的类加载器。而启动类加载没有上级类加载器。

    3. Arthas中的相关功能

      类加载器的继承关系可以通过classloader-t查看:

    4. 双亲委派机制解决的三个问题:

      • 重复的类:如果一个类出现在三个类加载的加载位置,则会被启动类加载器加载,根据双亲委派机制,他的优先级是最高的。

      • 自己创建的String类:自己创建的String类是不能被加载的,最终会交由启动类加载器加载在rt.jar包中的String类。

      • 类加载器的关系:应用类加载器的父类加载器是扩展,扩展类加载器没有父类加载器,但是会委派给启动类加载器加载。

    3.打破双亲委派机制
    3.1自定义类加载器

    例如Tomcat服务器,一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。如果不打破双亲委派机制,当应用类加载器加载Web应用1中的Servlet之后,Web应用2中相同限定名的MyServlet类就无法被加载了。而Tomcat则使用了自定义类加载器来实现应用之间类的隔离,每一个应用会有一个独立的类加载器加载对应的类。

    1. ClassLoader的原理:主要包含了四个核心方法

       /*
           类加载的入口,提供了双亲委派机制,内部会调用findClass
       */
       public Class<?> loadClass(String name) 
           
       /*
           由加载器子类实现,获取二进制数据调用defineClass,比如URLCLassLOader会根据文件路径去获取类文件中的二进制数据
       */
       protected Class<?> findClass(String name)
           
        /*
           做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
        */
       protected final Class<?> defineClass(byte[] b, int off, int len)
       ​
       /*
           执行类生命周期的连接阶段
       */
       protected final void resolveClass(Class<?> c)
    2. 自定义加载器的父类加载器:在不手动设置父类加载器时,自定义加载器的父类加载器为应用程序类加载器

       // 在JDK8中,ClassLoader类中提供了构造方法设置parent的内容
       private ClassLoader(Void unused, ClassLoader parent) {
           this.parent = parent;
           if (ParallelLoaders.isRegistered(this.getClass())) {
               parallelLockMap = new ConcurrentHashMap<>();
               package2certs = new ConcurrentHashMap<>();
               assertionLock = new Object();
           } else {
               // no finer-grained lock; lock on the classloader instance
               parallelLockMap = null;
               package2certs = new Hashtable<>();
               assertionLock = this;
           }
       }
       /*
           这个构造方法由另外一个构造方法调用,其中父类加载器由由getSystemClassLoader方法设置,该方法返回的是AppClassLoader
       */
       protected ClassLoader() {
           this(checkCreateClassLoader(), getSystemClassLoader());
       }
    3. 两个自定义类加载器加载相同限定名的类:两个自定义加载器加载相同限定名的类是不会冲突的,在同一Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。

      在Arthas中可使用sc -d 类名的方式查看具体的情况:两个classloaderHash是不同的,说明两个自定义加载器不是相同的类加载器,并且加载出的类的实例也是不同的。

    3.2线程上下文类加载器

    打破双亲委派机制,可通过线程上下文类加载器 Thread.currentThread().setContextClassLoader() 来实现。通过改变线程的上下文类加载器,使得当前线程的类加载器优先加载某些类,而不是依赖父加载器。

    JDBC案例:JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,而DriverManager类位于rt.jar包中,由启动类加载器加载。但是依赖中,mysql驱动对应的类,由应用程序类加载器来加载。这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,这就违反了双亲委派机制。

    DriverManager使用SPI机制,最终加载jar包中对应的驱动类。在SPI中使用线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。

    3.3Osgi框架的类加载器

    历史上,OSGi模块化框架,它允许同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的功能。热部署指的是,在服务不停止的情况下,动态地更新字节码文件到内存中。

    热部署思路:

    ① 在出问题的服务器上部署一个 arthas,并启动。

    ② jad --source-only 类全限定名 > 目录/文件名.java ​ jad 命令反编译,然后可以用其它编译器,比如vim 来修改源码

    ③ mc -c类加载器的hashcode 目录/文件名.java -d 输出目录 ​ mc 命令用来编译修改过的代码

    ④ retransform class文件所在目录/xxx.class ​ 用 retransform 命令加载新的字节码

    注意事项:

    ① 程序重启之后,字节码文件就会恢复,除非将class文件放入jar包中进行更新

    ② 使用 retransform 不能添加方法或者字段,也不能更新正在执行中的方法

    4.JDK9之后的类加载器

    JDK8以及之前的版本中,扩展类加载器和应用程序类加载器的源码位于 rt.jar 包中sun.misc.Launcher.java。

    由于JDK9之后引入了module概念,类加载器在设计上发生了很多变化。

    1. 启动类加载器使用 Java 编写,位于 jdk.internal.loader.ClassLoader 类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。启动类加载器依然无法通过Java代码获取到,返回的仍然是null,与JDK8之前保持了统一

    2. 扩展类加载器被替换成了平台类加载器(Platform Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltClassLoader,BuiltClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。

    五.运行时数据区

            Java虚拟机在运行Java程序过程中管理的内存区域,称为运行时数据区。运行数据区按照线程共不共享分为两部分。

    • 线程不共享:这些区域中的数据是每个线程私有的,不同线程之间的数据不会互相影响。

      • 程序计数器

      • Java虚拟机栈

      • 本地方法栈

    • 线程共享:这些区域中的数据是多个线程共享的,即所有的线程都可以访问这些区域。

      • 方法区

    1. 程序计数器
    1. 定义:程序计数器(Program Counter Register)也叫PC寄存器,用于跟踪程序的执行状态。每个线程都有独立的程序计数器,用来记录当前线程要执行的字节码指令的地址。因为每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。也不存在垃圾回收,程序员无需对程序计数器做任何的处理

    2. 案例:

    3. 作用:

      1. 在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令。程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。

      2. 在多线程执行情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到哪一句指令并继续解释执行。

    2.栈
    2.1Java虚拟机栈

    Java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(StackFrame)来保存。

    通过idea DeBug查看栈帧的内容

    Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈。所以,虚拟机栈不存在垃圾回收,但是存在栈溢出问题。

    栈帧的组成:局部变量表、操作数栈、帧数据。

    1. 局部变量表:局部变量表的作用是在方法执行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变量表的内容。

      • 栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot) ,long和double类型占用两个槽,其 他类型占用一个槽。

      • 实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。

      • 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。

      • 局部变量表保存的内容有:实例方法的this对象、方法的参数、方法体中声明的局部变量。

      • 为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。

    2. 操作数栈

      操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。它是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。在编译期就可以确定操作数栈的最大深度,从而在实行是正确的分配内存大小。

      • 案例:加法运算中操作数栈的应用

    3. 帧数据

      • 当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系

      • 方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧这中的下一条指令的地址。所以当前栈帧中,需要存放此方法出口的地址。

      • 异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令的位置。

    4. 栈内存溢出

      Java虚拟机栈如果栈帧过多,占用内存超过占内存可以分配的最大大小就会出现内存溢出StackOverflowError)。

      • 默认大小:如果我们不指定栈的大小,JVM将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。

        Linux:x86:1MB ;ppc:2MB

        BSD:x86:1MB

        Solaris:x86:1MB

        Windows:基于操作系统默认值

      • 栈溢出模拟 

      • 栈大小设置:要修改Java虚拟机栈的大小,可以使用虚拟机参数 -Xss

        语法:-Xss栈大小

        单位:字节(默认,必须是1024的倍数,否则会报错)、k/K(KB,常用)、m\M(MB)、g\G(GB)

    5. 注意事项

      • 与 -Xss 类似,也可以使用 -XX:ThreadStackSize 调整标志来配置堆栈大小。

        格式为:-XX:ThreadStackSize=1024

      • HotSpot JVM对栈大小的最大值和最小值有要求:Windows(64位)下的 JDK8 测试最小值为180k,最大值为1024m

      • 局部变量过多、操作数栈深度过大也会影响栈内存的大小。一般情况下,工作中即便使用了递归进行操作,栈的深度最多也只能到几百,不会出现栈的溢出。所以此参数可以手动指定为 -Xss256k 节省内存。

    2.2本地方法栈

    Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。

    3.堆
    1. 定义

      一般Java程序中堆内存是空间最大的一块内存区域,创建出来的对象都存在于堆上。栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。

      堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemoryError异常。

    2. used、total、max

      堆空间有三个需要关注的值,used、total、max。used指的是当前已使用的堆内存,total是Java虚拟机已经分配的可用堆内存,max是Java虚拟机可以分配的最大堆内存。

      随着堆中的对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆。 如果堆内存不足,java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等。

    3. arthas中堆内存的相关功能

      • 堆内存used、total、max三个值可以通过 dashboard 命令看到

      • 手动指定刷新频率(不指定默认5秒一次):dashboard -i 刷新频率(毫秒)

      • 也可以使用 memory 命令只查看对内存使用情况(不会刷新)

    4. 设置大小

      如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64。在实际应用中一般都需要设置total和max的值。

      • 要修改堆的大小,可以使用虚拟机参数 –Xmx(max最大值)和 -Xms(初始的total)。

      • 语法:-Xmx值 -Xms值

      • 单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)

      • 限制:Xmx必须大于 2 MB,Xms必须大于1MB

      • 注意:Java服务端程序开发时,建议将 -Xmx 和 -Xms 设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况。

    5. 问题:为什么arthas中显示的heap堆大小与设置的值不一样呢?

      arthas中的heap堆内存使用了JMX技术中内存获取方式,这种方式与垃圾回收器有关,计算的是可以分配对象的内存,而不是整个内存。

    4.方法区

    方法区是存放基本信息的位置,线程共享,主要包括三个部分:类的元信息运行时常量池字符串常量池

    1. 类的元信息

      方法区是用来存储每个类的基本信息(元信息),一般称为InstanceKClass对象,在类的加载阶段完成。

    2. 运行时常量池

      除了存储类的元信息之外,还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池

    3. 字符串常量池

      字符串常量池(String Table)存储在代码中定义的常量字符串内容。例如:

      • 字符串常量池和运行时常量池的关系:早期设计时,字符串常量池是属于运行时常量池的一部分,它们存储的位置也是一致的。后续做出了调整,将字符串常量池和运行时常量池做了拆分。

        JDK7之前:运行时常量池逻辑包含字符串常量池,hotspot虚拟机对方法区的实现为永久代。

        JDK7:字符串常量池从方法区拿到了堆中,运行时常量池剩下的东西还在永久代。

        JDK8之后:hotspot移除了永久代用元空间取而代之,字符串常量池还在堆。

      • intern()方法介绍:String.intern() 方法是可以手动将字符串放入字符串常量池中,但是在不同的 JDK版本中,表现有所不同。
         

         public static void main(String[] args) {
                 String s1 = new StringBuilder().append("think").append("123").toString();
                 System.out.println(s1.intern() == s1); //true,JDK6之前是false
         ​
                 String s2 = new StringBuilder().append("ja").append("va").toString();
                 System.out.println(s2.intern() == s2); //false
         }

        在JDK6中:中intern () 方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代里面这个字符串实例的引用。JVM启动时就会把java这个需要用到的字符串加入到常量池中。所以执行结果均为false

        JDK7以及之后的版本:由于字符串常量池在堆上,所以intern () 方法会把第一次遇到的字符串的引用放入字符串常量池。所以直接结果为truefalse

    4. 方法区的设计

      • JDK7以及之前的版本将方法区存放在堆区域中的永久代(PermGen Space)空间,堆的大小由虚拟机参数来控制。

      • JDK8以及之后的版本将方法区存放在元空间(MetaSpace)中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。

    5. arthas中查看方法区

      使用 memory 打印出内存情况,JDK7及之前的版本查看ps_perm_gen属性。JDK8及之后的版本查看metaspace属性。

    6. 方法区溢出

      通过ByteBuddy框架,动态生成字节码数据,加载到内存中。通过死循环不停地加载到方法区,观察到方法区是否出现内存溢出的情况。(com.gty.jvm.part1.runtimeDataArea.methodArea.Demo1

      通过实验发现,JDK7上运行大概十几万次,就出现了错误。在JDK8上运行百万次,程序都没有出现任何错误,但是内存会直线升高。这说明JDK7和JDK8在方法区的存放上,采用了不同的设计。

      • JDK7 是将方法区存放在堆区域中的永久代空间,永久代空间的大小由虚拟机参数 -XX:MaxPermSize=值 来控制。

      • JDK8 将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用 -XX:MaxMetaspaceSize=值 将元空间最大大小进行限制,一般设置为256M。

    7. 静态变量的存储

      • JDK6及之前的版本中:静态变量是存放在方法区中的,也就是永久代。

      • JDK7及之后的版本中:静态变量是存放在堆中的Class对象中,脱离了永久代(可参考虚拟机源码:BytecodeInterpreter针对putstatic指令的处理)。

    5.直接内存

    直接内存(Direct memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。在 JDK1.4 中引入了 NIO 机制,使用了直接直接内存,主要为了解决一下两个问题:

    1. java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。

    2. IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。现在直接放入直接内存即可,同时Java堆上维护直接内存的引l用,减少了数据复制的开销。写文件也是类似的思路。

    要创建直接内存上的数据,可以使用ByteBuffer:

    • 语法:ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);

    • arthas:memory命令可以查看直接内存大小,属性名direct。

    如果需要手动调整直接内存的大小,可以使用-XX:MaxDirectMemorySize=大小:

    • 单位k或K表示千字节,m或M表示兆字节,g或G表示千兆字节。默认不设置该参数情况下,JVM 自动选择最大分配的大小。

    六.自动垃圾回收

    在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收

    所谓内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出

    1. Java的内存管理

      Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。其他很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器。

    2. 垃圾回收的对比

      • 自动垃圾回收:自动根据对象是否使用由虚拟机来回收对象。 优点:降低了程序员实现难度、降低对象回收Bug的可能性 缺点:程序员无法控制内存回收的及时性

      • 手动垃圾回收:由程序员编写实现对象的删除 优点:回收及时性高,由程序员把控回收的时机 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题

    1.方法区的回收

    方法区回收的内容主要是不再使用的类。判定一个类可以被卸载,需要同时满足下面三个条件:

    1. 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。

    2. 加载该类的类加载器已经被回收。

    3. 该类对应的 java.lang.Class 对象没有在任何地方被引用。

    方法区的回收 —— 手动触发回收

    • 如果需要手动触发垃圾回收,可以调用 System.gc() 方法。

    • 注意事项:调用 System.gc() 方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断。

    2.堆回收
    2.1 引用计数法和可达性分析算法
    1. 引用计数法

      1. 如何判断堆上的对象可以回收?

                Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许回收。

      2. 引用计数法:引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。

      3. 引用计数法的缺点——循环引用:引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法,但它也有缺点,主要有两点: 每次引用和取消引用都需要维护计数器,对系统性能有一定影响。 存在循环引用的问题,所谓循环引用就是当A引用B,B同时也引用A时会出现对象无法回收的问题。

      4. 查看垃圾回收日志

        如果想要查看垃圾回收的信息,可以使用 -verbose:gc 参数。

    2. 可达性分析算法

      Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。

      1. 哪些对象可以被称为GC Root对象

        • 线程Thread对象。

        • 系统类加载器加载的 java.lang.Class 对象。

        • 监视器对象,用来保存同步锁synchronized关键字持有的对象。

        • 本地方法调用时使用的全局对象。

      2. 查看GC Root

        通过arthas和eclipse Memeoy Analyzer(MAT) 工具可以查看GC Root,MAT工具是eclipse推出的Java堆内存检测工具。具体操作步骤如下:

        • 使用arthas的 heapdump命令将堆内存快照保存到本地磁盘中,命令后面要跟上要保存到的路径和文件名,文件名后缀为 .hprof。

        • 使用MAT工具打开堆内存快照文件(该工具要求JDK版本要在JDK17及以上)

        • 选择GC Roots功能查看所有的GC root

    2.2 五种对象引用

    可达性分析算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要有这层关系在,普通对象就不会被回收。除了强引用之外,Java还设计了几种其他引用方式:软引用、弱引用、虚引用、终结器引用。

    1. 软引用

      软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。

      在JDK1.2之后提供了 SoftReference 类来实现软引用,软引用常用于缓存中。

      软引用的执行过程:

      1. 将对象使用的软引用包装起来,new SoftReference<对象类型>(对象)

      2. 内存不足时,虚拟机尝试进行垃圾回收。

      3. 如果垃圾回收仍不能解决内存问题,回收软引用中的对象。

      4. 如果依然内存不足,抛出 OutOfMemeoy 异常。

      软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收。如何知道哪SoftReference对象需要回收呢?

      SoftReference提供了一套队列机制:

      1. 软引用创建时,通过构造器传入引用队列

      2. 软引用中包含的对象被回收时,该软引用对象会被放入引用队列

      3. 通过代码遍历引用队列,将 SoftReferenc e的强引用删除

      软引用的使用场景——缓存

      使用软引用实现学生数据的缓存

    2. 弱引用

      弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK1.2之后提供了WeakReference类来实现弱引l用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收。

    3. 虚引用和终结器引用

      这两种引用在常规开发中是不会使用的。

      • 虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用 PhantomReference 实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。

      • 终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的 finalize() 方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在 finalize() 方法中再将自身对象使用强引用关联上,但是不建议这样做。

    2.3 垃圾回收算法
    1. 核心思想

      垃圾回收要做的有两件事:

      1. 找到内存中存活的对象

      2. 释放不再存活对象的内存,使得程序能再次利用这部分空间

    2. 历史和分类

      • 1960年John McCarthy发布了第一个GC算法:标记-清除算法。

      • 1963年Marvin L. Minsky 发布了复制算法。

      本质上后续的所有的垃圾算法,都是在上述两种算法的基础上优化而来。

    3. 评价标准

      Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为 Stop The World 简称STW,如果STW过长则会影响用户的使用。

      判断GC算法是否优秀,可以从三个方面考虑:

      1. 吞吐量

        吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即 吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率也就越高。比如,虚拟机总共运行了100 分钟,其中GC花掉1分钟,那么吞吐量就是99%。

      2. 最大暂停时间

        最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。例如下图中,黄色部分的STW就是最大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时受到的影响就越短

      3. 堆使用效率

        不同的垃圾回收算法,对堆内存的使用方式是不同的。比如标记清楚算法,可以使用完整的堆内存;而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用的效率上来说,标记清除算法要优于复制算法。

      上述三种评价标准:吞吐量、最大暂停时间以及堆使用效率是不可兼得的。

      一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量

      不同的垃圾回收算法,适用于不同的场景。

    2.3.1 标记-清除算法
    1. 核心思想

      标记清除算法的核心思想分为两个阶段:

      1. 标记阶段:将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活的对象。

      2. 清除阶段:从内存中删除没有被标记也就是非存活的对象。

    2. 优点

      实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。

    3. 缺点

      1. 碎片化问题:由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。

      2. 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才 能获得合适的内存空间。

    2.3.2 复制算法
    1. 核心思想

      1. 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。

      2. 在垃圾回收GC阶段,将From中存活对象复制到To空间。

      3. 将两块空间的From和To名字互换。

    2. 优点

      1. 吞吐量高:复制算法只需要遍历一次存活对象复制到To空间,比标记-整理算法少一次遍历的过程,因而性能较好。但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动。

      2. 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化空间。

    3. 缺点

      1. 内存使用效率低:每次只能让一半的内存空间来为创建对象使用

    2.3.3 标记-整理算法

    标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生碎片问题的一种解决方案。

    1. 核心思想

      1. 标记阶段:将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活的对象。

      2. 整理阶段:将存活对象移动到堆的一端,清理掉存活对象的内存空间。

    2. 优点

      1. 内存使用效率高:整个堆内存都可以使用,不会像复制算法只能使用半个堆内存。

      2. 不会发生碎片化:在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间。

    3. 缺点

      1. 整理阶段的效率不高:整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two-Finger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能。

    2.3.4 分代垃圾回收算法

    现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。

    分代垃圾回收将整个内存区域划分为年轻代(新生代)和老年代。年轻代又包括伊甸园区(Eden区)和幸存者区,幸存者区分为幸存者0区和幸存者1区。

    1. arthas查看分代之后的内存情况

      • 在JDK8中,添加 -XX:+UseSerialGC 参数使用分代回收的垃圾回收器,运行程序。

      • 在arthas中使用 memory 命令查看内存,显示出三个区域的内存情况。

    2. 调整内区域大小

      根据一下虚拟机参数,调整堆的大小并观察结果。注意加上 -XX:+UseSerialGC

    3. 核心思想

      分代回收时,创建出来的对象,首先会被放入Eden区。随着对象在Eden区越来越多,如果Eden区满了,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。Minor GC会把需要Eden区中和From(S0)需要回收的对象回收,把没有回收的对象放入To(S1)区。接下来,S0会变成To区,S1变成From区。当Eden区满时再往里放入对象,依然会发生Minor GC。此时会回收Eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。注意,每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1

      如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。当老年代中空间不足,无法放入新的对象时,先尝试Minor GC 如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。

    4. 为什么分代GC算法要把堆分成年轻代和老年代?

      系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收。比如用户获取订单数据,订单数据返回给用户之后就可以释放了。老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。在虚拟机的默认设置中,新生代大小要远小于老年代的大小。

      分代GC算法将堆分成年轻代和老年代主要原因如下:

      1. 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。

      2. 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择,灵活度较高。

      3. 分代的设计中允许只回收新生代(Minor GC),如果能满足对象分配的要求就不需要对整个堆进行回收(Full GC),STW的时间就会减少。

    2.4 垃圾回收器

    垃圾回收器的组合关系

    垃圾回收器是垃圾回收算法的具体体现,由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。具体的关系图如下:

    1. 垃圾回收器1

      • 年轻代-Serial垃圾回收器:Serial是一种单线程串行回收年轻代的垃圾回收器。

        • 回收年代和算法:年轻代、复制算法

        • 优点:单CPU处理器下吞吐量非常出色

        • 缺点:多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会使用户线程处于长时间的等待

        • 适用场景:Java编写的客户端程序或者硬件配置有限的场景

      • 老年代-SerialOld垃圾回收器:SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收

        -XX:+UseSerialGC 新生代、老年代都使用串行回收器。

        • 回收年代和算法:老年代、标记-整理算法

        • 优点:单CPU处理器下吞吐量非常出色

        • 缺点:多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会使用户线程处于长时间的等待

        • 适用场景:与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用

    2. 垃圾回收器 2

      • 年轻代-ParNew垃圾回收器

        ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收。 -XX:+UseParNewGC 新生代使用ParNew回收器,老年代使用串行回收器。

        • 回收年代和算法:年轻代、复制算法

        • 优点:多CPU处理器下停顿时间较短

        • 缺点:吞吐量和停顿时间不如G1,所以JDK9之后不建议使用

        • 适用场景:JDK8以及之前的版本中,与CMS老年代垃圾回收器搭配使用

      • 老年代 - CMS(Concurrent Mark Sweep)垃圾回收器

        CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收在某些步骤中同时执行,减少了用户线程的等待时间。 参数:-XX:+UseConcMarkSweepGC

        • 回收年代和算法:老年代、标记-清除算法

        • 优点:系统由于垃圾回收出现的停顿时间较短,用户体验好

        • 缺点:内存碎片问题、退化问题、浮动垃圾问题

        • 适用场景:大型的互联网系统中用户请求数据量大、频率高的场景。比如订单接口、商品接口等

        CMS执行步骤:

        1. 初始标记:用极短的时间标记出GC Roots能直接关联到的对象。

        2. 并发标记:标记所有的对象,用户线程不需要暂停。

        3. 重新标记:由于并发标记阶段有些对象发生了变化,存在错标、漏标等情况,需要重新标记。

        4. 并发清理:清理死亡的对象,用户线程不需要暂停。

        CMS缺点:

        1. CMS使用了标记-清除算法,在垃圾收集结束后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停,可以使用 -XX:CMSFullGCsBeforeCompaction=N(默认为0)调整N次Full GC之后再整理。

        2. 如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。

        3. 无法处理在并发清理过程中产生的”浮动垃圾“,不能做到完全的垃圾回收。

    3. 垃圾回收器 3

      • 年轻代 - Parallel Scavenge垃圾回收器

        Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量,具有自动调整对内存大小的特点。

        • 回收年代和算法:年轻代、复制算法

        • 优点:吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数

        • 缺点:不能保证单次的停顿时间

        • 适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象。比如:大数据的处理、大文件导出

        Parallel Scavenge允许手动设置最大暂停时间和吞吐量。   Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。

        • 最大暂停时间:-XX:MaxGcPauseMillis=n,设置每次垃圾回收时的最大停顿毫秒数

        • 吞吐量:-XX:GCTimeRatio=n,设置吞吐量为n(用户线程执行时间=n/n + 1)

        • 自动调整内存大小:-XX:+UseAdaptiveSizePolicy,设置可以让垃圾回收器根据吞吐量和最大停顿时间的毫秒数自动调整内存大小

      • 老年代 - Parallel Old垃圾回收器

        Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。 参数:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC 可以使用Parallel Scavenge + Parallel Old这种组合。

        • 回收年代和算法:老年代、标记-整理算法

        • 优点:并发收集,在多核CPU下效率较高

        • 缺点:暂停时间比较长

        • 适用场景:与Parallel Scavenge配套使用

    4. G1垃圾回收器

      JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。ParallelScavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小。CMS关注暂停时间,但是吞吐量方面会下降。而G1设计目标就是将上述两种垃圾回收器的优点融合:

      1. 支持巨大的堆空间回收,并有较高的吞吐量。

      2. 支持多CPU并行垃圾回收。

      3. 允许用户设置最大暂停时间。

      1.内存结构:

      G1出现之前的垃圾回收器,内存结构一般是连续的。而G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过 堆空间大小/2048 计算得到,也可以通过参数 -XX:G1HeapRegionSize=32m 指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。

      1. 垃圾回收方式

        G1垃圾回收有两种方式:年轻代回收(Young GC)、混合回收(Mixed GC)

      • 年轻代回收(Young GC):回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。 执行流程:

        1. 新创建出来的对象会存放在Eden区,当G1判断年轻代区不足(max默认堆空间大小的60%),无法分配对象时需要回收时执行 Young GC。G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。

        2. 标记出Eden和Survivor区域中的存活对象。

        3. 根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。

        4. 后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。

        5. 当某个存活对象的年龄到达阈值(默认15),将被放入老年代。

        6. 部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。

      • 混合回收(Mixed GC):多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent 默认45%),会触发混合回收Mixed GC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbage first)名称的由来。最后清理阶段使用复制算法,不会产生内存碎片。 混合回收分为:

        • 初始标记(initial mark):标记Gc Roots引用的对象为存活。

        • 并发标记(concurrent mark):将第一步中标记的对象引用的对象,标记为存活。

        • 最终标记(remark或者Finalize Marking):标记一些引用改变漏标的对象不管新创建、不再关联的对象。

        • 并发清理(cleanup):将存活对象复制到别的Region区,不会产生内存碎片。

      1. Full GC

      如果清理过程中发现没有足够的空Region区存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。

      1. G1 – Garbage First 垃圾回收器

        参数1: -XX:+UseG1GC 打开G1的开关,JDK9之后默认不需要打开 参数2:-XX:MaxGCPauseMillis=毫秒值 最大暂停的时间

      • 回收年代和算法:年轻代+老年代、复制算法

      • 优点:对比较大的堆如超过6G的堆回收时,延迟可控;不会产生内存碎片;并发标记的SATB算法效率高

      • 缺点:JDK8之前还不够成熟

      • 适用场景:JDK8最新版本、JDK9之后建议默认使用