笨蛋学JVM

文章目录


JVM思维导图

1.认知JVM

1.1JVM = Java virtual Machine

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

1.2JVM的功能

1.2.1解释和运行

  • 对字节码文件中的指令,实时解释运行为机器码,让计算机执行

1.2.2内存管理

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

1.2.3即时编译(Just-In-Time=JIT)

  • 对**热点代码(很短的时间内被多次调用的代码)**进行优化,提升执行效率
解释并优化
保存至内存
直接调用
热点代码字节码指令
汇编和机器码
RAM/内存
再次执行
RAM/内存
  • 支持跨平台性
实时解释
实时解释
字节码指令
windows机器码
Linux机器码

1.3常见的JVM

在这里插入图片描述

1.4JVM的组成

JVM的组成
类加载器ClassLoader
JVM
运行时数据区域-JVM管理的内存
执行引擎
本地接口

在这里插入图片描述

  • 字节码文件加载到JVM中,JVM通过类加载器ClassLoader加载class字节码文件中的内容到JVM管理的内存中,在内存中通过执行引擎将字节码指令解释成机器码,执行引擎调用本地接口已经编译好的方法再JVM管理的内存中创建对象。

2.字节码文件

2.1字节码文件

  • 字节码文件中保存了源代码编译后的内容,以二进制的方式存储,无法直接用记事本打开阅读

2.1.1字节码文件的组成

在这里插入图片描述

基础信息指一般信息及接口
1魔数 2字节码文件对应的Java版本号 3访问标识public final等等 4父类和接口
常量池
保存了字符串常量 类或接口名 字段名主要在字节码指令中使用 通过编号的方式进行符号引用
字段
当前类或接口声明的字段信息
方法
当前类或接口声明的方法信息字节码指令
属性
类的属性 比如源码的文件名 内部类的列表等
  • 一般信息

在这里插入图片描述

2.1.1.1字节码文件的组成部分-Magic魔数
  • 文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容

  • 软件使用文件的头几个字节(字节头)去校验文件的类型,若软件不支持该种类型就会出错

    文件类型字节数文件头
    JPEG(jpg)3FFD8FF
    PNG(png)489504E47
    bmp2424D
    XML(xml)53C3F786D6C
    AVI(avi)441564920
    Java字节码文件(.class)4CAFEBABE
  • Java字节码文件种,将文件头称为Magic魔数

2.1.1.2字节码文件的组成部分-主副版本号
  • 主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号(主版本号-44 = 当前的JDK版本)

  • 版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容

2.1.1.3字节码文件的组成部分-常量池
  • 常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据
2.1.1.4字节码文件的组成部分-方法(避免相同的内容重复定义,节省空间)
  • 操作数栈是临时存放数据的地方,局部变量表是存放方法的局部变量的位置
  • iconst_0将值放入操作数栈中
  • istore_1将值从操作数栈中取出放入局部变量表数组中
  • iload_1将局部变量表数组中的指定位置的值放入操作数栈中
  • iinc 1 by 1将局部变量表数组中的指定位置的值增加1

2.2字节码常用工具

2.2.1javap -v命令

  • javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容
  • 直接输入javap查看所有参数
  • javap -v 字节码文件名称 查看具体的字节码信息。(若是jar包则需要先使用jar -xvf命令解压)

2.2.2arthas

新建文件夹 -> 执行curl -O https://arthas.aliyun.com/arthas-boot.jar ->运行程序,进入cmd窗口输入java -jar arthas-boot.jar -> 找到正在运行的程序输入对应的编号

dashboard -i 2000 -n 1:每隔两秒对运行的程序进行监控并输出到屏幕,一共执行3次
dump对已加载类的字节码文件到特定目录,dump -d 文件存放的路径 包名.文件名
jad反编译已加载类的源码,jad 包名.类名

2.3类的生命周期

描述了一个类加载、使用、卸载的整个过程

类的生命周期5阶段
类的生命周期7阶段
连接-Linking
卸载-Unloading
使用-Using
初始化-Initialization
解析-将常量池中的符号引用替换成指向内存的直接引用
准备-给静态变量赋初值
验证-验证内容是否满足Java虚拟机规范
加载-Loading

2.3.1加载阶段

通过类加载器将类的字节码信息加载到内存中,Java虚拟机在方法区和堆区中分别创建一个对象以备后用
  1. 类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息

  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中

  3. 生成一个InstanceKlass对象,保存类的所有信息,里面还包含实现特定功能比如多态的信息

  4. Java虚拟机会在堆中生成一份与方法去中数据类似的java.lang.Class对象

    作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8之后)

  • 在加载阶段,对于开发者来说,就只需要访问堆中的Class对象而不需要访问方法区中的所有信息

Java虚拟机就能很好的控制开发者访问数据的范围

在这里插入图片描述

因为方法区中的一些方法是使用C++编写的,而Java并不能访问,所以就将方法区InstanceKlass中我们所需要用到的一些数据复制到堆区中,而开发者就只需要访问堆区中java.lang.Class的对象,从而不再直接方法区中的对象,提高了数据安全性

