JVM内存模型和类加载机制

本文详细介绍了Java虚拟机(JVM)的内存模型,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区等各区域的功能及异常情况。此外,还阐述了内存溢出和内存泄漏的概念。类加载过程分为加载、验证、准备、解析和初始化五个阶段,详细描述了每个阶段的任务。文章还讨论了类加载器的工作原理和双亲委派模型,以及类加载机制如何保证安全性与效率。最后,简要提及了类加载器的缓存机制和类加载的触发时机。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM内存模型

概述

程序计数器 : 如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

虚拟机栈: java方法的执行和结束对应着栈帧的入栈和出栈,

栈帧 : 用于存储局部变量表,操作栈,动态链接,方法出口等信息

局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。
本地方法栈 : 本地方法栈是与虚拟机栈发挥的作用十分相似, 区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++, 我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

堆 : 对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。因此需要重点了解下。

实例在堆中( 也就是new的对象 )

方法区: 保存在着被加载过的每一个类的信息;static变量信息也保存在方法区中;
可以看做是将类(Class)的元数据,保存在方法区里;

方法区是线程共享的;当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。

img

img

内存溢出和内存泄漏是什么

简述一下:

溢出: 所需要用的内存大于系统给的内存

泄漏: 某对象不用了但是没被回收

类加载过程

加载

指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

class来源有很多 , 如本地系统 / 网络中的class文件 / JAR包 / java源文件动态编译 等

加载”阶段是“类加载”生命周期的第一个阶段。在加载阶段,虚拟机要完成下面三件事:

​ ①、通过一个类的全限定名来获取定义此类的二进制字节流。

②、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

③、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

验证

验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致

文件格式验证 : 验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理
元数据验证 : 对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证 : 最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证 : 主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

准备

类准备阶段负责为类的静态变量分配内存,并设置默认初始值, 非静态变量不会分配内存;

解析

将类的二进制数据中的符号引用替换成直接引用。

符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
举例 : 比如拿到某人身份证号123456879(符号引用 也就是占位符的意思) , 我们得不到什么有价值的信息,但是去公安局查询的话就能查到这个人的精准家庭地址( 直接引用 )

初始化

初始化是为类的静态变量赋予正确的初始值

如果类中有语句:private static int a = 10,它的执行过程是这样的

首先字节码文件被加载到内存后先进行链接的验证这一步骤,验证通过后准备阶段
给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0
然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

类的加载时机

创建类的实例,也就是new一个对象
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(Class.forName(“com.chen.demo”))
初始化一个类的子类(会首先初始化子类的父类)
JVM启动时标明的启动类,即文件名和类名相同的那个类
除此之外,下面几种情形需要特别指出:

对于一个 final 类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

类加载器

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。

一个类的唯一标志: 类名.包名.类加载器名

根类加载器

它用来加载 Java 的核心类,是用原生代码来实现的
并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

扩展类加载器

它负责加载JRE的扩展目录
lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。
由Java语言实现,父类加载器为null。

系统类加载器

被称为系统(or 应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。

程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。

如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

类加载器加载Class步骤

检测此Class是否载入过,即在缓冲区中是否有此Class,返回对应的java.lang.Class对象,否则进入第2步。

没有父类加载器 or 父类是根类加载器 or 本身就是根类加载器,则跳到第4步

如果父类加载器存在,则进入第3步。

请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。

请求使用根类加载器去载入目标类,如果载入成功则返回对象,否则抛异常。

当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。

从文件中载入Class,成功后跳至第8步。

抛出ClassNotFountException异常。

返回对应的java.lang.Class 对象。

判断缓冲区是否有此Class, 有返回,没有就判断是否有父类加载器
父类存在则用父类加载器去加载, 加载成功返回jlc对象, 否则使用当前类加载器寻找Class文件, 如果找到, 则载入, 然后加载成功, 否则抛出CNFE异常
如果本身是根类加载器: 加载成功返回jlc对象, 否则抛出CNFE异常

类加载机制

全盘负责

当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

双亲委派

所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
在这里插入图片描述

双亲委派机制 工作原理 : 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成

双亲委派机制的优势:

可避免类重复加载 : Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
安全 : java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

缓存机制

缓存机制将会保证所有加载过的Class都会被缓存
当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。

这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
载过的Class都会被缓存
当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。

这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值