栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

虚拟机类加载机制

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

虚拟机类加载机制

类的生命周期可以分为加载,验证,准备,解析,初始化,使用和卸载7个阶段。其中验证,准备,解析三个部分统称为连接。

其中类的加载,验证,准备,初始化,卸载这五部的顺序是确定的,但解析阶段则不一定,它在某些情况下可以等到初始化阶段后在开始,主要是为了支持JAVA语言的动态绑定特性。

1.类的加载

加载过程中JAVA虚拟机需要完成三件事:

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

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

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口。

JAVA虚拟机规范中没有指明如何通过类的全限定名获取此类的二进制字节流,主要方法可分为以下几种:

1)从ZIP压缩包中读取。e.g JAR,EAR,WAR

2)从网络中获取。e.g. WebApplet

3)运行时计算生成。e.g.java.lang.reflect.Proxy中,采用ProxyGenerator.generateProxyClass()来为特定接口生成对应代理类的二进制字节流。

4)由其他文件生成。e.g.JSP文件

5)从数据库中读取。

6)通过加密的文件获取。

非数组类的加载可以由JAVA虚拟机中的引导类加载器来完成,也可以由用户自定义的类加载器去完成。但对数组类而言,情况有些不同:

对于一个数组 T[]

1)若T为引用类型,则需要先加载对应的类,然后将该数组标识在加载该类型的类加载器的名称空间上。

2)若T为基本类型,JAVA虚拟机会把数组标记为与引导类加载器相关联。

3)数组类的可访问性与它的组件类型(即T的类型)的可访问性一致。如不是引用类型,默认为public,引用类型则对应相应类的访问权限。

2.验证

在加载class文件的时候,如果不检查输入的字节流,对其完全信任的话,很可能因为载入了有错误或有恶意企图的字节码导致整个系统受到攻击甚至崩溃。

验证阶段主要进行以下四个动作:

1)文件格式的验证

第一阶段要验证字节流是否符合class文件格式的规范。主要验证的有以下内容:

1.是否以魔术0xCAFEBABE开头。(不懂的可以自行搜索JAVA类文件结构)

2.主、次版本号是否在当前JAVA虚拟机接受范围之内。

3.常量池的常量中是否有不被支持的常量类型(通过tag标志位检查)

4.指向常量的各种索引值是否有指向不存在的常量或不符合常量类型的常量。(常量池中的数据类型仅有17种,索引值越界则无法找到对应的常量)

5.CONSTANT_Utf8_info是否有不符合UTF-8编码的数据

6.Class文件中各个部分及文件本身是否有被删除或附加的其他信息。

2)元数据的验证

第二阶段是验证字节码描述信息的语义的准确性。主要验证以下内容:

1.这个类是否有父类(除java.lang.Object类之外都应有父类)。

2.这个类的父类是否继承了不被允许的类(如被final修饰的类)。

3.如果这个类不是抽象类,则检查其是否实现了父类或接口中的要求实现的所有方法。

4.类中的字段、方法是否与父类产生矛盾(重写父类的final方法,或出现了不符合要求的重载方法)。

3)字节码的验证

第三阶段主要通过控制数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。即在第二阶段完成对元数据信息的数据类型校验之后,对类的方法体(class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出伤害虚拟机的行为。主要验证的内容有:

1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。e.g.不会出现操作数栈放置了一个int类型的数据,使用时按long型加载到本地变量表中。

2.保证任何指令都不会跳转到方法体以外的字节码指令上。

3.保证方法体中的类型转换总是有效的。

JDK6之后在Code属性的属性表中新增加了一项名为StackMapTable的新属性,这项属性描述了方法体所有的基本块开始时本地变量表和操作栈应有的状态,在字节码验证期间,只需要检查StackMapTable中的记录是否合法即可,从而节省了大量的校验时间。可通过 -XX:-UseSplitVerifier类关闭这项优化。

4)符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候。可看做是对类自身以外的各类信息进行匹配性校验。主要验证内容:

1.符号引用中通过字符串描述的全限定名是否能找到对应的类。

2.在指定的类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。

3.符号引用中的类,字段,方法的可访问性是否可以被当前类访问。

3.准备

准备阶段是正式为类中定义的变量(即static修饰的变量)分配内存并设置初始值的时候,其中除static final修饰的变量直接赋予对应值外,其余的变量均赋默认的初始值,后续将在初始化中的方法中完成对它的赋值。

public static int a = 123;//赋值为a =0,而不是123
public static final int b =124;//赋值b=124

至于static final修饰的变量,在类的字段表中存在一个constantValue属性,变量b会按照constantValue中存放的值进行初始化。

4.解析

