JVM(基础篇)

打印 上一主题 下一主题

主题 962|帖子 962|积分 2896

一.初识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基础信息


  • Magic魔数
        文件是无法通过文件扩展名来确定文件范例的,文件扩展名可以随意修改,不影响文件的内容。软件利用文件的头几个字节(文件头)去校验文件的范例,假如软件不支持该种范例就会堕落。Java字节码文件中,将文件头称为magic魔数


  • 主副版本号
主副版本号指的是编译字节码文件的 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常用工具


  • 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类的全限定名:反编译已加载类的源码。
利用方法:
  1.  curl -O https://arthas.aliyun.com/arthas-boot.jar    --->  首次需执行
  2.  java -jar arthas-boot.jar
  3.  ​
  4.  然后选择要选择的进程号等待arhas项目启动,就可以进行操作(classloader等等)
复制代码
三.类的生命周期

1.生命周期概述

类的生命周期

2.加载阶段

2.1加载阶段


  • 加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。程序员可以利用Java代码拓展的不同的渠道。(本地文件:磁盘上的字节码文件、动态署理天生:程序运行时利用动态署理天生、通过网络传输的类:早期的Applet技术利用)
  • 类加载器获取不同渠道的信息
  • 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中。天生一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。

  • 同时,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虚拟机规范》中的束缚,这个阶段一样平常不必要程序员参与。重要包含以下四个部分,

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

  • 元信息验证,比方类必须有父类(super不能为空)。

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

  • 符号引用验证,比方是否访问了其他类中private的方法等。
案例:版本号的检测

结论:主版本号不能高于运行环境主版本号,假如主版本号相等,副版本号也不能凌驾。
3.2准备


  • 准备阶段为静态变量(static)分配内存并设置初始值。 (1) 准备阶段只会给静态变量赋初始值,而每一种根本数据范例和引用数据范例都有其初始值。
    (2) final修饰的根本数据范例的静态变量,准备阶段直接会将代码中的值举行赋值。

         

3.3剖析


  • 剖析阶段重要是将常量池中的符号引用更换为直接引用。
  • 符号引用就是在字节码文件中利用编号来访问常量池中的内容。

  • 直接引用不在利用编号,而是利用内存中地点举行访问详细的数据。

4.初始化阶段

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

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



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

    • 访问一个类的静态变量或者静态方法,注意变量是final修饰的而且等号右边是常量不会触发初始化。
    • 调用Class.forName(String className)。
    • new一个该类的对象时。
    • 执行Main方法的当前类。
    添加 -XX:+TraceClassLoading 参数可以打印出加载并初始化的类





Test1:




  • clinit指令在特定环境下不会出现:

    • 无静态代码块且无静态变量赋值语句
    • 有静态变量的声明,但是没有赋值语句
    • 静态变量的界说利用final关键字,这类变量会在准备阶段直接举行初始化

  • 继承关系中 Demo02.java

    • 直接访问父类的静态变量,不会触发子类的初始化。
    • 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

四.类加载器


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

    • 企业级应用:SPI机制、类的热部署、Tomcat类的隔离
    • 大量的口试题:什么是类的双亲委派机制、打破类的双亲委派机制、自界说类加载器
    • 办理线上问题:利用 Arthas 不停机办理线上故障

