JVM基础——类加载与内存区域

参考文献
一篇JVM详细图解,坚持看完!带你真正搞懂Java虚拟机
深入理解 Java 虚拟机(JVM)从入门到精通

初识JVM

JVM即Java Virtual Machine,Java虚拟机,是Java运行的虚拟环境,跨平台的基础,其核心功能主要有:

  1. 解释运行 。Java是一种既编译又解释的语言,编译器将.java文件编译为.class字节码文件,JVM负责将其解释为机器码;
  2. 内存管理。对象和方法的内存分配,以及垃圾回收等内存管理工作都由JVM完成;
  3. 即时编译。为了支持跨平台,代码是由虚拟机实时解释的,若不作优化性能远远不如C,故JVM采用了JIT即时编译技术,将热点代码转为机器码并保存至内存,可达到接近C的性能。

JVM本质也是一种软件,目前也有很多其他语言借助JVM运行(Kotlin、Scala),故只要遵守虚拟机规范即可(官方规范在此,有兴趣也可以自己搓个JVM),常见有以下几种实现:

  • HotSpot: Oracle/Sun 官方主推的 JVM 实现,也是目前使用最广泛的版本(OpenJDK、Oracle JDK 默认)
  • GraalVM:Oracle 推出的新一代多语言虚拟机,不仅支持 Java,还兼容 Scala、Python、C++ 等
  • OpenJ9:IBM 开源的 JVM 实现(基于 J9)
  • Dragonwell:阿里基于 Oracle HotSpot(OpenJDK)的定制化

JVM结构详解

JVM结构如下图:

其大致工作过程为:

  1. 类加载器(ClassLoader)加载.class字节码文件到JVM内存;
  2. 执行引擎对字节码进行处理(解释执行+JIT编译),转化为机器码;
  3. JVM管理运行时内存(分配/回收),支撑代码执行;
  4. 执行完成后,释放资源并退出JVM进程

本地方法为C++实现的方法,不可修改

下面将逐个内容进行详解。

使用工具

后面涉及查看字节码、进程监控等要使用诸多工具,这里提前拿出来整合在一起。

字节码查看工具jclasslib

字节码是未编码的二进制文件,需用特定工具jclasslib查看,该工具目前已集成在IDEA中,打开方式如下:

字节码与源码对照如下:

集成的是个阉割版,要使用更多功能还是得装插件:

使用插件看的一般信息:

可见更精准的字节码如下:

进程监控工具Arthus

Arthas 是阿里开源的Java诊断工具,无侵入式排查JVM/应用问题,可在程序运行时诊断问题。

官方下载链接:https://arthas.aliyun.com/doc/download.html

有关快速使用教程可见:https://arthas.aliyun.com/doc/quick-start.html

上线后的程序未生效,可用该工具查看源码是否正确更新。

较强的功能是可用jad class文件名反编译成源码,如果没有保护的话将直接获取到原源码信息。

注释,变量名等还需处理,能保存基本执行逻辑

介绍一个不停机上线热补丁的功能:

复制代码
jad --source-only 类名>目录/文件名.java		// 反编译
// 修改代码
mc -c 加载器哈希 目录/文件名.java -d 输出目录 // 编译修改过的代码 
retransform 字节码路径						// 加载新的字节码

应急手段,无法修改类结构(新增、删除方法),已有对象不受影响(已创建实例使用旧字节码,可能产生数据不一致问题),仅用于紧急bug修复。

其他内置命令

javap -v 字节码文件 jdk自带反编译工具,可查看字节码内容,jar包可用jar -xvf解压后查看

sd-jdi.jar 也可查看字节码及元数据信息,使用方法可见https://javabetter.cn/jvm/hsdb.html

字节码文件

一般信息中包含:

  1. 魔数:字节码固定为0xCAFEBABE,系统通过该文件头校验文件类型,JPEG为FFD8FF,PNG为89504E47
  2. 主副版本号:通常关注主版本号,编译版本为主版本号-44,环境冲突字节码与环境不一致时通常调整字节码文件;
  3. 方法计数:类中的方法数量,构造方法隐式,故为2;

常量池

为避免相同内容重复定义,节省空间,将字段存储在常量池,如定义两个String,不同字段指向同一个常量。

方法

方法的字节码文件,常用命令如下:

命令 含义
istore_1 操作数栈顶数存到局部变量表索引1的位置
iload_1 局部变量表索引1的位置存入操作数栈
iconst_1 int常量1压入操作数栈
iadd 栈顶两个数相加
iinc 1 1 局部变量表1自增1

由此我们比较i++i=i+1的性能差距,字节码如下:

复制代码
2 iinc 1 by 1		# 自增1

5 iload_1			# 加载局部变量表到操作数栈
6 iconst_1			# 常量压入操作数栈
7 iadd				# 栈顶两个数相加	
8 istore_1			# 保存局部变量