查看内存中的对象

使用位于JDK安装目录下的lib文件夹中的sa-jdi.jar中自带的hsdb工具

在这里插入图片描述

启动命令java -cp sa-jdi.jar sun.jvm.hotspot.HSDB -> 输入对应的进程号,查看相关对象信息
在这里插入图片描述

2.3.2连接阶段

2.3.2.1验证
  • 验证主要是检测Java字节码文件是否遵守了Java虚拟机规范中的约束

  • 主要包含以下四部分:

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

    2. 元信息验证,比如类必须有父类,默认有父类Object

    3. 验证程序执行指令的语义,比如方法内的指令执行中跳转到其他方法总

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

在这里插入图片描述

2.3.2.2准备
  • 准备阶段只会为静态变量(static)分配内存并设置初始值,其初始值根据不同的基本数据类型和引用数据类型的初始值再区分

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

2.3.2.3解析
  • 解析阶段主要是将常量池中的符号引用(编号)替换为直接引用(内存地址)
  • 符号引用就是在字节码文件中使用编号来访问常量池中的内容
  • 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据

2.3.3初始化阶段

  • 初始化阶段会执行静态代码块中的代码,并为静态变量赋值

  • 初始化阶段会执行字节码文件中clinit(class init)部分的字节码指令

    clinit方法中的执行顺序与Java中编写的顺序是一致的

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

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

    1. 访问一个类的静态变量或静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化
    2. 调用Class.forName(String className)
    3. new一个该类的对象时
    4. 执行Main方法的当前类
  • 以下几种方式不会对类进行初始化

    1. 无静态代码块且无静态变量赋值语句
    2. 有静态变量的声明,但是没有赋值语句
    3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
  • 直接访问父类的静态变量,不会触发子类的初始化

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

  • 数组的创建不会导致数组中元素的类进行初始化

  • final修饰的变量若赋值的内容需要执行指令才能得出结果,则会执行clinit方法进行初始化

3.类加载器

3.1什么是类加载器

  • 类加载器(ClassLoader)是Java虚拟机提供给应用程序区实现获取类和接口字节码数据的技术

    类加载器只参与加载过程中的字节码获取并加载到内存这一部分

    在这里插入图片描述

3.2类加载器的分类

类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的
在这里插入图片描述

3.3JDK8及8以前版本的类加载器

3.3.1虚拟机底层实现

启动类加载器Bootstrap:加载Java中最核心的类
  • 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供,由C++编写的类加载器

  • 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等

    位置在C:\Program Files\Java\jdk1.8.0_361\jre\lib

在这里插入图片描述

  • 若想要运行Java程序,则需要在Java的基础运行环境的jre中运行

  • 当运行Bootstrap类加载器时,因为虚拟机是偏向底层应用的,而Java程序是偏向上层应用的,所以当运行该段程序时,会打印出null。(路径在 External Libraires -> <1.8> -> rt.jar -> java -> lang -> String 找到String类)

    public class ClassLoaderDemo {
        public static void main(String[] args) throws IOException {
    
            ClassLoader classLoader = String.class.getClassLoader();
            System.out.println(classLoader);
    
            //使程序运行完不再退出
            System.in.read();
        }
    }
    
