Java中的大魔王JVM -- 类加载

本文介绍了Java虚拟机(JVM)的基本结构及其在跨平台运行中的作用。JVM包括类加载器子系统、运行时数据区、执行引擎和本地接口库。重点讨论了类加载过程,包括加载、验证、准备、解析和初始化五个阶段。类加载器按双亲委派模式工作,保证类的唯一性和安全性。文章还提到了自定义类加载器和OSGI规范在模块化系统中的应用。

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

这两天准备了点JVM的一些知识,拿来聊一聊吧。

首先国际惯例介绍下JVM(Java Virtual Machine) Java虚拟机。内容包括一套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、一个方法区和一个垃圾回收器。我们平时说Java语言跨平台运行,主要就是靠这JVM,平时写完代码运行的过程,就是代码文件经过虚拟机编译成相应的.class字节码文件,然后经过解释器翻译成不同平台(操作系统)可以解析的语言调用本地方法库,然后进行执行。虚拟机的实例随着进程的开始而开始,有多个进程启动就会创建多个虚拟机实例。

JVM的组成包括一个类加载器子系统(Class Loader SubSystem),运行时数据区(Runtime Data Area),执行引擎, 和本地接口库(Native Interface Library)。那这两天就按照java执行的顺序把JVM大概过一遍吧,今天先聊聊类加载相关的知识吧。

前面部分的过程就是我们编写Java类,代码如下:

public class Test {
	private static String name = "张三";
	private static int needMoney = 150;

	public static void main(String[] args) {
		Test test = new Test();
		test.getMoney();
	}
	
	public void getMoney() {   //张三好可恶,经常找我要钱,这次要150
		System.out.println("程序开始");
		Object tech13 = new Object();
		
		int zhifubao = 100;//我支付宝只有100
		int weixin = 100;//微信只有100
		
		if ((zhifubao + weixin) > 150) {
			tech13.hashCode();
			zhifubao -= 100;
			weixin -= 50;
		}
		System.out.println("getMoney runs over...");
	}
}

代码进入了Java编译器,编译成了.class类文件,那类文件内容是什么样的呢?(除了Java,还有其他的语言,包括Groovy、JRuby、Jython、Scala都是使用Java虚拟机。)

class文件
class文件

上面一大堆的数字,是不是看着就有点晕(其实对JVM来说,可是一片排列得整整齐齐的数据),左侧对这些数据进行了“翻译”,然后对照着这个表看一下虚拟机是怎么解读的:

字节码如图 class文件 所示,开头的四个字节(u4:4个字节无符号数,16进制) CAFEBABE(cafe baby)Java 的头像就是一杯咖啡,这个魔数是用来区分不同的文件格式的,比如 89504E47 代表的就是一个图片格式png、47 49 46 38代表是一个git文件、50 4B 03 04代表是一个zip压缩文件。如果魔数是CAFEBABE,就可以识别出来是Java的class文件,那是否就可以被虚拟机执行呢,还不行,看看后面两个字节(u2)0000,代表的是次版本号0,再后面两个字节0034代表的是主版本号52,52对应的Java版本是1.8(只有jvm的版本在该版本及以上才能执行该class文件):

紧跟在版本号之后的是一个叫作常量池的表(cp_info),常量池的数量一般是不固定的,在常量池的入口会放置2个字节的无符号数constatn_pool_count,常量池的计数是从1开始的,和平常集合的计数起点有些不同,这里的3D转为十进制就是61,说明该常量池中有60个常量。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于 Java语言层面的常量概念,如文本字符串、声明为 final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。常量池中的每一项都是一个表,其项目类型共有 14 种,如下表所示:

tag:占用一个字节大小。比如值为 7,说明是 CONSTANT_Class_info 类型表,代表一个类或接口的符号引用。接下来2位字节用来保存一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型的常量,此常量代表了这个类或接口的全限定名,索引值为0x0002,即指向了常量池中的第二项常量。(name_index:是一个索引值,可以将它理解为一个指针,指向常量池中索引为 name_index 的常量表。比如 name_index = 2,则它指向常量池中第 2 个常量。)

