JVM

JVM

初识JVM

什么是JVM?

  • JVM 本质上是一个运行在计算机上的程序,它的职责是运行 java字节码文件

JVM的功能

  1. 解耦和运行
  • 对字节码文件中的指令。实时的解释成机器码,让计算机执行
  1. 内存管理
  • 自动为对象、方法等分配内存
  • 自动的垃圾回收机制,回收不再使用的对象
  1. 即时编译
  • 对热点代码进行优化,提升执行效率

即时编译

  • Java语言如果不做任何优化,性能不如C、C++等语言

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

  • JVM提供了 即时编译(Just-in-Time 简称JIT) 进行性能的优化,最终能达到接近C、C++语言的运行性能,甚至在特定场景下实现超越

Java虚拟机规范

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

常见的JVM虚拟机有哪些?

  • HotSpot、GraalVM、OpenJ9等,另外DragonWell龙井JDK也提供了一款功能增强版的JVM。其中使用最广泛的是Hot5pot虚拟机

字节码文件详解

Java虚拟机的组成

字节码文件的组成

常量池

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

方法

  • 字节码中的方法区域是存放 字节码指令 的核心位置,字节码指令的内容存放在方法的Code属性中
  • 操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置

问题

int i = 0; i = i++; 最终i的值是多少?

答案是0, 通过分析字节码指令发现,i++先把0取出来放入临时的操作数栈中,接下来对 i 进行加1, i 变成了1,最后再将之前保存的临时值0放入 i,最后 i 就变成了0

字节码文件常见工具

javap -v命令

  • javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。**适合在服务器上查看字节码文件内容 **
  • 直接输入javap查看所有参数
  • 输入 javao -v 字节码文件名称 查看具体的字节码信息

jclasslib插件

  • jclasslib也有Idea插件版本,建议开发时使用Idea插件版本,可以在代码编译之后实时看到字节码文件内容

阿里Arthas

  • Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修 改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率
  • 官网
  • dump 类的全限定名:dump 已加载类的字节码文件到特定目录
  • jad 类的全限定名:反编译已加载类的源码

总结

  1. 如何查看字节码文件?
  • 本地文件可以使用 jclasslib工具查看,开发环境使用 jclasslib 插件
  • 服务器上文件使用 javap 命令直接查看,也可以通过 arthas 的 dump 命令导出字节码文件再查看本地文件。还可以使用 jad 命令反编译出源代码
  1. 字节码文件的核心组成有哪些?

类的生命周期

生命周期概述

加载阶段

  • 1、加载(Loading)阶段第一步是 类加载器 根据类的权限定名通过不同的渠道以二进制流的方式获取字节码信息。程序员可以使用Java代码拓展的不同的渠道

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

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

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

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

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

连接阶段

验证
  • 连接(Linking)阶段的第一个环节时验证,验证的主要目的时检测 Java 字节码文件是否遵守了《Java虚拟机规范》中的约束。**这个阶段一般不需要程序员参与 **
  • 主要包含如下四部分
  1. 文件格式验证,比如文件是否以 0xCAFEBABE 开头,主次版本号是否满足当前 Java 虚拟机版本要求
  2. 元信息验证,例如类必须有父类(super不能为空)
  3. 验证程序执行指令的语义,比如方法内的指令执行到一半强行跳转到其他方法中去
  4. 符号引用验证,例如是否访问了其他类中 private 的方法等
准备
  • 准备阶段为静态变量(static)分配内存并设置初始值
数据类型 初始值
int 0
long 0L
short 0
char ‘\u0000’
byte 0
boolean false
double 0.0
引用数据类型 null
  • final 修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值
解析
  • 解析阶段主要是将常量池中的符号引用替换为直接引用
  • 符号引用就是在字节码文件中使用编号来访问常量池中的内容

初始化阶段

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

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

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

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

  1. 访问一个类的静态变量或者静态方法,注意变量是 final 修饰的并且等号右边是常量不会触发初始化
  2. 调用 Class.forName (String className)
  3. new 一个该类的对象时
  4. 执行 Main 方法的当前类
  • clinit 指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的
  1. 无静态代码块且无静态变量赋值语句
  2. 有静态变量的声明,但是没有赋值语句
  3. 静态变量的定义使用 final 关键字,这类变量会在准备阶段直接进行初始化
  • 直接访问父类的静态变量,不会触发子类的初始化
  • 子类的初始化 clinit 调用之前,会先调用父类的 clinit 初始化方法
笔试题

如果把new B02()去掉会怎么样呢?

总结

  1. 加载:根据类的全限定名把字节码文件的内容加载并转换成合适的数据放入内存中,存放在方法区和堆上
  2. 连接-验证:魔数、版本号等验证,一般不需要程序员关注
  3. 连接-准备:为静态变量分配内存并设置初始值
  4. 连接-解析:将常量池中的符号引用(编号)替换为直接引用(内存地址)
  5. 初始化:执行静态代码块和静态变量的赋值

