JVM内存结构与类加载机制

在Java的世界中,虚拟机(JVM)是我们每一个程序的运行环境,而它的内存结构 更是决定我们程序运行性能的关键因素。理解JVM的内存结构,不仅可以帮助我们编写出更高效的代码,而且可以在程序出现问题时,更快地定位并解决问题。本文将从两个模块(内存结构、类加载机制)了解JVM。

一.JVM内存结构

java虚拟机在执行程序的过程中会将内存划分为不同的数据区域,可以了解下图

图中包含虚拟线程,本地方法栈,方法区,堆以及程序计数器五个组成,其中又可以拆分两个模块介绍

1.线程私有区域

线程的私有内存是每个线程独立拥有的空间,它们无需考虑线程安全,生命周期与线程一致(线程启动时创建,线程终止时销毁),以便减少同步的开销。

  • 程序计数器:对应示意图中线程私有区域的最核心组件,其本质是"指令地址指示器",存储当前线程正在执行的Java字节码指令的地址。首先CPU是时分复用的,线程切换时,需要记录当前线程的执行位置,当恢复线程时才能准确继续执行,这就是程序计数器的核心作用。它是JVM中唯一不会发生内存溢出(OutOfMemoryError)的区域,底层由CPU寄存器直接支持,执行效率非常高。
  • 虚拟机栈:虚拟机栈又包含了几个不同模块。

1.栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

2局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

3.操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

4.动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

5.方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

本地方法栈:其与虚拟机栈作用相似,区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务,也会抛出也会抛出StackOverflowError和OutOfMemoryError异常。

2.线程共享内存

  • 堆(Heap) :对应示意图中线程共享区域的最大板块,是JVM中最大的内存区域,它本质是"对象存储的核心区域",几乎所有Java对象都在堆中创建,也是垃圾回收(GC)的主要区域。堆的GC操作采用分代收集算法,区分了新生代和老年代;新生代又分为:Eden空间、From Survivor(S0)空间、To Survivor(S1)空间。在java虚拟机规范中规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。也就是说堆的内存是一块块拼凑起来的。要增加堆空间时,往上"拼凑"(可扩展性)即可,但当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  • 方法区:存储已加载的类信息(类名、字段、方法、接口)、常量池(字符串常量、数字常量)、静态变量、即时编译器(JIT)编译后的代码等。

二.类加载机制

1.类的加载

JVM将class文件字节码文件加载到内存中,并且将这些静态数据转换成方法区中的运行时数据结构,在堆或方法区中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。

2.类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,包括了七个阶段的生命周期分别为:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载 ,其中验证,准备和解析三个部分统称链接。类的加载过程必须安照加载 ->验证 ->准备 ->初始化 ->卸载这五个顺序,但解析阶段在某些特定情况下,为了运行时动态绑定特性,会在初始化后再开始,且这些阶段通常都是互相交叉的混合进行,一个阶段执行过程中调用或激活另外一个阶段

加载

加载阶段通过类全名来获取定义此类的二进制字节流,再将字节流所代表的静态存储结构转换为方法区的运行时的数据,并在java堆中生成一个代表它这个类的java.lang.Class对象,作为方法区这些数据的访问入口,由于加载阶段可以使用系统提供的类加载器,也可以使用用户自定义的类加载器完成,所以相对来说是开发期可控性最强的。

验证

验证是链接阶段第一步,用来确保class文件的字节流中包含的信息符合当前虚拟机要求且不会危害虚拟机自身安全,主要包括四个检验流程:文件格式验证(验证class文件格式规范),元数据验证(对字节码描述信息进行语义分析,验证点包括这个类是否有除Object以外的父类,类是否继承被final修饰的不可继承的类,如父类是抽象类,是否实现父类或接口中要求实现的所以方法),字节码验证(进行数据流和控制流分析,保证被校验类的方法在运行时不会出现危害虚拟机的行为),符号引用验证(通过字符串描述的全限定名是否能找到对应的类,符号引用类中的类,是否可被访问)。

准备

这个是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这时候内存分配的仅包括类变量(static修饰的变量),不包括实例变量,而实例变量会在对象实例化时再随对象一起分配在堆中

解析