大致规则就是那样子的,除此之外还有访问标志、类索引、父类索引等内容,由于篇幅问题,这里就不继续展开介绍了,

详细的介绍可以参考这篇文章:

Class文件内容解析

Class 文件结构及深入字节码指令

大致了解完class文件,及JVM执行class文件的条件,现在具体看下JVM是如何加载这些class文件的,那么就要引出今天的重点内容----类加载机制。

JVM的类加载过程一共分为5个阶段,加载、验证、准备、解析、初始化。等初始化的步骤完成后就可以正常使用该类,然后等到不需要了,就将该类卸载。

既然是加载,那就得问几个人生哲学的问题,加载什么?从哪里加载?加载到哪里去?

那么先回答下第一个问题,加载什么?JVM的使命就是加载执行class文件;第二个问题,从哪里加载?JVM获取class文件的途径有以下几种:直接获取本地磁盘中的字节码文件、从jar包或者war包中加载、网络下载的字节码文件、由Java文件动态编译生成、从专门的数据库中获取。那么加载到哪里去呢,在下面的文章里将会详细回答。

首先我们了解下类加载的几个环节:

加载。加载分为以下几个步骤:1、通过全限定类名来获取此类的二进制字节流;2、将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构;3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据访问入口。

验证。jvm加载过程将验证、准备、解析总概为连接阶段,连接阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。验证过程包括文件格式验证、元数据验证、字节码验证、符号引用验证。

其中,文件格式验证包括了前面谈到的魔数是否是cafebabe,主次版本号是否在当前虚拟机处理范围之内,常量是否合理等,文件各个部分内容是否有被删除或者附加,文件格式验证是为了保证输入的字节流能正确地解析并存储与方法区内;

元数据验证包括了是否存在父类,父类继承链是否正确,抽象类是否实现了父类或接口中要求实现的方法,字段和方法是否和父类产生矛盾,元数据验证是为了保证不存在不符合Java语言规范的元数据信息;

字节码验证通过数据和控制流分析来确定程序语义是否合法是否符合逻辑,字节码验证为了保证指令能按照正常的顺序执行;

符号引用验证发生在解析阶段,包括了验证符号引用中通过字符串描述的全限定名是否能找到对应的类,在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段,符号引用中的类、字段、方法的访问性是否可被当前类访问,为了保证可以将符号引用转化为直接引用。(NoSuchMethodError、NoSuchFieldError....)

准备。给类变量分配内存(方法区中)并设置变量初始值。(此阶段只有类变量进行内存分配,不包括实例变量,实例变量会在对象实例化时随对象一起分配到Java堆中。)

解析。虚拟机将常量池内的符号引用替换为直接引用,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符。

初始化。执行<clinit>()方法,整整开始执行类中定义的Java程序。

其实在Java程序启动时,并不会一次性加载程序中所有的.class文件,而是在程序运行的过程中,动态地加载相应的类到内存中,通常情况下,ClassLoader主动加载class文件到内存中有这两种情况:1、调用类构造器 2、调用类中的静态变量或者静态方法。

那么这里就引出了ClassLoader(类加载器)的概念,什么是类加载器呢: 把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码块称为  类加载器。Java中自带三个类加载器:BootstrapClassLoader(启动类加载器)、ExtClassLoader(扩展类加载器)、AppClassLoader(应用程序类加载器)。其中BootstrapClassLoader是底层使用C/C++语言写的,其他两个是Java语言写的,当然,除此之外,我们还可以通过继承ClassLoader自定义类加载器。

BootstapClassLoader的加载路径包括:(sun.boot.class.path)