通过启动类加载器加载自己的jar包:
  • 放入/jre/lib下进行扩展(不推荐,会出现文件名不符合jvm规范以致于文件名不匹配的问题)
  • 使用参数进行扩展(推荐,使用-Xbootclasspath/a:jar包路径/jar包名

3.3.2Java中实现

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

在这里插入图片描述

  • URLClassLoader:将指定jar包的字节码数据加载进来
  • ClassLoader:规范了整个类加载器的执行过程
扩展类加载器Extension:加载扩展Java比较通用的类(不是特别重要)
  • 扩展类加载器(Extension Class Loader)是JDK中提供的使用Java编写的类加载器

  • 默认加载Java安装目录/jre/lib/ext下的类文件

    位置在C:\Program Files\Java\jdk1.8.0_361\jre\lib\ext

    在这里插入图片描述

通过扩展类加载器加载自定义jar包
  • 放入/jre/lib/ext下进行扩展(不推荐,尽可能不要更改JDK安装目录中的内容)

  • 使用参数进行扩展(推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,但是会覆盖掉原始目录

    windows:“-Djava.ext.dirs=原始jar包目录;自定义jar包目录”

    macos/Linux:“-Djava.ext.dirs=原始jar包目录:自定义jar包目录”)


应用程序类加载器Application:加载应用classpath使用的类文件/第三方依赖中的字节码文件
  • 应用程序类加载器Application加载的jar包覆盖了启动类加载器Bootstrap和扩展类加载器Extension

3.3.3双亲委派机制(解决类到底由谁加载)

3.3.3.1双亲委派机制的作用
  1. 保证类加载的安全性

    通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性

  2. 避免重复加载

    双亲委派机制让一个类只能被同一个类加载器加载,就可以避免同一个类被多次加载,减少加载过程中的性能开销

3.3.3.2类加载的双亲委派机制
  • 当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载

  • 向下委派加载起到了加载优先级的作用

    在这里插入图片描述

  • 当类加载器发现一个类在自己的加载路径中就会去加载这个类,若没有发现,则从下往上继续查找是否被加载过,若发现被加载了则直接将这个类进行加载,若一直到最顶层的类加载器都没有被加载,则由顶向下进行加载。

  • 若所有类加载器都无法加载该类,则会抛出类无法找到的错误

3.3.3.3双亲委派优先级
  • 若一个类重复出现在三个类加载器的加载位置,则由启动类加载器加载,根据双亲委派机制,其优先级是最高的
  • 若项目中创建java.lang.String类,不会被加载,因为根据自底向上的查找类加载器过程中会发现启动类加载器早就加载了
3.3.3.4主动加载一个类
  1. 使用Class.forName方法,使用当前类的类加载器去加载指定的类
  2. 获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载
    //获取main方法所在类的类加载器,应用程序类加载器
    ClassLoader classLoader = Demo.class.getClassLoader();
    System.out.println(classLoader);
        
    //使用应用程序类加载器加载 com.aaa.bbb.CCC
    Class<?> clazz = classLoader.loadClass("com.aaa.bbb.CCC");
    System.out.println(clazz.getClassLoader());
    
3.3.3.5父类加载器
  • 每个java实现的类加载器中保存类一个成员变量叫"父"(Parent)类加载器,可以理解为是其上级而不是继承关系

  • 应用程序类加载器的父类加载器是扩展类加载器

  • 扩展类加载器的父类因为启动类加载器使用C++编写,获取不到,所以为null

  • 启动类加载器使用C++编写,没有父类加载器

    parent=扩展类记载器
    parent=null
    应用程序类加载器Application
    扩展类加载器Extension
    启动类加载器Bootstrap
3.3.3.6双亲委派机制的优势
  • 避免恶意代码替换JDK中的核心类库,确保核心类库的完整性和安全性
  • 避免一个类重复地加载

3.3.4打破双亲委派机制

打破双亲委派机制
自定义类加载器并重写loadClass方法就可以将双亲委派机制的代码去除
自定义类加载器
Osgi框架实现了一套新的类加载器机制 允许同级之间进行类的加载
Osgi框架的类加载器
利用上下文类加载器加载类 比如JDBC和JNDI等
线程上下文类加载器
3.3.4.1自定义类加载器(重写findClass方法)
  • Tomcat程序中可以运行多个Web应用,若两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且两个类不是同一个类
  • 若不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中的MyServlet类就无法被加载了

  • Tomcat使用了自定义类加载器来实现应用之间类的隔离,每个应用都会有一个独立的类加载器加载对应的类,就不再执行双亲委派机制

  • ClassLoader中包含了4个核心方法
    • public Class<?> loadClass(String name)
      

      类加载的入口,提供了双亲委派机制。内部会调用findClass

    • public Class<?> findClass(String name)
      

      由类加载器子类实现,用来加载字节码文件中的信息,获取二进制数据调用defineClass

      URLClassLoader会根据文件路径去获取类文件中的二进制数据

    • protected final Class<?> defineClass(String name,byte[] b,int off,int len)
      

      做一些类名的校验,调用虚拟机底层的方法将字节码信息加载到虚拟机内存中

      执行到这,类加载阶段执行完毕

    • protected final void resolveClass(CLass<?> c)
      

      执行类生命周期的连接阶段

  • 核心代码

    //parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull
    //否则调用父类加载器的加载方法
    if(parent != null){
        c=parent.loadClass(name,false);
    }else{
    //调用启动类加载器加载类
        c=findBootstrapClassOrNull(name);
    }
    
    //若父类加载器都未加载成功,则由本类加载器加载
    if(c==null)
        c=findClass(name);
    
  • 自定义类加载器不手动设置parent的话,则默认父类是由getSystemClassLoader()方法设置,该方法返回一个应用程序类加载器AppClassLoader

    重写private ClassLoader(Void unused, ClassLoader parent)方法完成自定义父类

  • 两个自定义类加载器加载相同限定名的类,不会冲突,因为在同一个java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类

3.3.4.2线程上下文类加载器(由启动类加载器委派应用程序类加载器去加载类)
  • JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动

  • DriverManager类位于rt.jar包中,由启动类加载器加载

  • 依赖中的mysql驱动对应的类,由应用程序类加载器加载

  • DriverManage使用SPI机制

    (Service Provider Interface,是JDK内置的服务提供发现机制

    工作原理:

    1. 在ClassPath路径下的META-INF/service文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现
    2. 使用ServiceLoader加载实现类),最终加载jar包中对应的驱动类
  • SPI中使用了线程上下文中保存的类加载器进行类的加载,而且该类加载器一般是应用程序类加载器

  • 使用自定义类加载器加载:Thread.currentThread().setContextClassLoader(自定义类加载器名)

    在这里插入图片描述

线程上下文类加载器打破了双亲委派机制

​ 因为这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制

线程上下文类加载器未打破双亲委派机制

​ 因为JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,依旧是通过启动类加载器向下委派直至委派给应用程序类加载器去加载类,类的加载依然遵循双亲委派机制

3.3.4.3OSGi模块化
  • OSGi模块化框架,存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的功能
  • 热部署功能就是指在服务不停止的情况下,动态地更新字节码文件到内存中
    在这里插入图片描述

3.4JDK8之前及JDK9之后的类加载器对比

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

  • JDK9引入了module的概念,类加载器在设计上发生了变化

    1. 启动类加载器使用java编写,位于jdk.internal.loader.ClassLoaders类中

      Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件

      启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一

    2. 扩展类加载器被替换成了平台类加载器(Platform Class Loader)

      平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件

      平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑

4.运行时数据区域

4.1总览

  • Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区

    线程共享
    线程不共享
    方法区
    程序计数器
    Java虚拟机栈
    本地方法栈
    • 线程共享:每个线程都可以获取数据并使用,存在线程安全的问题
    • 线程不共享:每当创建一个线程后,就会有一个程序计数器、Java虚拟机栈、本地方法栈,提高安全性

4.2程序计数器

  • 也叫PC寄存器,每个线程都会通过程序计数器记录当前要执行的字节码指令的地址
  • 控制程序指令的执行,实现分支、跳转、异常等逻辑
  • 在多线程执行的情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续运行

4.2.1是否会发生内存溢出

  • 内存溢出指的是程序正在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限
  • 程序计数器中的每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的
  • 无需对程序计数器做任何处理

4.3Java虚拟机栈

  • 采用栈的数据接口来管理方法调用中的基本数据,先进后出,每一个方法的调用都使用一个栈帧来保存

    在这里插入图片描述

  • 若抛出异常,则会在控制台打印出当前栈中所保存的各个栈帧的情况,获取到栈帧里面保存的对应的方法名

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

4.3.1栈帧的组成

栈帧的组成
局部变量表的作用是在运行过程中存放所有的局部变量
局部变量表
帧数据主要包含动态连接和方法出口以及异常表的引用
帧数据
操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
操作数栈

4.3.2局部变量表LocalVariableTable

  • 局部变量表的作用是在运行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变量表的内容
  • 字节码文件中的局部变量表
    • Nr.:局部变量的编号
    • 起始PC:从哪一行字节码指令开始可以去访问该局部变量
    • 长度:该局部变量的长度,限制生效范围
    • 序号:对应的槽的起始序号
    • 名字:局部变量存放的内存地址以及局部变量名
  • 栈帧中的局部变量表是一个数组,数组中的每一个位置称之为槽(slot),long和double类型占用两个槽,其他类型占用一个槽
  • 实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址
  • 局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量
  • 局部变量表中的槽为了节省空间,是可以复用的,当一旦某个局部变量不再生效,则当前槽就可以再次被使用

4.3.3操作数栈

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

4.3.4帧数据

  • 当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址动态链接就保存了编号到运行时常量池的内存地址的映射关系
  • 方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址
  • 异常表存放的是代码中出现异常或未出现异常的处理信息,包含了try代码块和catch代码块执行后跳转到的字节码指令位置

4.3.5栈内存溢出

  • Java虚拟机栈若栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出
  • Java虚拟机栈内存溢出时会出现StackOverflowError的错误

在这里插入图片描述

4.3.5.1虚拟机栈默认大小

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

4.3.5.2设置虚拟机栈的大小

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

语法:-Xss栈大小

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

4.3.5.3注意事项
  1. 与-Xss类似,也可以使用-XX:ThreadStackSize调整标志来配置堆栈大小

    格式为:-XX:ThreadStackSize=1024

  2. HotSpot JVM对栈大小的最大值和最小值有要求

    Windows(64位)下的JDK8测试最小值为180k,最大值为1024m

  3. 局部变量过多、操作数栈深度过大也会影响栈内存大小

  4. 一般情况下,即便使用了递归,栈的深度最多只能到几百,不会出现栈的溢出。

    参数可以手动指定为-Xss256k节省内存

4.4本地方法栈

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

在这里插入图片描述

4.5堆

  • Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上

  • 栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享

    在这里插入图片描述

4.5.1堆空间

  • 堆空间有三个需要关注的值
    • used --> 当前已使用的堆内存
    • total --> Java虚拟机已经分配的可用堆内存
    • max --> Java虚拟机可以分配的最大堆内存
  • 使用dashboard命令查看,可以手动指定刷新频率(不指定默认5秒一次):dashboard -i 刷新频率(毫秒)
  • 随着堆中的对象增多,当total可以使用的内存即将不足时,Java虚拟机会将继续分配内存给堆
  • 若堆内存不足,Java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等

4.5.2配置堆空间参数

  • 若不设置任何的虚拟机参数,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必须大于2MB,Xms必须大于1MB

  • arthas中的heap堆内存使用了JMX技术中内存获取方式,这种方式与垃圾回收期有关,计算的是可以分配对象的内存,而不是整个内存
  • Java服务端程序开发时,建议将-Xmx和-Xms设置为相同的值(total==max),在程序启动后可使用的总内存就是最大内存,而无需向Java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况
  • -Xmx具体设置的值与实际的应用程序运行环境有关

4.6方法区

4.6.1方法区的组成

  • 方法区存放的是基础信息的位置,线程共享,主要包含三部分内容:
    • 类的元信息–>保存了所有类的基本信息
    • 运行时常量池–>保存了字节码文件中的常量池内容
    • 字符串常量池–>保存了字符串常量

4.6.2方法区的作用

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

  • 方法区除了存储类的元信息之外,还存放了运行时常量池。常量池中存放的是字节码中的常量池内容

  • 字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池

    当常量池加载到内存之中后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池


  • 方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同

    HotSpot设计如下:

    • JDK7及之前的版本将方法区存放在堆区域中的永久代空间,有最大值内存空间,堆的大小由虚拟机参数来控制(查看ps_perm_gen)
    • JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,占用的是操作系统上的空间,无最大值内存空间,默认情况下只要不超过操作系统承受的上限,可以一直分配(查看metaspace)

    在这里插入图片描述

4.6.3JDK7和JDK8的虚拟机参数

  • JDK7将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数-XX:MaxPermSize=值来控制
  • JDK8将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用**-XX:MaxMetaspaceSize=值**将元空间最大大小进行限制

4.7字符串常量池

  • 方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池(StringTable)

  • 字符串常量池存储在代码中定义的常量字符串内容。比如“123”就会被放入字符串常量池

    在这里插入图片描述

    4.7.1字符串常量池和运行时常量池

    在这里插入图片描述

    4.7.2intern方法

  • String.intern()可以手动将字符串放入字符串常量池中

    String a="1";
    String b="2";
    String c="12";
    //由于在底层中使用了StringBuilder,
    //最后使用了new String,所以d的值在堆中
    String d=a+b;
    
    //是直接通过在字符串常量池中相加,
    //所以e的值还是在字符串常量池中
    String e="1"+"2";
    
    -----------------------------------------------------------
    String input1 =scanner.next().intern();
    String input2 =scanner.next().intern();
    
    //若input1和input2输入的值相同,则会相等
    input1 == input2;// true
    
  • JDK6版本的intern()方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代中字符串的引用。JVM启动时会把Java加入到常量池中

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

  • JDK7及之后版本中由于字符串常量池在堆中,所以intern()方法会将第一次遇到的字符串的引用放入字符串常量池中

    JDK7及之后的版本,静态变量是存放在堆中的Class对象,脱离了永久代

4.8直接内存

  • 直接内存并不在《Java虚拟机规范》中,不属于Java运行时的内存区域

    使用直接内存,是为了解决:

    1. Java堆中的对象若不再使用要回收,回收时会影响对象的创建和使用

    2. IO操作比如读文件,需要将文件读入直接内存(缓存区)再把数据复制到Java堆中

      现在直接放入直接内存,同时Java堆上维护直接内存的引用,减少了数据复制的开销

    3. JDK8之后,主要为了保护在方法区中的数据

4.8.1配置直接内存

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

  • 单位k或k表示千字节,m或M表示兆字节,g或G表示千兆字节。

    默认不设置该参数情况下,JVM自动选择最大分配的大小

5.自动垃圾回收

5.1内存泄漏

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

5.2Java的内存管理

  • 为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection 简称GC)机制

    通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行会后