由此可见自增操作效率比重新赋值高得多。(但通常JIT会优化成等价机器码,性能差别不大)

类的生命周期

类的生命周期即类的加载、使用、卸载过程,我们通常关注其准备阶段,即加载、连接、初始化。

加载

该部分负责将类加载到JVM内存中,具体过程如下:

  1. 类加载器根据全限定类名,以二进制流形式获取字节码信息;
  2. JVM解析字节码文件后,在元空间生成InstanceKlass对象(保存类的访问标志、常量池、字段表等基础元数据);
  3. JVM在堆中生成class对象,对象持有指向InstanceKlass的指针。

补充:

InstanceKlass是 HotSpot JVM 中用 C++ 实现的核心数据结构,是Java类在JVM底层的元数据载体,每个类只有一个实例,对Java层完全透明。

类加载时,JVM 在元空间创建 InstanceKlass存储类元数据,在堆创建 Class作为访问代理,也就是说InstanceKlass是真身,Class是访问代理。

连接

连接阶段主要完成验证、准备和解析工作。

验证负责校验字节码是否满足Java虚拟机要求,主要校验以下内容:

  1. 文件格式,魔数是否为cafebabe,以及主次版本号;
  2. 元信息验证,如类必须有父类;
  3. 字节码验证,检查语义;
  4. 符号引用验证,是否访问其他类的私有方法。

准备阶段负责完成前置工作,为静态变量static分配内存并设置初始值,final修饰的直接赋值,无初始值过程。

解析过程负责将常量池中的符号引用替换为直接引用。

初始化

执行静态代码块中的代码,为静态变量赋值,执行字节码中的clinit构造部分(静态变量赋值和静态代码块执行)。

以下行为可能触发初始化:

  1. 访问类的静态变量或方法,final修饰且右边是常量的不会触发;
  2. 调用class.forName(String className)
  3. new类对象时;
  4. Main方法的当前类。

特殊情况不执行clinit方法:

  1. 无静态代码块且无静态变量赋值语句;
  2. 有静态变量声明,但无赋值语句;
  3. 静态变量的定义使用了static final关键字(编译期常量),准备阶段直接初始化。

访问父类不触发子类初始化,子类调用clinit前,JVM会保证父类的clinit已执行。

类加载器

类加载器ClassLoader负责提供给应用获取类和接口字节码数据,根据加载类的类型不同可分为启动类加载器、扩展类加载器和应用程序类加载器。

启动类加载器Bootstrap

启动类加载器负责加载Java程序的默认依赖,即jre/lib/rt.jar下的文件,如StringArrayList等类。

该加载器使用C++实现,用户无法通过代码获取启动类加载器。

扩展类加载器Extension

扩展类加载器负责加载通用但非必须的类,通常为jre/lib/ext下的类。

该类间接继承自URLClassLoader,可使用虚拟机参数-Djava.ext.dirs="jar包目录"覆盖或追加加载目录。

应用程序类加载器Application

应用程序类加载器加载额外应用类,具体为classpath用户路径下的类,可用Arthas的classloader -l命令查看类加载器的哈希值、加载的类列表等信息。

要注意的是相同的类加载器+相同的全类名会被视作一个类。

双亲委派机制

三类核心类加载器核心目的是解决类的隔离和安全问题 ,如果使用一个用户能接触到的类加载器加载所有类,可能会出现用户代码篡改核心类库的场景(如加载重写的java.lang.String),具体一个类由何种加载器加载要由双亲委派机制决定。

双亲委派机制的核心功能有两点:

  1. 保证类加载的安全性,核心类库不可替换;
  2. 避免重复加载。

具体过程为:

  1. 向上查找,类加载器接受加载类任务时自底向上查找是否被加载过,未被加载则进入下一阶段;
  2. 向下加载,若类不在当前加载器的加载目录,则向下委派,可保证优先由启动类加载。

若核心类(如String)已加载,再次加载用户重写的类不会被加载,保证安全性。

若要主动加载可使用反射的Class.forName()加载字节码文件。

打破双亲委派机制

前面提到,同一个加载器加载的相同全类名的类会被视为一个类,当一个服务运行多个应用时,可能导致类的冲突(比如Tomcat会运行多个web应用),所以有时候需要打破双亲委派机制。

首先介绍下ClassLoader的原理,类加载器加载类大致可以分为如下四步:

  1. loadClass,类加载的入口,提供双亲委派机制;

  2. findClass,子类的具体实现,获取具体二进制数据;

  3. defineClass,调用JVM方法,将数据加载到内存;

  4. resolveClass,进入连接阶段。