1.类加载器的分类

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

  • Java:

    • JDK中默认提供或者自界说:JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以本身根据需求定制。
    • 继承抽象类ClassLoader:所有Java中实现的类加载器都必要继承ClassLoader这个抽象类。

  • 虚拟机底层实现:

    • 虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言与虚拟机底层语言同等,比如 Hotspot 利用C++。
    • 加载程序运行时的基础类:包管Java程序运行中基础类被精确地加载,比如 java.lang.String,确保其可靠性。
    类加载器的设计JDK8和8之后的版本差别较大,JDK8之后的版本中默认的类加载器有如下几种:
       

    • 虚拟机底层实现:启动类加载器(Bootstrap),加载Java中最核心的类。
    • 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包名 举行扩展。
       

    • 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.双亲委派机制


  • 界说:当加载一个类时,他总是先让他的父级类加载器去加载,确保系统中的类优先加载。直到父类加载器找不到类时,在逐级向下让子级类加载器加载,假如找不到,终极会抛异常 ClassNotFoundException。这样做是为了防止。我们本身界说的类更换了系统中的类。
    总结来说就是:自底向上查找是否加载过,再由顶向下举行加载。
  • 类加载器的层级关系:
    每个Java实现的类加载器中保存了一个成员变量叫 “父”(Parent)类加载器,可以理解为它的上级,并不是继承关系。
    应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是null。这是因为启动类加载器利用C++编写,不属于Java的类加载器。而启动类加载没有上级类加载器。

  • Arthas中的相关功能
    类加载器的继承关系可以通过classloader-t查看:

  • 双亲委派机制办理的三个问题:

    • 重复的类:假如一个类出如今三个类加载的加载位置,则会被启动类加载器加载,根据双亲委派机制,他的优先级是最高的。
    • 本身创建的String类:本身创建的String类是不能被加载的,终极会交由启动类加载器加载在rt.jar包中的String类。
    • 类加载器的关系:应用类加载器的父类加载器是扩展,扩展类加载器没有父类加载器,但是会委派给启动类加载器加载。

3.打破双亲委派机制

3.1自界说类加载器

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

  • ClassLoader的原理:重要包含了四个核心方法
    1.  /*
    2.      类加载的入口,提供了双亲委派机制,内部会调用findClass
    3.  */
    4.  public Class<?> loadClass(String name)
    5.      
    6.  /*
    7.      由加载器子类实现,获取二进制数据调用defineClass,比如URLCLassLOader会根据文件路径去获取类文件中的二进制数据
    8.  */
    9.  protected Class<?> findClass(String name)
    10.      
    11.   /*
    12.      做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
    13.   */
    14.  protected final Class<?> defineClass(byte[] b, int off, int len)
    15.  ​
    16.  /*
    17.      执行类生命周期的连接阶段
    18.  */
    19.  protected final void resolveClass(Class<?> c)
    复制代码
  • 自界说加载器的父类加载器:在不手动设置父类加载器时,自界说加载器的父类加载器为应用程序类加载器
    1.  // 在JDK8中,ClassLoader类中提供了构造方法设置parent的内容
    2.  private ClassLoader(Void unused, ClassLoader parent) {
    3.      this.parent = parent;
    4.      if (ParallelLoaders.isRegistered(this.getClass())) {
    5.          parallelLockMap = new ConcurrentHashMap<>();
    6.          package2certs = new ConcurrentHashMap<>();
    7.          assertionLock = new Object();
    8.      } else {
    9.          // no finer-grained lock; lock on the classloader instance
    10.          parallelLockMap = null;
    11.          package2certs = new Hashtable<>();
    12.          assertionLock = this;
    13.      }
    14.  }
    15.  /*
    16.      这个构造方法由另外一个构造方法调用,其中父类加载器由由getSystemClassLoader方法设置,该方法返回的是AppClassLoader
    17.  */
    18.  protected ClassLoader() {
    19.      this(checkCreateClassLoader(), getSystemClassLoader());
    20.  }
    复制代码
  • 两个自界说类加载器加载相同限定名的类:两个自界说加载器加载相同限定名的类是不会冲突的,在同一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概念,类加载器在设计上发生了许多变化。

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

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

五.运行时数据区

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


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

    • 程序计数器
    • Java虚拟机栈
    • 本地方法栈

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

    • 方法区


1. 程序计数器


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

  • 作用:

    • 在代码执行过程中,程序计数器会记录下一行字节码指令的地点。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令。程序计数器可以控制程序指令的举行,实现分支、跳转、异常等逻辑。
    • 在多线程执行环境下,Java虚拟机必要通过程序计数器记录CPU切换前表明执行到哪一句指令并继续表明执行。

2.栈

2.1Java虚拟机栈

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

通过idea DeBug查看栈帧的内容