5.3自动垃圾回收/手动垃圾回收

5.3.1自动垃圾回收

  • 自动根据对象是否使用由虚拟机来回收对象

    • 优点:降低程序员实现难度、降低对象回收bug的可能性
    • 缺点:程序员无法控制内存回收的及时性
  • 线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。

    而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存

5.3.2手动垃圾回收

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

5.4自动垃圾回收场景

  1. 解决系统僵死的问题

    大厂的系统出现的许多系统僵死问题都与频繁的垃圾回收有关

  2. 性能优化

    对垃圾回收器进行合理的设置可以有效地提升程序的执行性能

  3. 高频面试题

    常见的垃圾回收器、常见的垃圾回收算法、四种引用、项目中有用哪一种垃圾回收器

5.5方法区的回收

  • 方法区中能回收的内容主要就是不再使用的类

    判定一个类可以被卸载,需要同时满足下面三个条件:

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

    Class<?> clazz = loader.loadClass("com.a.b.C");
    Object o = clazz.newInstance();
    o = null;
    
  2. 加载该类的类加载器已经被回收

    URLClassLoader loader = new URLClassLoader(
    	new URL[] {new URL("file:D:\\lib\\")}
    );
    loader=null;
    

    例:每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件

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

    Class<?> clazz = loader.loadClass("com.a.b.C");
    clazz = null
    
  • 打印类被加载的日志:-XX:+TraceClassloading
  • 打印类被卸载的日志:-XX:+TraceClassUnloading

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

  • 语法:System.gc();

  • 注意事项:

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