要点:

  1. 静态变量的定义使用 final 关键字,这类变量会在准备阶段直接进行初始化(除非要执行方法)
  2. 直接访问父类的静态变量,不会触发子类的初始化。子类的初始化 clinit 调用之前,会先调用父类的 clinit 初始化方法

类加载器

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

类加载器的分类

  • 类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的
  • 类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种

  • 类加载器的详细信息可以通过classloader命令查看:

classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource

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

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

  • 放入 jre/lib 下进行扩展

    不推荐,尽可能不要去更改 JDK 安装目录中的内容,会出现即使放进去由于文件名不匹配的问题也不会正常的被加载

  • 使用参数进行扩展

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

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

扩展类加载器
  • 扩展类加载器(Extension Class Loader)是JDK中提供的,使用 Java 编写的类加载器
  • 默认加载Java安装目录/jre/lib/ext下的类文件

通过扩展类加载器去加载用户 jar 包

  • 放入 jre/lib 下进行扩展

    不推荐,尽可能不要去更改 JDK 安装目录中的内容,会出现即使放进去由于文件名不匹配的问题也不会正常的被加载

  • 使用参数进行扩展

    推荐,使用 -Djava.ext.dirs=jar 包目录进行扩展,这种方式会覆盖掉原始目录,可以用 ;(windows):(macos/linux) 追加上原始目录

  • 类加载器的加载路径可以通过 classloader–c hash 值查看

双亲委派机制

在Java中如何使用代码的方式去主动加载一个类呢?

方式1:使用Class.forName方法,使用当前类的类加载器去加载指定的类

方式2:获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载

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

  • 应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空。

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

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

  • 在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器

  • 如果类加载的parent为null,则会提交给启动类加载器处理

  • 如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载。所以看上去是自顶向下尝试加载

  • 第二次再去加载相同的类,仍然会向上进行委派,如果某个类加载器加载过就会直接返回

双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载

  • 另一个案例:com.itheima.my.B这个类在当前程序的classpath中,看看是如何加载的

问题
  1. 重复的类:如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?
  • 启动类加载器加载,根据双亲委派机制,它的优先级是最高的
  1. String类能覆盖吗?在自己的项目中去创建一个java.lang.String类,会被加载吗?
  • 不能,会交由启动类加载器加载在 rt.jar 包中的 String 类
  1. 类加载器的关系:这几个加载器彼此之间存在关系吗?
  • 应用类加载器的父类加载器是扩展类加载器,扩展类加载器没有父类加载器,但是会委派给启动类加载器加载
作用

双亲委派机制有什么用?

  1. 保证类加载的安全性
  • 通过双亲委派机制,让顶层的类加载器去加载核心类,避免恶意代码替换 JDK 中的核心类库,比如 java.lang.String,确保核心类库的完整性和安全性
  1. 避免重复加载
  • 双亲委派机制可以避免同一个类被多次加载,上层的类加载器如果记载过类,就会直接返回该类,避免重复加载

打破双亲委派机制

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

  • Tomcat 使用了自定义类加载器来实现应用之间类的隔离。每一个应用会有一个独立的类加载器加载对应的类

  • ClassLoader中包含了4个核心方法,双亲委派机制的核心代码就位于loadClass方法中

1
2
3
4
5
6
7
8
9
10
11
//类加载的入口,提供了双亲委派机制。内部会调用findClass
public Class<?> loadClass(String name)

//由类加载器子类实现,获取二进制数据调用defineClass,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据
protected Class<?> findClass(String name)

//做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(String name, byte[] b, int off, int len) //

//执行类生命周期中的连接阶段
protected final void resolveClass(Class<?> c)
  • 打破双亲委派机制的核心就是将下边这一段代码重新实现
1
2
3
4
5
6
7
8
9
10
//parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull
//否则调用父类加载器的加载方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
//父类加载器爱莫能助,我来加载!
if (c == null)
c = findClass(name);

问题

  1. 自定义加载器父类怎么是AppClassLoader呢?

  • 以 Jdk8 为例,ClassLoader 类中提供了构造方法设置 parent 的内容
1
2
3
4
5
6
7
8
private ClassLoader(Void unused, ClassLoader paarent){
this.parent = parent;
if(ParallelLoaders.isRegistered(this.getClass())){
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
}
}
  • 这个构造方法由另外一个构造方法调用,其中父类加载器由 getSystemClassLoader 方法设置,该方法返回的 是 AppClassLoader
1
2
3
protected ClassLoader(){
this(checkCreateClassLoader(), getSystemClassLoader());
}
  1. 两个自定义类加载器加载相同限定名的类,不会冲突吗?
  • 不会冲突,在同一个 Java 虚拟机中,只有相同类加载器 + 相同的类限定名才会被认为是同一个类
  • 在 Arthas 中使用 sc–d 类名的方式查看具体的情况
线程上下文类加载器
  • JDBC 中使用了 DriverManager 来管理项目中引入的不同数据库的驱动,比如 mysql 驱动、oracle 驱动

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

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

  • DriverManager 属于 rt.jar 是启动类加载器加载的。而用户 jar 包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制

