类加载机制
发布日期:2021-09-25 11:48:26 浏览次数:4 分类:技术文章

本文共 5739 字,大约阅读时间需要 19 分钟。

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制

类从被加载到虚拟机内存中开始,整个生命周期包括:加载Loading,验证Verification,准备Preparation,解析Resolution,初始化Initialization,使用Using和卸载Unloading的7个阶段,其中验证、准备、解析3个部分统称为连接Linking。加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始

1.加载

在加载阶段,虚拟机需要完成以下3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存区生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

类的加载由类加载器完成,通过使用不同的类加载器,可以从不同来源加载类的二进制数据,有如下几种来源:

  • 从本地文件系统加载class文件
  • 从ZIP包中读取,最终成为日后JAR、EAR、WAR的基础
  • 从网络获取,eg,Applet
  • 运行时计算生成,eg,动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
  • 由其他文件生成,eg,jsp应用,即由jsp文件生成对应的Class类
  • 从数据库中读取

加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class对象(并没有明确规定在java堆中,对于HopSpot,Class对象比较特殊,它虽然是对象,但是放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口

2.验证

目的:确保Class文件的字节流中包含的信息符合当前虚拟机的需求,并且不会危害虚拟机自身的安全

大致上会完成如下4个阶段的校验工作:

(1)文件格式验证

验证字节流是否符合Class文件格式的规范,并能被当前版本的虚拟机处理。该阶段的验证基于二进制字节流,只有通过了这个阶段的验证后,字节流才会进入内存的方法区进行存储,so后面的3个阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流

可能包括下面这些验证点:

  • 是否以魔数0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理范围之内
  • 常量池中的常量是否有不被支持的常量类型
  • 指向常量的索引值中是否有指向不存在的常量或不符合类型的常量
  • Class文件各个部分及文件本身是否有被删除的或附加的其他信息

(2)元数据验证

对字节码的数据信息进行语义分析,保证其描述的信息符合java语言规范

可能包括下面这些验证点:

  • 这个类是否有父类,除了java.lang.Object,所有的类都应该有父类
  • 这个类的父类是否继承了不允许被继承的类,eg,被final修饰的类
  • 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法

(3)字节码验证

通过对数据流和控制流分析,确定程序语义是合法的符合逻辑的,该阶段将对类的方法进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似:在操作栈放置了一个int类型的数据,使用时却按照long类型来加载如本地变量表中
  • 保证跳转指令不会跳转到方法体以外的字节码指令上

(4)符号引用验证

目的是确保解析动作能正常执行,如果无法通过符号引用验证,将抛出一个java.lang.IncompatibleClassChangeError异常的子类,该阶段可以看做是对类自身以外(常量池中的各种符号的引用)的信息进行匹配性校验,通常需要校验以下:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类、字段、方法的访问性(private、default、protected、public)是否可被当前类访问

对于虚拟机加载机制来说,验证阶段是非常重要但不一定必要的,如果所运行的全部代码都已经被反复使用和验证过,在实施阶段就可使用-Xverify:none参数关闭大部分的类验证措施,缩短虚拟机类加载时间

3.准备

该阶段正式为类变量分配内存并设置类变量初始值,这些变量使用的初始值都在方法区中进行分配,注意:

(1)进行内存分配的仅包括类变量(被static修饰),不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆

(2)初始值通常情况下是数据类型的零值,eg:

public static int value=123;

变量value在准备阶段后的初始值为0而不是123,这时候尚未开始执行任何java方法

但在相对的特殊情况,如下:

public static final int value=123;

则将value设置为123

4.解析

该阶段虚拟机将常量池内的符号引用替换为直接引用,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行

5.初始化

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,初始化阶段是执行构造器<clinit>()方法的过程

在java类中对类变量指定初始值有两种方式,都被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行它们:

  • 声明类变量时指定初始值
  • 使用静态初始化块为类变量指定初始值

JVM初始化一个类包含以下几个步骤:

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:

  • 创建类的实例,包括:使用new操作符,通过反射,通过反序列化
  • 调用某个类的类方法(静态方法)
  • 访问某个类或接口的类变量,或为该类变量赋值
  • 使用反射的方式强制创建某个类或接口对应的java.lang.Class对象,eg,Class.forName("Person");,如果系统还未初始化Person类,则这行代码会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象
  • 初始化某个类的子类,当初始化某个类的子类时,该子类的所有父类都会被初始化
  • 直接使用java.exe命令运行某个主类

——但是,也有几种特殊情况:

eg,通过子类引用父类的静态字段,不会导致子类初始化:

public class SuperClass {	static{		System.out.println("superclass init");	}	public static int value=123;}public class SubClass extends SuperClass{	static{		System.out.println("SubClass init");	}}public class Test {	public static void main(String[] args) {		// TODO Auto-generated method stub		System.out.println(SubClass.value);	}}

输出:

superclass init123

eg,当某个类变量使用了final修饰符,而且它的值可以在编译时就确定下来,那么程序其他地方使用该变量时,实际上并没有使用该类变量,而是相当于使用常量:

public class ConstClass {	static{		System.out.println("ConstClass init");	}	public static final String HELLOWORLD="Hello World!";}public class Test {	public static void main(String[] args) {		// TODO Auto-generated method stub		System.out.println(ConstClass.HELLOWORLD);	}}

输出:

Hello World!

eg,通过数组定义来引用类,不会触发此类的初始化:

public class Test {	public static void main(String[] args) {		// TODO Auto-generated method stub		SuperClass[] sca=new SuperClass[10];	}}

输出:发现并没有输出superclass init,说明并未触发org.fenixsoft.classloading.SuperClass初始化阶段

附:类加载器

在java中,一个类使用其全限定类名作为标识;JVM中,一个类用其全限定类名和其类加载器作为其唯一标识

从JVM角度,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),用c++语言实现,是虚拟机自身的一部分;另一种是所有其他类加载器,都由java实现,独立于虚拟机外部,且全部继承自抽象类java.lang.ClassLoader 

从java开发角度,有三类:

1.启动类加载器(Bootstrap ClassLoader)

2.扩展类加载器(Extension ClassLoader)

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

双亲委派模型(Parents Delegation Model),除了顶层的启动类加载器,其余的类加载器都应当有自己的父类加载器,并不是一个强制性的约束模型,是java设计者推荐给开发者的一种类加载器实现方式

工作过程:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求,子类加载器才会尝试自己去加载:

protected Class
loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查请求的类是否已经被加载过 Class
c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // 说明父类加载器无法完成请求 } if (c == null) { // 父类加载器无法加载时 // 调用本身的方法进行类加载 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }

持续更新,某些内容的具体。。。

转载地址:https://blog.csdn.net/Autumn03/article/details/81109737 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:ConcurrentHashMap
下一篇:java写一个线程安全的单例模式Singleton

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年03月24日 02时54分01秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章