Java虚拟机栈随着线程的创建而创建,而回收则会在线程的烧毁时举行。由于方法大概会在不同线程中执行,每个线程都会包含一个本身的虚拟机栈。以是,虚拟机栈不存在垃圾回收,但是存在栈溢出问题。
栈帧的组成:局部变量表、操作数栈、帧数据。

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

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

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

    • 方法参数也会保存在局部变量表中,其顺序与方法中参数界说的顺序同等。
    • 局部变量表保存的内容有:实例方法的this对象、方法的参数、方法体中声明的局部变量。
       

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

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


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

  • 帧数据

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

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

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

  • 栈内存溢出
    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)


  • 注意事项

    • 与 -Xss 类似,也可以利用 -XX:ThreadStackSize 调解标记来设置堆栈大小。
      格式为:-XX:ThreadStackSize=1024
    • HotSpot JVM对栈大小的最大值和最小值有要求:Windows(64位)下的 JDK8 测试最小值为180k,最大值为1024m
    • 局部变量过多、操作数栈深度过大也会影响栈内存的大小。一样平常环境下,工作中即便利用了递归举行操作,栈的深度最多也只能到几百,不会出现栈的溢出。以是此参数可以手动指定为 -Xss256k 节流内存。

2.2本地方法栈

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


  • 界说
    一样平常Java程序中堆内存是空间最大的一块内存地区,创建出来的对象都存在于堆上。栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
    堆内存大小是有上限的,当对象不停向堆中放入对象到达上限之后,就会抛出OutOfMemoryError异常。
  • used、total、max
    堆空间有三个必要关注的值,used、total、max。used指的是当前已利用的堆内存,total是Java虚拟机已经分配的可用堆内存,max是Java虚拟机可以分配的最大堆内存。
    随着堆中的对象增多,当total可以利用的内存即将不敷时,java虚拟机会继续分配内存给堆。 假如堆内存不敷,java虚拟机就会不停的分配内存,total值会变大。total最多只能与max相等。

  • arthas中堆内存的相关功能

    • 堆内存used、total、max三个值可以通过 dashboard 命令看到
    • 手动指定革新频率(不指定默认5秒一次):dashboard -i 革新频率(毫秒)
    • 也可以利用 memory 命令只查看对内存利用环境(不会革新)


  • 设置大小
    假如不设置任何的虚拟机参数,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虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆紧缩的环境。

  • 问题:为什么arthas中显示的heap堆大小与设置的值不一样呢?
    arthas中的heap堆内存利用了JMX技术中内存获取方式,这种方式与垃圾回收器有关,计算的是可以分配对象的内存,而不是整个内存。