5.6堆回收

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

  • 根据对象是否被引用决定,若对象被引用了,则说明该对象还在使用,不允许被回收

5.6.1引用计数法和可达性分析法

5.6.1.1引用计数法

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

  • 优点:实现简单

  • 缺点:

    • 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响

    • 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题

      A、B实例对象在栈上已经没有变量引用了,由于计数器还是1无法回收,出现了内存泄漏

查看垃圾回收日志的信息:使用 -verbose:gc参数

5.6.1.2可达性分析法
  • 优点:
    • 解决了引用计数法的循环引用问题
    • 不需要在对对象的引用发生变化时增加或减少引用的技术

可达性分析算法将对象分为两类,且对象与对象之间存在引用关系:

  • 垃圾回收的根对象(GC Root),GC Root一般不会被回收
  • 普通对象

可达性分析算法指的是如果从某个普通对象到GC Root对象是可达的(也就是普通对象能通过引用链找到GC Root对象),对象就不可被回收;若引用链不存在就可以被回收。

在这里插入图片描述

5.6.1.3GC Root对象
  • Thread线程对象,引用线程栈帧中的方法参数、局部变量等

    比如main主线程Thread对象

  • System Class系统类加载器加载的java.lang.Class对象,引用类中的静态变量

    比如sun.misc.Launcher系统类加载器

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

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