这个阶段是符号引用替换直接引用的过程(符号引用:一组描述所引用对象的符号,可以是任何形式的字面量。直接引用:可以直接指向目标的指针,相对偏移量或能间接定位到目标的句柄),主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行解析。

初始化

是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外个角度来表达:初始化阶段是执行类构造器()方法的过程。在以下四种情况下初始化过程会被触发执行:1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。2.使用java.lang.reflect包的方法对类进行反射调用的时候3.当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化4.jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类

使用与卸载

使用:在初始化完成后,类就进入了漫长的使用期。程序可以通过 Class 对象创建实例、访问方法、读写字段。 卸载:当满足该类的所有实例都被垃圾回收,加载该类的ClassLoader实例已被垃圾回收,该类对应的java.lang.Class 对象没有任何地方被引用,无法通过任何方式(包括反射)访问到该类的方法,类就可以被卸载,并从方法区回收。

3.类加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。且如果想比较两个类是否"相等",只有在两个类是同一个类加载器加载的前提下才有意义。

第一层:启动类加载器

它是整个类加载器体系的始祖与根基,由JVM底层的C/C++代码实现 ,负责加载最核心的Java运行时位于JRE安装目录下的lib目录库(如java.lang.*, java.util.*, java.io.* 等核心包)

第二层:平台类加载器(扩展类加载器)

是由Java语言编写的类,属于是启动类加载器的直接子加载器,负责加载Java扩展功能库或由java.ext.dirs 系统变量指定的路径中。

第三层:应用程序类加载器(系统类加载器)

也是一个Java类,属于上述平台类加载器的直接子加载器,负责加载用户类路径下的所有库,既通过-classpath-cp 参数指定的路径,或者是环境变量 CLASSPATH 所包含的路径。我们自己编写的所有Java代码(.class文件),以及项目依赖的第三方Jar包,几乎都是由这个类加载器加载的,也是程序默认的上下文类加载器。

4.双亲委派

当一个类加载器收到一个类请求时会先将这个请求逐级向上传递给自己的父加载器(请求链:应用程序类加载器 -> 平台类加载器 -> 启动类加载器)。而启动类加载器首先在自己的核心库目录里面查找,如果找到则直接由它加载流程结束,如果启动类加载器没找到,则请求会返回到平台类加载器,让它在扩展库目录里查找,如还没找到,会最终才将这个请求回到最初发起请求的应用程序类加载器并执行自己的加载逻辑去用户类路径下寻找并加载这个类。

其能防止核心API被恶意篡改。假设有别人写了一个 java.lang.String 类放在用户类路径下,由于双亲委派机制,加载请求会优先交给顶层的启动类加载器,而启动类加载器会在核心 rt.jar 中找到官方的 String 类并加载。用户自定义的 String 类永远不会被加载,从而杜绝了通过伪造核心类破坏系统的可能。以及确保了同一个类在JVM中只有一份定义。只要父加载器加载了某个类,子加载器就没有必要、也无法再次加载它。这保证了像 java.lang.Object 这样的基础类,在全虚拟机范围内有且仅有一个 Class 对象。且由于每一层的加载器都有明确的加载范围,使得层次清晰,让类库的依赖和管理井然有序

相关推荐
ding_zhikai2 小时前
【Web应用开发笔记】Django笔记3-2:部署我的简陋网页
笔记·后端·python·django
掘金者阿豪2 小时前
从MongoDB到金仓数据库:文档数据库国产化替代的实战路径与价值重构
后端
敲敲了个代码2 小时前
构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的
开发语言·前端·javascript·后端·rust
Penge6662 小时前
Go反射练习:从复杂结构体中提取统一接口实例
后端
贾铭2 小时前
如何实现一个网页版的剪映(二)
前端·后端
troublea2 小时前
Laravel5.x核心特性全解析
数据库·spring boot·后端·mysql
白衣鸽子2 小时前
Java 线程同步-05:基于Sync抽象类的公平锁和非公平锁
后端
漫霂2 小时前
WebSocket入门
后端·websocket
笨蛋不要掉眼泪2 小时前
Spring Cloud Gateway 核心篇:深入解析过滤器(Filter)机制与实战
java·服务器·网络·后端·微服务·gateway