4.方法区

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

  • 类的元信息
    方法区是用来存储每个类的根本信息(元信息),一样平常称为InstanceKClass对象,在类的加载阶段完成。
  • 运行时常量池
    除了存储类的元信息之外,还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通过内存地点快速的定位到常量池中的内容,这种常量池称为运行时常量池

  • 字符串常量池
    字符串常量池(String Table)存储在代码中界说的常量字符串内容。比方:


    • 字符串常量池和运行时常量池的关系:早期设计时,字符串常量池是属于运行时常量池的一部分,它们存储的位置也是同等的。后续做出了调解,将字符串常量池和运行时常量池做了拆分。
      JDK7之前:运行时常量池逻辑包含字符串常量池,hotspot虚拟机对方法区的实现为永久代。
      JDK7:字符串常量池从方法区拿到了堆中,运行时常量池剩下的东西还在永久代。
      JDK8之后:hotspot移除了永久代用元空间取而代之,字符串常量池还在堆。

    • intern()方法先容:String.intern() 方法是可以手动将字符串放入字符串常量池中,但是在不同的 JDK版本中,表现有所不同。
       
      1.  public static void main(String[] args) {
      2.          String s1 = new StringBuilder().append("think").append("123").toString();
      3.          System.out.println(s1.intern() == s1); //true,JDK6之前是false
      4.  ​
      5.          String s2 = new StringBuilder().append("ja").append("va").toString();
      6.          System.out.println(s2.intern() == s2); //false
      7.  }
      复制代码
      在JDK6中:中intern () 方法会把第一次碰到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代里面这个字符串实例的引用。JVM启动时就会把java这个必要用到的字符串参加到常量池中。以是执行结果均为false
      JDK7以及之后的版本:由于字符串常量池在堆上,以是intern () 方法会把第一次碰到的字符串的引用放入字符串常量池。以是直接结果为truefalse


  • 方法区的设计

    • JDK7以及之前的版本将方法区存放在堆地区中的永久代(PermGen Space)空间,堆的大小由虚拟机参数来控制。
    • JDK8以及之后的版本将方法区存放在元空间(MetaSpace)中,元空间位于操作系统维护的直接内存中,默认环境下只要不凌驾操作系统蒙受的上限,可以不停分配。

  • arthas中查看方法区
    利用 memory 打印出内存环境,JDK7及之前的版本查看ps_perm_gen属性。JDK8及之后的版本查看metaspace属性。

  • 方法区溢出
    通过ByteBuddy框架,动态天生字节码数据,加载到内存中。通过死循环不停地加载到方法区,观察到方法区是否出现内存溢出的环境。(com.gty.jvm.part1.runtimeDataArea.methodArea.Demo1
    通过实验发现,JDK7上运行大概十几万次,就出现了错误。在JDK8上运行百万次,程序都没有出现任何错误,但是内存会直线升高。这说明JDK7和JDK8在方法区的存放上,采取了不同的设计。

    • JDK7 是将方法区存放在堆地区中的永久代空间,永久代空间的大小由虚拟机参数 -XX:MaxPermSize=值 来控制。
    • JDK8 将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认环境下只要不凌驾操作系统蒙受的上限,可以不停分配。可以利用 -XX:MaxMetaspaceSize=值 将元空间最大大小举行限定,一样平常设置为256M。

  • 静态变量的存储

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

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


5.直接内存

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

  • java堆中的对象假如不再利用要回收,回收时会影响对象的创建和利用。
  • IO操作比如读文件,必要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。如今直接放入直接内存即可,同时Java堆上维护直接内存的引l用,减少了数据复制的开销。写文件也是类似的思路。
要创建直接内存上的数据,可以利用ByteBuffer:


  • 语法:ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);
  • arthas:memory命令可以查看直接内存大小,属性名direct。
假如必要手动调解直接内存的大小,可以利用-XX:MaxDirectMemorySize=大小:


  • 单位k或K表示千字节,m或M表示兆字节,g或G表示千兆字节。默认不设置该参数环境下,JVM 自动选择最大分配的大小。
六.自动垃圾回收

在C/C++这类没有自动垃圾回收机制的语言中,一个对象假如不再利用,必要手动释放,否则就会出现内存走漏。我们称这种释放对象的过程为垃圾回收,而必要程序员编写代码举行回收的方式为手动回收
所谓内存走漏指的是不再利用的对象在系统中未被回收,内存走漏的积累大概会导致内存溢出


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

    • 自动垃圾回收:自动根据对象是否利用由虚拟机往返收对象。 优点:降低了程序员实现难度、降低对象回收Bug的大概性 缺点:程序员无法控制内存回收的实时性
    • 手动垃圾回收:由程序员编写实现对象的删除 优点:回收实时性高,由程序员把控回收的时机 缺点:编写不当容易出现悬空指针、重复释放、内存走漏等问题

1.方法区的回收

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

  • 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
  • 加载该类的类加载器已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用。
方法区的回收 —— 手动触发回收


  • 假如必要手动触发垃圾回收,可以调用 System.gc() 方法。
  • 注意事项:调用 System.gc() 方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的哀求,详细是否必要执行垃圾回收Java虚拟机会自行判定。
2.堆回收