5.6.1.4查看GC Root

通过arthas和eclipse Memory Analyzer(MAT)工具查看GC Root

  • 使用arthas的heapdump命令将堆内存快照保存到本地磁盘中
  • 使用MAT工具打开堆内存快照文件
  • 选择GC Roots功能查看所有的GC Root

5.6.2五种对象引用

  1. **强引用:**即GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收;通过强引用引用的对象就是不可被回收的,可以被保留

  2. **软引用:**相对于强引用是一种比较弱的引用关系,当程序使用完一个对象后,就会解除强引用对象;若一个对象只有软引用关联到它,当程序内存不足时,就会将引用中的数据进行回收,释放一定的堆内存

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

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

    在这里插入图片描述

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

  5. 终结器引用:指的是在对象需要被回收时,对象将会被放置在Finalizer类中的引用队列中,并在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做,如果耗时过长会影响其他对象的回收。

5.6.2.1软引用的执行过程
  1. 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)
  2. 内存不足时,虚拟机尝试进行垃圾回收
  3. 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象
  4. 如果依然内存不足,抛出OutOfMemory异常
byte[ ] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[ ]> softReference = new SoftReference<byte[ ]>(bytes);
System.out.println(softReference.get());

在这里插入图片描述

5.6.2.2内存不足如何回收掉软引用对象的数据
SoftReference提供了一套队列机制
  1. 软引用创建时,通过构造器传入引用队列
  2. 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
  3. 通过代码遍历引用队列,将SoftReference的强引用删除
  • 软引用也可以使用继承自SoftReference类的方式来实现,StudentRef类就是一个软引用对象

    通过构造器传入软引用包含的对象,以及引用队列

5.6.2.3实战-软引用实现学生数据的缓存

在这里插入图片描述

5.6.2.4弱引用的执行过程
byte[ ] bytes = new byte[1024 * 1024 * 100];
WeakReference<byte[ ]> weakReference = new WeakReference<byte[ ]>(bytes);
bytes=null;
System.out.println(weakReference.get());

System.gc();

System.out.println(weakReference.get());

5.6.3垃圾回收算法

5.6.3.1核心思想
  • 垃圾回收要做的两件事:
    1. 找到内存中存活的对象
    2. 释放不再存活对象的内存,使得程序能再次利用这部分空间
5.6.3.2四种垃圾回收算法:
标记-清除算法/MarkSweep GC
复制算法/Copying GC
标记-整理算法/Mark Compact GC
分代GC/Generational GC
  • 1960年发布第一个GC算法:标记-清除算法
  • 1963年发布复制算法
  • 标记-整理算法是在标记-清除算法上的基础优化而来
  • 分代GC算法是根据情况混合使用前面三种算法
5.6.3.3STW
  • Java垃圾回收过程会通过单独的GC线程来完成,但无论使用哪种GC算法,都会有部分阶段需要停止所有的用户线程,该过程被称之为Stop The World简称STW,若STW时间过长则会影响用户的使用
5.6.3.4垃圾回收算法的评价标准
  • 吞吐量:CPU用于执行用户代码的时间与CPU总执行时间的比值,即吞吐量=执行用户代码时间 / (执行用户代码时间 + GC时间),吞吐量数值越高,垃圾回收的效率就越高

  • 最大暂停时间:所有在垃圾回收过程中的STW时间最大值,最大暂停时间越短,用户使用系统时收到的影响就越短

  • 堆使用效率:不同垃圾回收算法,对堆内存的使用方式是不同的,从堆使用效率上来说,标记清除算法优于复制算法

    在这里插入图片描述

  • 注意:

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

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