打破双亲委派机制有以下三种方法:

  1. 自定义类:自定义类加载器,并重写loadClassfindClass方法。
  2. 线程上下文:JDBC用启动类加载器加载DriverManager,但用应用类加载器加载mysql驱动,同样违反了双亲委派机制。可用Thread.currentThread().getContextClassLoader();获取应用程序类加载器,得以找到驱动。
  3. OSGI框架:osgi模块化框架,存在不同级之间类加载器的委托加载,可实现热部署。

JVM内存区域

JVM内存区域主要指的是进程运行时的数据区域,根据是否被线程共享可分为两类:

  • 线程不共享:程序计数器、JVM栈、本地方法栈;
  • 线程共享:方法区、堆区。

程序计数器

程序计数器(PC 寄存器)记录当前线程正在执行的字节码指令地址;类加载阶段会将指令偏移量转换为实际内存地址,JVM 执行引擎根据程序计数器的值定位并执行对应指令。

多线程环境下,CPU 会不断切换线程执行,为了保证线程切换后能恢复到正确的执行位置,每个线程都必须拥有独立的程序计数器,因此程序计数器是线程私有的。

程序计数器是 JVM 中唯一不会发生内存溢出(OOM)和栈溢出(SOF) 的区域,开发者无需手动管理。

虚拟机栈是描述 Java 方法执行的内存模型,每个线程独有 ,一个虚拟机栈。每个方法执行时,都会创建一个栈帧(Stack Frame),用于存储:局部变量表、操作数栈、动态链接、方法返回地址等信息。

方法从调用到执行完毕的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧核心组成:

  • 局部变量表:数组结构,存储方法内的局部变量、方法参数;
  • 操作数栈:栈式结构,最大深度由编译器确定,用于执行运算、存储中间结果;
  • 动态链接:将符号引用转换为直接内存地址的引用;
  • 方法返回地址:方法执行完毕后,回到调用该方法的位置。

异常与配置

该区域可能发生两种异常

StackOverflowError:线程请求的栈深度超过虚拟机允许的最大深度(如无限递归);

OutOfMemoryError:栈扩展时无法申请到足够内存。

栈默认大小约 1MB(取决于操作系统与架构),可通过虚拟机参数-Xss指定栈大小(如-Xss256k)。HotSpot 虚拟机对栈大小的限制范围:100k ~ 1024M,256k 是生产环境常用配置,可有效节省内存。

JVM 内存中最大的一块区域,线程共享 ,唯一用途是存储对象实例与数组,是垃圾回收(GC)的核心管理区域。

堆内存分为三部分:used已使用、total已分配和max最大可分配,堆内存溢出used=max=total内存已满且无法继续扩容时,会报OutOfMemory错误。

通常使用虚拟机参数-Xms数值指定初始堆内存大小、Xmx数值指定最大可分配内存大小,生产环境建议将-Xms与-Xmx设为相同值,避免堆内存频繁扩容带来性能损耗。

方法区

线程共享区域,用于存储 JVM 加载的类元信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

JDK7前方法区存放在永久代空间,托管在JVM堆内,JDK8后方法区在元空间,该空间位于操作系统维护的直接内存。

直接内存指不属于Java运行的内存区域,JDK1.4后引入NIO,使用直接内存,主要解决两个问题:

  1. 避免堆内对象频繁 GC 带来的性能影响;
  2. 减少 IO 操作中「本地内存 ↔ Java 堆」的数据拷贝,提升效率。

运行时常量池是方法区的一部分,存放编译期生成的字面量、符号引用,类加载后存入运行时常量池,支持运行期间动态添加常量。

总结

JVM内存加载的整体流程如下:

  1. 类加载器通过双亲委派模型加载字节码到 JVM 内存,保证核心类不被篡改,实现类加载的安全与隔离;
  2. (存储对象 / 数组)和方法区(存储类元信息)是线程共享区域,其中堆是 JVM 最大内存区域,也是垃圾回收的核心目标;
  3. 每个线程拥有独立的程序计数器 (记录执行位置)和虚拟机栈 (管理方法调用);程序计数器无溢出风险,虚拟机栈可能发生栈溢出或内存溢出,但线程私有区域本身不会产生内存泄漏;
相关推荐
m0_518019482 小时前
使用Kivy开发跨平台的移动应用
jvm·数据库·python
weixin_421922692 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
Fortune792 小时前
Python迭代器(Iterator)揭秘:for循环背后的故事
jvm·数据库·python
cm6543202 小时前
Python字典与集合:高效数据管理的艺术
jvm·数据库·python
2401_846341652 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
qq_416018723 小时前
Python多线程与多进程:如何选择?(GIL全局解释器锁详解)
jvm·数据库·python
m0_662577973 小时前
用Python生成艺术:分形与算法绘图
jvm·数据库·python
暮冬-  Gentle°3 小时前
用Python制作一个文字冒险游戏
jvm·数据库·python
m0_587958954 小时前
游戏与图形界面(GUI)
jvm·数据库·python