2.1 引用计数法和可达性分析算法


  • 引用计数法

    • 如何判定堆上的对象可以回收?
              Java中的对象是否能被回收,是根据对象是否被引用来决定的。假如对象被引用了,说明该对象还在利用,不允许回收。

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

    • 查看垃圾回收日志
      假如想要查看垃圾回收的信息,可以利用 -verbose:gc 参数。


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

    • 哪些对象可以被称为GC Root对象

      • 线程Thread对象。
      • 系统类加载器加载的 java.lang.Class 对象。
      • 监视器对象,用来保存同步锁synchronized关键字持有的对象。
      • 本地方法调用时利用的全局对象。

    • 查看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还设计了几种其他引用方式:软引用、弱引用、虚引用、终结器引用。

  • 软引用
    软引用相对于强引用是一种比较弱的引用关系,假如一个对象只有软引用关联到它,当程序内存不敷时,就会将软引用中的数据举行回收。
    在JDK1.2之后提供了 SoftReference 类来实现软引用,软引用常用于缓存中。
    软引用的执行过程:

    • 将对象利用的软引用包装起来,new SoftReference<对象范例>(对象)
    • 内存不敷时,虚拟机尝试举行垃圾回收。
    • 假如垃圾回收仍不能办理内存问题,回收软引用中的对象。
    • 假如依然内存不敷,抛出 OutOfMemeoy 异常。
    软引用中的对象假如在内存不敷时回收,SoftReference对象本身也必要被回收。如何知道哪SoftReference对象必要回收呢?
    SoftReference提供了一套队列机制:
       

    • 软引用创建时,通过构造器传入引用队列
    • 软引用中包含的对象被回收时,该软引用对象会被放入引用队列
    • 通过代码遍历引用队列,将 SoftReferenc e的强引用删除
    软引用的利用场景——缓存
    利用软引用实现学生数据的缓存

  • 弱引用
    弱引用的整体机制和软引用根本同等,区别在于弱引用包含的对象在垃圾回收时,不管内存够不敷都会直接被回收。在JDK1.2之后提供了WeakReference类来实现弱引l用,弱引用重要在ThreadLocal中利用。弱引用对象本身也可以利用引用队列举行回收。
  • 虚引用和终结器引用
    这两种引用在通例开发中是不会利用的。

    • 虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以吸收到对应的关照。Java中利用 PhantomReference 实现了虚引用,直接内存中为了实时知道直接内存对象不再利用,从而回收内存,利用了虚引用来实现。
    • 终结器引用指的是在对象必要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的 finalize() 方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在 finalize() 方法中再将自身对象利用强引用关联上,但是不发起这样做。

2.3 垃圾回收算法


  • 核心思想
    垃圾回收要做的有两件事:

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

  • 历史和分类

    • 1960年John McCarthy发布了第一个GC算法:标记-扫除算法。
    • 1963年Marvin L. Minsky 发布了复制算法。
    本质上后续的所有的垃圾算法,都是在上述两种算法的基础上优化而来。

  • 评价尺度
    Java垃圾回收过程会通过单独的GC线程来完成,但是不管利用哪一种GC算法,都会有部分阶段必要制止所有的用户线程。这个过程被称之为 Stop The World 简称STW,假如STW过长则会影响用户的利用。
    判定GC算法是否优秀,可以从三个方面思量:

    • 吞吐量
      吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即 吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率也就越高。比如,虚拟机总共运行了100 分钟,此中GC花掉1分钟,那么吞吐量就是99%。
    • 最大停息时间
      最大停息时间指的是所有在垃圾回收过程中的STW时间最大值。比方下图中,黄色部分的STW就是最大停息时间,显而易见上面的图比下面的图拥有更少的最大停息时间。最大停息时间越短,用户利用系统时受到的影响就越短
    • 堆利用效率
      不同的垃圾回收算法,对堆内存的利用方式是不同的。比如标记清楚算法,可以利用完备的堆内存;而复制算法会将堆内存一分为二,每次只能利用一半内存。从堆利用的效率上来说,标记扫除算法要优于复制算法。
    上述三种评价尺度:吞吐量、最大停息时间以及堆利用效率是不可兼得的。
    一样平常来说,堆内存越大,最大停息时间就越长。想要减少最大停息时间,就会降低吞吐量
    不同的垃圾回收算法,实用于不同的场景。

