计算机系统应用教程网站

网站首页 > 技术文章 正文

JVM知识点——深入理解JVM的类加载

btikc 2024-11-02 11:09:05 技术文章 5 ℃ 0 评论

前言:

前面又说到Java程序实际上是将。class文件放入JVM中运行。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是JVM的类加载机制

一、类加载的过程

类从加载虚拟机内存中开始到卸载出内存为止,生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。



1、加载

通过一个类的全限定名来获取定义此类的二进制字节流(没有指明二进制字节流要从一个Class文件中获取,可以从ZIP包中读取,从网络中获取,运行时计算生成等等)将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口完成后,虚拟机外部的二进制字节流就按照虚拟机所需格式储存在方法区中。这里稍微理解一下对象和类的概念,对象是实例化的类。类的信息是存储在方法区中的,对象是存储在Java堆中的。类是对象的模板,对象是类的实例。这里主要讲类。

2、验证

加载阶段未完成,连接阶段已经开始。两者之间会交叉运行。因为Class文件可以用任何途径产生,字节流不符合Class文件格式的约束,虚拟机会抛出java.lang.VerifyError异常,所以为了保护虚拟机,JVM会有以下四个方面的验证

文件格式验证:即验证类文件结构

元数据验证:这个是否有父类,父类是否继承了不允许被继承的类等等

字节码验证:对类的方法体进行校验,JDK1.6后只需检查StackMapTable属性中的记录是否合法,JDK1.7后对于主版本号大于50的Class文件,使用类型检查来完成数据流分析

符号引用验证:全限定名是否能找到对应的类,在指定类中是否存在符合方法的字段描述以及简单名称描述的方法,字段。访问性是否正确。验证不成功会抛出java.lang.incompatibleClassChangeError异常的子类。

3、准备

正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个时候进行内存分配的只包括类变量(被static修饰的变量),并不包括实例变量,实例变量是在对象实例化时随对象一起分配在java堆中。这时候分配的初始值为零值,假设一个变量定义为:public static int value = 123;则设置变量的初始值应该为0, 而不是123。 把value赋值为123的putstatic指令是在程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。如果字段属性表中存在ConstantValue属性,那么在准备阶段会将value赋值为ConstantValue属性所指定的值 。例如:public static final int value = 123; 那么在准备阶段,则会将value赋值为123;

4、解析

将符号引用转换为直接引用的过程。符号引用是一组以符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。而直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

解析动作主要针对:类或接口、字段(类成员变量)、类方法、接口方法等引用进行。

类或接口的解析:判断所要转化成的直接引用是对数组类型,还是对普通的对象类型的引用,从而进行不同的解析。

字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,查找流程如下图所示:



最后需要注意:理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译。

类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。

接口方法解析:与类方法解析步骤类似,由于接口不会有父类,因此,只递归向上搜索父接口就行了。

5、初始化

是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源。到初始化阶段,才真正开始执行类中的Java程序代码。即初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法解析过程:<clinit>方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器的顺序是由语句在源文件中出现的顺序决定的,所以静态语句块中只能访问到定义在静态语句块之前的变量定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中的一个被执行的<clinit>()方法的类肯定是java。lang。Object。<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法所以这个<clinit>()方法主要是给静态变量赋值和执行静态语句块。接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法,但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能会造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

小结:

目前来说也就是加载阶段获取类的信息,验证阶段验证类是否符合JVM的标准。这两个阶段可以交叉运行。然后就需要在准备阶段给类分配内存空间,设置常量的初始值,初始化静态变量等等。解析阶段就是将符号引用转换为直接引用,让类在JVM上有对应的内存。最后是初始化阶段就是执行类中的静态变量赋值语句和静态语句块。到这里的类一些基础的工作已经做好了,可以使用了。

二、Java中的类加载器与双亲委派机制

实现“通过一个类的权限定名来获取描述此类的二进制字节流”这个动作的代码模块称为类加载器java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制。JVM中用来完成上述功能的具体实现就是类加载器。类加载器读取。class字节码文件将其转换成java。lang。Class类的一个实例。每个实例用来表示一个java类。通过该实例的newInstance()方法可以创建出一个该类的对象。

1、引导类加载器(Bootstrap ClassLoader)

这个类加载器负责将<JAVA_HOME>\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的。是虚拟机自身的一部分

2、扩展类加载器(Extendsion ClassLoader)

这个类加载器负责加载<JAVA_HOME>\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器。

3、应用程序类加载器(Application ClassLoader)

这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。一般情况下这就是系统默认的类加载器

4、自定义的类加载器

这个加载器可以满足我们加载类的特殊需求,需要继承java.lang.ClassLoader类并且覆盖其中的findClass()方法和defineClass()方法。

5、双亲委派机制

上面的4个加载器并不是并行加载的,JVM中是通过双亲委派机制来加载的:



双亲委派模型是一种组织类加载器之间关系的一种规范,它的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载。这样的好处是:java类随着它的类加载器一起具备了带有优先级的层次关系。这是十分必要的,比如java.lang.Object,它存放在\jre\lib\rt。jar中,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中。Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了。

三、必须对类进行初始化的5种情况

1、遇到new,getstatic,putstatic,invokestatic字节码指令。最常见的Java代码场景是:使用new实例化对象、读取或设置一个类的静态字段(被final修饰或已在编译器把结果放入常量池的静态字段除外)、调用一个类的静态方法

2、使用java.lang.reflect包的方法对类进行反射调用

3、类继承了父类,父类要先初始化

4、虚拟机启动,初始化主类

5、当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表