解析阶段是将JAVA虚拟机中的符号引用替换为直接引用的过程,即字节码解析过后的结果替换为指向实际目标地址的指针、相对偏移量或者一个能间接定位到目标的句柄。通常情况下,JAVA虚拟机会对第一次解析的结果进行缓存,譬如在运行时直接引用常量池中的记录,并把常量表示为已解析的状态,从而避免重复解析。若该符号第一次解析成功,后续可从缓存中获取,如果该符号第一次解析失败,后续请求的对这个符号的解析也会出现失败状态。

解析的动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符这7个符号进行。

重点来介绍一下几个解析:

1)类或接口的解析

class A{
    B b;
}

类似上面的结构,假如当前代码所处的类为A,要把从未见过的一个符号引用解析为类B的引用。解析过程将包括以下3个步骤:

1.判断B的类型是否为数组类型,如果不是,虚拟机将会调用相应的类加载器对类B进行加载。

2.如果B的类型为数组类型,会按照1的规则加载数组的元素类型。之后由虚拟机生成一个代表该数组维度和元素的数组对象。

3.在上面两步没有出现异常后,判断A类是否有对B类的访问权限,如果不具备访问权限,将抛出java.lang.IllegalAccessError异常。

2)字段解析

在解析字段符号时,首先会对字段表内class_index项中索引的CONSTANT_Class_info进行解析,找到字段所属的类或者接口的符号引用。如果解析过程中出现异常,则解析失败。当然,这个字段可能代表的一个该类的父类或实现父接口的属性(即子类中并没有定义这个字段,但这个字段在父类或父接口中有定义),因此在成功解析之后还要根据给类实现的接口及继承的父类对所有接口和类进行后续字段的搜索,并返回父接口与父类中与简单名称和字段描述符都匹配的字段的字段的直接引用。

这样说可能有点抽象,用一段代码来解释:

public class Test7 {
    @Test
    public void test(){
        Son1 son1 = new Son1();
        System.out.println(son1.a);//0
        System.out.println(son1.b);//2
        System.out.println(son1.c);//1
    }
}
class Father1{
    public int a = 0;
}
class Son1 extends Father1 implements interface1{
    public int c = 1;
}
interface interface1{
    public int b = 2;
}

可以看到,Son1()中并没有写a和b属性,但是a和b属性却可以正常输出,这就是JAVA虚拟机在父类及父接口中由下而上进行的字段匹配。

如果在父类或者父类接口中没有找到相应的字段,那么说明不存在这个字段,将抛出异常java.lang.NoSuchField的异常

3)方法解析

在方法解析的过程中,首先要对类进行解析,解析的过程与字段解析相同,与字段一样,同样存在着有继承于父类或实现接口后没有被重写的方法(对于接口来说,如果抽象类实现了接口,可以不重写接口方法),那么此时需要递归地取寻找父类或者父类接口中的相关方法。若最后没有找到相应的方法,则抛出java.lang.NoSuchMethod的异常。

如果解析完成后发现方法的访问权限不足,将会抛出java.lang.IllegalAccessError异常。

5.初始化

类的初始化是类加载的最后一个核心步骤,在这个阶段主要是根据程序编码指定助管计划取初始化类变量和其他资源。

主要步骤如下:

1.()用于对静态变量和静态语句块中的变量赋值。

2.init()方法对非静态变量和非静态语句块中的变量赋值。

3.调用相应的构造方法对类进行构造。 

如果这个类存在父类,在执行子类的()之前会优先执行父类的()方法,对于init()也是一样。另外类的()方法在多线程的环境下只能被一个线程执行,其余线程会阻塞等待,因此()方法只会被执行依次,且可以保证线程初始化的线程安全。

具体可以参考下面这段代码:

public class Print {

     public Print(String s){
         System.out.print(s + " ");
     }
 }
 public class Parent{

     public static Print obj1 = new Print("1");

     public Print obj2 = new Print("2");

     public static Print obj3 = new Print("3");

     static{
         new Print("4");
     }

     public static Print obj4 = new Print("5");

     public Print obj5 = new Print("6");

     public Parent(){
         new Print("7");
     }

 }
 public class Child extends Parent{

     static{
         new Print("a");
     }

     public static Print obj1 = new Print("b");

     public Print obj2 = new Print("c");

     public Child (){
         new Print("d");
     }

     public static Print obj3 = new Print("e");

     public Print obj4 = new Print("f");

     public static void main(String [] args){
         Parent obj1 = new Child ();
         Parent obj2 = new Child ();
     }
 }

输出结果为 :1 3 4 5 a b e 2 6 7 c f d 2 6 7 c f d

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/270644.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号