2.3.1 标记-扫除算法


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

    • 标记阶段:将所有存活的对象举行标记。Java中利用可达性分析算法,从GC Root开始通过引用链遍历出所有存活的对象。
    • 扫除阶段:从内存中删除没有被标记也就是非存活的对象。


  • 优点
    实现简单,只必要在第一阶段给每个对象维护标记位,第二阶段删除对象即可。
  • 缺点

    • 碎片化问题:由于内存是一连的,以是在对象被删除之后,内存中会出现许多细小的可用内存单元。假如我们必要的是一个比较大的空间,很有大概这些内存单元的大小过小无法举行分配。
    • 分配速率慢:由于内存碎片的存在,必要维护一个空闲链表,极有大概发生每次必要遍历到链表的末了才 能得到合适的内存空间。

2.3.2 复制算法


  • 核心思想

    • 准备两块空间From空间和To空间,每次在对象分配阶段,只能利用此中一块空间(From空间)。
    • 在垃圾回收GC阶段,将From中存活对象复制到To空间。
    • 将两块空间的From和To名字互换。

  • 优点

    • 吞吐量高:复制算法只必要遍历一次存活对象复制到To空间,比标记-整理算法少一次遍历的过程,因而性能较好。但是不如标记-扫除算法,因为标记扫除算法不必要举行对象的移动。
    • 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入To空间中,以是对象以外的地区都是可用空间,不存在碎片化空间。

  • 缺点

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

2.3.3 标记-整理算法

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

  • 核心思想

    • 标记阶段:将所有存活的对象举行标记。Java中利用可达性分析算法,从GC Root开始通过引用链遍历出所有存活的对象。
    • 整理阶段:将存活对象移动到堆的一端,清算掉存活对象的内存空间。

  • 优点

    • 内存利用效率高:整个堆内存都可以利用,不会像复制算法只能利用半个堆内存。
    • 不会发生碎片化:在整理阶段可以将对象往内存的一侧举行移动,剩下的空间都是可以分配对象的有效空间。

  • 缺点

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

2.3.4 分代垃圾回收算法

现代优秀的垃圾回收算法,会将上述形貌的垃圾回收算法组合举行利用,此中应用最广的就是分代垃圾回收算法(Generational GC)。
分代垃圾回收将整个内存地区划分为年轻代(新生代)和老年代。年轻代又包罗伊甸园区(Eden区)和幸存者区,幸存者区分为幸存者0区和幸存者1区。


  • arthas查看分代之后的内存环境

    • 在JDK8中,添加 -XX:+UseSerialGC 参数利用分代回收的垃圾回收器,运行程序。
    • 在arthas中利用 memory 命令查看内存,显示出三个地区的内存环境。

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

  • 核心思想
    分代回收时,创建出来的对象,首先会被放入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异常。
  • 为什么分代GC算法要把堆分成年轻代和老年代?
    系统中的大部分对象,都是创建出来之后很快就不再利用可以被回收。比如用户获取订单数据,订单数据返回给用户之后就可以释放了。老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。在虚拟机的默认设置中,新生代大小要远小于老年代的大小。
    分代GC算法将堆分成年轻代和老年代重要原因如下:

    • 可以通过调解年轻代和老年代的比例来顺应不同范例的应用程序,进步内存的利用率和性能。
    • 新生代和老年代利用不同的垃圾回收算法,新生代一样平常选择复制算法,老年代可以选择标记-扫除和标记-整理算法,由程序员来选择,机动度较高。
    • 分代的设计中允许只回收新生代(Minor GC),假如能满足对象分配的要求就不必要对整个堆举行回收(Full GC),STW的时间就会减少。