5.6.3.5标记-清除算法
  1. 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有的存活对象
  2. 清除阶段,从内存中删除没有被标记也就是非存活对象

在这里插入图片描述


  • 优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可
  • 缺点:
    • 碎片化问题:由于内存是连续的,在对象被删除后,内存中会出现很多细小的可用内存单元,若需要一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配
    • 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间
5.6.3.6复制算法
  1. 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)
  2. 在垃圾回收GC阶段,将From中存活对象复制到To空间
  3. 将两块空间的From和To名字互换

  • 完整例子

    1. 将堆内存分割成两块From空间 / To空间,对象分配阶段,创建对象

      在这里插入图片描述

    2. GC阶段开始,将GC Root搬运到To空间

      在这里插入图片描述

    3. 将GC Root关联的对象,搬运到To空间

      在这里插入图片描述

    4. 清理From空间,并把名称互换

      在这里插入图片描述


  • 优点:

    • 吞吐量高:复制算法只需要遍历一次存活对象复制到To空间即可,性能较标记-整理算法好,但是不如标记-清除算法,因为标记-清除算法不需要进行对象的移动
    • 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间
  • 缺点:

    • 内存使用效率低:每次只能让一半的内存空间来为创建对象使用
5.6.3.7标记-整理算法
  • 也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案
  1. 标记阶段,将所有存活的对象进行标记,Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象
  2. 整理阶段,将存活对象移动到堆的一端,清理掉存活对象的内存空间

在这里插入图片描述


  • 优点:
    • 内存使用效率高,整个堆内存都可以使用,不会向复制算法只能使用半个堆内存
    • 不会发生碎片化,在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
  • 缺点:
    • 整理阶段的效率不高,整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two-FInger、表格算法、ImmixGC等高效的整理算法此阶段的性能
5.6.3.8分代GC算法
  • 现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)

  • 分代垃圾回收将整个内存区域划分为年轻代和老年代

    在这里插入图片描述

arthas查看分代之后的内存情况
  • 在JDK8中,添加**-XX:+UseSerialGC**参数使用分代回收的垃圾回收器,运行程序

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

    在这里插入图片描述

  1. 分代回收时,创建出来的对象,首先会被放入Eden伊甸园区

  2. 随着对象在Eden区越来越多,若Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC

  3. Minor GC会把需要Eden和From需要回收的对象回收,把没有回收的对象放入To区

  4. 接下来,S0会变成To区,S1会变成From区,当Eden区满时再往里放入对象,依然会发生Minor GC

  5. 此时会回收Eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0

    注意:每次Minor GC都会为对象记录他的年龄,初始值为0,每次GC完加1

  6. 如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代

  7. 当老年代中空间不足,无法放入新的对象时,现场时Minor GC如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收

  8. 若Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常

5.6.4垃圾回收器

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

主要原因:
  • 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能
  • 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员选择灵活度比较高
  • 分代的设计中允许只回收新生代(Minor GC),若能满足对象分配的要求就不需要对整个堆进行回收(Full GC),STW时间就会减少
5.6.4.2垃圾回收器的组合关系

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

在这里插入图片描述

年轻代-Serial垃圾回收器
  • Serial是一种单线程串行回收年轻代的垃圾回收器
  • 回收年代及算法:年轻代-复制算法
  • 优点:单CPU处理器下吞吐量非常出色
  • 缺点:多CPU下吞吐量不如其他垃圾回收器,堆若偏大会让用户线程处于长时间的等待
  • 适用场景:Java编写的客户端程序或硬件配置有限的场景
老年代-SerialOld垃圾回收器
  • SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
  • -XX:+UseSerialGC 新生代、老年代都使用串行回收器
  • 回收年代及算法:老年代-标记整理算法
  • 优点:单CPU处理器下吞吐量非常出色
  • 缺点:多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
  • 适用场景:与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用
年轻代-ParNew垃圾回收器
  • ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收
  • -XX:+UseParNewGC 新生代使用ParNew回收器,老年代使用串行回收器
  • 回收年代及算法:年轻代-复制算法
  • 优点:多CPU处理器下停顿时间较短
  • 缺点:吞吐量和停顿时间不如G1,所以在JDK9之后不建议使用
  • 适用场景:JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
老年代-CMS(Concurrent Mark Sweep)垃圾回收器
  • CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程等待的时间

  • XX:+UseConcMarkSweepGC

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

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

  • 缺点

    • 内存碎片问题

      CMS会在Full GC时进行碎片的整理,但是会导致用户线程暂停,可以使用

      -XX:CMSFullGCsBeforeCompaction=N 参数(默认为0)调整N次Full GC之后再整理

    • 退化问题:在某些特定的情况下会退化成Serial Old的单线程垃圾回收器

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

    • 浮动垃圾问题:在清理过程中,有些垃圾可能会回收不掉

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

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