加载路径:D:\Software\JDK\jre\lib\resources.jar
加载路径:D:\Software\JDK\jre\lib\rt.jar
加载路径:D:\Software\JDK\jre\lib\sunrsasign.jar
加载路径:D:\Software\JDK\jre\lib\jsse.jar
加载路径:D:\Software\JDK\jre\lib\jce.jar
加载路径:D:\Software\JDK\jre\lib\charsets.jar
加载路径:D:\Software\JDK\jre\lib\jfr.jar
加载路径:D:\Software\JDK\jre\classes
加载路径:D:\Software\AndroidStudio\lib\intellij-coverage-agent-1.0.508.jar

ExtClassLoader的加载路径包括:(java.ext.dirs)

加载路径:D:\Software\JDK\jre\lib\ext
加载路径:C:\Windows\Sun\Java\lib\ext

AppClassLoader的加载路径包括:(java.class.path)

加载路径:D:\Software\SDK\platforms\android-29\android.jar
加载路径:D:\Software\SDK\platforms\android-29\data\res
加载路径:D:\File\workspace\android\TestRetrofit2\app\build\intermediates\javac\debug\classes
加载路径:D:\File\workspace\android\TestRetrofit2\app\build\intermediates\compile_and_runtime_not_namespaced_r_class_jar\debug\R.jar
。。。此处省略一系列应用依赖的第三方框架里面的jar包等。。。
加载路径:D:\Software\AndroidStudio\lib\intellij-coverage-agent-1.0.508.jar
加载路径:D:\Software\AndroidStudio\lib\idea_rt.jar

在Java中,每个类加载器都有一个父加载器,我们通过以下代码获取各个类加载器的父加载器获得结果如图:

ClassLoader cl = TestClassLoader.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
System.out.println("ClassLoader\'s parent\'s parent is:"+cl.getParent().getParent());

双亲委派模式

那么JVM中怎么知道安排哪个类加载器去加载某个目标类文件呢:通过双亲委派模式。一个类在收到类加载请求后不会尝试自己加载这个类,而是向上委派给父加载器,父加载器接收到请求后又会将其向上委派给自己的父加载器...若父加载器无法加载该类(在相应的路径下找,上面有提到),则将信息反馈给子加载器并向下委派子加载器去加载该类,子加载器收到请求后也按照同样的过程执行...,最后如果还是找不到该类,则JVM会抛出ClassNorFoudException。(用简单的一句话概括就是:向上去依赖检查,向下去依次加载)

(这么理解吧,就是有好吃的,先孝顺给自己的长辈,比如应用程序类加载器孝顺给父加载器扩展类加载器,然后扩展类加载器孝顺给启动类加载器,然后启动类加载器拿到了试了下,没牙齿咬不动,就给了子加载器,子加载器也试了下,也是没牙齿,咬不动。。。最后的子加载器也咬不动,就把东西扔了:不能吃)

双亲委派机制的核心就是保障类的唯一性和安全性。针对要加载的类  Class1,无论哪个类加载器加载这个类,最后都将委派给启动类加载器,这保证了类加载的唯一性(对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等,相等指的是equals、isAssignableFrom、isInstance方法返回的结果),如果JVM中存在包名和类名相同的两个类,则该类无法被加载,JVM也无法完成加载流程。

其实双亲委派机制知识Java推荐的一种机制,并非强制,我们想要定义自己的类加载器,则可以通过继承ClassLoader类来实现,在这个过程中如果要保持双亲委派机制,则重写findClass方法,如果要破坏双亲委派机制,则重写loadClass方法。

讲到自定义类加载器,也顺便提一下OSGI(Open Service Gateway Initiative),Java动态化模块化系统的一系列规范,基于OSGI可以实现模块级的热插拔功能,这样提高了应用升级的安全性和便捷性,也减小了安装包的压力。关于模块化我之前公司的项目中有用过,后续有机会跟大家分享下。

这里先讲到类加载机制将class文件加载到虚拟机内存中,既然是加载到内存中,那么在内存中是如何分配和运行的呢,下一篇文章跟大家聊聊JVM中的内存管理(运行时数据区)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值