问题

  1. DriverManager 怎么知道 jar 包中要加载的驱动在哪儿?
  • spi 全称为(Service Provider Interface),是 JDK 内置的一种服务提供发现机制
  • spi 的工作原理
    1. 在 ClassPath 路径下的 META-INF/services 文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现
    2. 使用 ServiceLoader 加载实现类
1
2
//获取Driver对象
ServiceLoader<Driver> loaderDrivers = ServiceLoader.Load(Driver.class);

总结

  1. 启动类加载器加载 DriverManager。

  2. 在初始化 DriverManager 时,通过 SPI 机制加载 jar 包中的 myql 驱动

  3. SPI 中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象

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

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

JDK9之后的类加载器

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

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

  1. 启动类加载器使用Java编写,位 jdk.internal.loader.ClassLoaders类中
  • Java中的 BootClassLoader 继承自 BuiltinClassLoader 实现从模块中找到要加载的字节码资源文件

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

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

总结

  1. 类加载器的作用是什么?
  • 类加载器(ClassLoader)负责在类加载过程中的字节码获取并加载到内存这一部分。通过加载字节码数据放入内存转换成 byte[],接下来调用虚拟机底层方法将 byte[] 转换成方法区和堆中的数据
  1. 有几种类加载器?
  • 启动类加载器(BootstrapClassLoader)加载核心类
  • 扩展类加载器(Extension ClassLoader)加载扩展类
  • 应用程序类加载器(Application ClassLoader)加载应用 classpath 中的类
  • 自定义类加载器,重写 findClass 方法。
  • JDK9 及之后扩展类加载器(Extension ClassLoader)变成了平台类加载器(Platform ClassLoader)
  1. 什么是双亲委派机制?
  • 每个 Java 实现的类加载器中保存了一个成员变量叫“父”(Parent)类加载器。 自底向上查找是否加载过,再由顶向下进行加载。避免了核心类被应用程序重写并覆盖的问题,提升了安全性

  1. 怎么打破双亲委派机制?
  • 重写 loadClass 方法,不再实现双亲委派机制
  • JNDI、JDBC、JCE、JAXB 和 JBI 等框架使用了 SPI 机制 + 线程上下文类加载器
  • OSGi 实现了一整套类加载机制,允许同级类加载器之间互相调用

运行时数据区

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

程序计数器

  • 程序计数器(Program Counter Register)也叫 PC 寄存器,每个线程会通过程序计数器记录当前要执行的的字节码指令的地址

  • 在加载阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址

  • 在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令

  • 程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑

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

问题

程序计数器在运行中会出现内存溢出吗?

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

  • Java 虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存
  • Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线 程中执行,每个线程都会包含一个自己的虚拟机栈

局部变量表

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

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

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

  • 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致
  • 局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量

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

操作数栈

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

帧数据

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

  • 方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址
  • 异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置

内存溢出

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

  • Java 虚拟机栈内存溢出时会出现 StackOverflowError 的错误

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

虚拟机栈

注意事项

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

    格式为: -XX:ThreadStackSize=1024

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

    比如测试如下两个参数:

    -Xss1k

    -Xss1025m

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

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

总结

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

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

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

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

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

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

  • 随着堆中的对象增多,当 total 可以使用的内存即将不足时,Java 虚拟机会继续分配内存给堆

  • 如果堆内存不足,Java 虚拟机就会不断的分配内存,total 值会变大。 total 最多只能与 max 相等

  • 如果不设置任何的虚拟机参数,max 默认是系统内存的 1/4,total 默认是系统内存的1/64。在实际应用中一般都需要设置 total 和 max 的值
  • Oracle官方文档
  • Java 服务端程序开发时,建议将 -Xmx 和 -Xms 设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向 Java 虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况

设置大小

  • 要修改堆的大小,可以使用虚拟机参数 –Xmx(max最大值)和-Xms (初始的total)
  • 语法:-Xmx 值 -Xms 值
  • 单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
  • 限制:Xmx 必须大于 2 MB,Xms 必须大于 1MB

问题

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

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

方法区

  • 方法区是存放基础信息的位置,线程共享,主要包含三部分内容:
    • 类的元信息:保存了所有类的基本信息
    • 运行时常量池:保存了字节码文件中的常量池内容
    • 字符串常量池:保存了字符串常量
  • 方法区是用来存储每个类的基本信息(元信息),一般称之为 InstanceClass 对象。在类的加载阶段完成

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

  • 方法区是《Java虚拟机规范》中设计的虚拟概念,每款 Java 虚拟机在实现上都各不相同。Hotspot 设计如下:
    • JDK7 及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制
    • JDK8 及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配

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

字符串常量池

  • 方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池(StringTable)
  • 字符串常量池存储在代码中定义的常量字符串内容。比如 “123” 这个123 就会被放入字符串常量池

JVM
https://www.renkelin.vip/2023/10/16/JVM/
Author
Kolin
Posted on
October 16, 2023
Licensed under