2.4 垃圾回收器

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


  • 垃圾回收器1

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


      • 回收年代和算法:年轻代、复制算法
      • 优点:单CPU处理器下吞吐量非常精彩
      • 缺点:多CPU下吞吐量不如其他垃圾回收器,堆假如偏大会利用户线程处于长时间的等待
      • 实用场景:Java编写的客户端程序或者硬件设置有限的场景

    • 老年代-SerialOld垃圾回收器:SerialOld是Serial垃圾回收器的老年代版本,采取单线程串行回收
      -XX:+UseSerialGC 新生代、老年代都利用串行回收器。

      • 回收年代和算法:老年代、标记-整理算法
      • 优点:单CPU处理器下吞吐量非常精彩
      • 缺点:多CPU下吞吐量不如其他垃圾回收器,堆假如偏大会利用户线程处于长时间的等待
      • 实用场景:与Serial垃圾回收器搭配利用,或者在CMS特别环境下利用


  • 垃圾回收器 2

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


      • 回收年代和算法:年轻代、复制算法
      • 优点:多CPU处理器下停顿时间较短
      • 缺点:吞吐量和停顿时间不如G1,以是JDK9之后不发起利用
      • 实用场景:JDK8以及之前的版本中,与CMS老年代垃圾回收器搭配利用

    • 老年代 - CMS(Concurrent Mark Sweep)垃圾回收器
      CMS垃圾回收器关注的是系统的停息时间,允许用户线程和垃圾回收在某些步调中同时执行,减少了用户线程的等待时间。 参数:-XX:+UseConcMarkSweepGC


      • 回收年代和算法:老年代、标记-扫除算法
      • 优点:系统由于垃圾回收出现的停顿时间较短,用户体验好
      • 缺点:内存碎片问题、退化问题、浮动垃圾问题
      • 实用场景:大型的互联网系统中用户哀求数据量大、频率高的场景。比如订单接口、商品接口等
      CMS执行步调:
           

      • 初始标记:用极短的时间标记出GC Roots能直接关联到的对象。
      • 并发标记:标记所有的对象,用户线程不必要停息。
      • 重新标记:由于并发标记阶段有些对象发生了变化,存在错标、漏标等环境,必要重新标记。
      • 并发清算:清算死亡的对象,用户线程不必要停息。
      CMS缺点:
           

      • CMS利用了标记-扫除算法,在垃圾网络竣事后会出现大量的内存碎片,CMS会在Full GC时举行碎片的整理。这样会导致用户线程停息,可以利用 -XX:CMSFullGCsBeforeCompaction=N(默以为0)调解N次Full GC之后再整理。
      • 假如老年代内存不敷无法分配对象,CMS就会退化成Serial Old单线程回收老年代。
      • 无法处理在并发清算过程中产生的”浮动垃圾“,不能做到完全的垃圾回收。


  • 垃圾回收器 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配套利用


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

    • 支持巨大的堆空间回收,并有较高的吞吐量。
    • 支持多CPU并行垃圾回收。
    • 允许用户设置最大停息时间。
    1.内存结构:
    G1出现之前的垃圾回收器,内存结构一样平常是一连的。而G1的整个堆会被划分成多个大小相等的地区,称之为区Region,地区不要求是一连的。分为Eden、Survivor、Old区。Region的大小通过 堆空间大小/2048 计算得到,也可以通过参数 -XX:G1HeapRegionSize=32m 指定(此中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
       

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

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

      • 新创建出来的对象会存放在Eden区,当G1判定年轻代区不敷(max默认堆空间大小的60%),无法分配对象时必要回收时执行 Young GC。G1在举行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的均匀耗时,以作为下次回收时的参考依据。这样就可以根据设置的最大停息时间计算出本次回收时最多能回收多少个Region地区了。比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。
      • 标记出Eden和Survivor地区中的存活对象。
      • 根据设置的最大停息时间选择某些地区将存活对象复制到一个新的Survivor区中(年龄+1),清空这些地区。
      • 后续Young GC时与之前相同,只不外Survivor区中存活对象会被搬运到另一个Survivor区。
      • 当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
      • 部分对象假如大小凌驾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区,不会产生内存碎片。

       

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

    • G1 – Garbage First 垃圾回收器
      参数1: -XX:+UseG1GC 打开G1的开关,JDK9之后默认不必要打开 参数2:-XX:MaxGCPauseMillis=毫秒值 最大停息的时间
       

    • 回收年代和算法:年轻代+老年代、复制算法
    • 优点:对比较大的堆如凌驾6G的堆回收时,延迟可控;不会产生内存碎片;并发标记的SATB算法效率高
    • 缺点:JDK8之前还不敷成熟
    • 实用场景:JDK8最新版本、JDK9之后发起默认利用


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

泉缘泉

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表