CMS执行步骤
  1. 初始标记,用极短的时间标记出GC Roots能直接关联到的对象
  2. 并发标记,标记所有的对象,用户线程不需要暂停
  3. 重新标记,由于并发标记阶段有些对象会发生变化,存在错标、漏标等情况,需要重新标记
  4. 并发清理,清理死亡的对象,用户线程不需要暂停

在这里插入图片描述

年轻代-Parallel Scavenge垃圾回收器
  • Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点
  • 回收年代及算法:年轻代-复制算法
  • 优点:吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数
  • 缺点:不能保证单次的停顿时间
  • 适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象,比如大数据的处理,大文件的导出

  • 最大暂停时间-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)垃圾回收器
  • Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小
  • CMS关注暂停时间,但是吞吐量方面会下降

G1则是将两种垃圾回收器的优点融合

  • 支持巨大的堆空间回收,并有较高的吞吐量
  • 支持多CPU并行垃圾回收
  • 允许用户设置最大暂停时间

  • G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的

  • 分为Eden、Survivor、Old区

  • Region的大小通过堆空间大小/2048计算得到,也可以通过参数 -XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M

  • G1垃圾回收的两种方式

    • 年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数

      -XX:MaxGcPauseMillis=n(默认200)设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间

    • 混合回收(Mixed GC)

在这里插入图片描述


  • 执行流程
  1. 新创建地对象会存放在Eden区,当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC

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

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

    G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域

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

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

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

  7. 多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法完成

    • 混合回收分为:

      • 初始标记:标记GC Roots引用的对象为存活
      • 并发标记:将第一步中标记的对象引用的对象,标记为存活
      • 最终标记:标记一些引用改变漏标的对象,不管新创建、不再关联的对象
      • **并发清理:**使用复制算法将存活对象复制到别的Region不会产生内存碎片

      在这里插入图片描述

  8. G1堆老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1名称的由来

  9. 最后清理阶段使用复制算法,不会产生内存碎片

注意

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

  • 参数1:-XX:+UseG1GC 打开G1的开关,JDK9之后默认不需要打开
  • 参数2:-XX:MaxGCPauseMillis=毫秒值最大暂停的时间
  • 回收年代及算法:年轻代+老年代,复制算法
  • 优点
    • 对比较大的堆如超过6G的堆回收时,延迟可控,
    • 不会产生内存碎片,
    • 并发标记的SATB算法效率高
  • 缺点:JDK8之前还不够成熟
  • 适用场景:JDK8最新版本、JDK9之后建议默认使用
5.6.4.3垃圾回收器的选择

JDK8及之前:

  • ParNew + CMS(关注暂停时间)
  • Parallel SCavenge + Parallel Old(关注吞吐量)
  • G1(JDK8之前不建议,较大堆并且关注暂停时间)

JDK9之后:

  • G1(默认)

从JDK9之后,JDK默认的垃圾回收器已经修改为G1,所以在生产环境上使用G1

ByteBuddy框架

  • 引入依赖

    <dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.12.23</version>
    </dependency>
    
  • 创建ClassWriter对象

    ClassWriter classWriter = new ClassWriter(0);
    
  • 调用visit方法,创建字节码数据

    classWriter.visit(Opcodes.v1_7,Opcodes.ACC_PUBLIC,name,null,"java/lang/Object",null);
    byte[] bytes=classWriter.toByteArray();
    

Arthas

Arthas常用命令

参数列表:https://arthas.aliyun.com/doc/commands.html

  • **dashboard:**dashboard -i 2000 -n 1:每隔两秒对运行的程序进行监控并输出到屏幕,一共执行3次

  • **memory:**展示当前运行的程序的内存

  • **dump:**dump对已加载类的字节码文件到特定目录,dump -d 文件存放的路径 包名.文件名

  • **jad:**jad反编译已加载类的源码,jad 包名.类名

  • **sc:**搜索出所有已经加载到JVM中的Class信息,输出java.lang.String的类信息:sc -d java.lang.String

  • **heapdump:**heapdump 目录路径 文件名.hprof 将堆内存快照保存到本地磁盘中

  • **classloader:**classloader查看类加载器的继承树,urls,类加载信息,使用classloader区getResource
    查看所有的类加载器的hash值:classloader -l,查看指定的classloader的jar包:classloader -c hash值,查看类加载器的父子关系:classloader -t在这里插入图片描述

Arthas不停机解决线上问题(热部署)

  1. 在有问题的服务器上部署一个arthas,并启动
  2. jad --source-only 类全限定名 > 目录/文件名.java
    jad命令反编译,然后可以用其它编辑器,比如vim来修改源码
  3. mc -c 类加载器的hashcode 目录/文件名.java -d 输出目录
    mc命令用来编译修改过的代码
  4. retransform class文件所在目录/xxx.class
    用retransform命令来重新加载字节码文件加载到内存中,起到了刷新的作用

注意:

  1. 程序重启之后,字节码文件会恢复,除非将class文件放入jar包中进行更新
  2. 使用retransform不能添加方法或者字段,也不能更新正在执行中的方法
  • 29
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值