JVM学习之连接模型(7)

一 符号引用解析,类加载器选择,命名空间基础

 

1 独立的class文件是通过符号引用与其它类互相联系的,这些符号引用都保存在class文件常量池里,class文件被装载后,jvm为其生成一个对应的运行时常量池,符号引用就是保存在运行时常量池里,程序运行中要使用某个符号引用,那么就要对符号引用进行解析,即是根据符号引用查找到实体,再把符号引用替换成一个直接引用的过程,这个过程也称为常量池解析.

        按照类装载子系统装载类的全过程的顺序也可以知道,如果类型已被初始化,那么类型肯定已经连接了,但不保证其使用的符号引用被解析,因为常量池解析发生在符号引用被第一次使用时,只有使用到了,才去解析.

 

2 解析时要对符号引用的存在性和访问权限等进行检验.jvm实现允许在程序执行的不同时间进行解析.不管在什么时机进行解析,都在程序执行过程中第一次实际试图访问一个符号引用时候才会抛出符号引用的相关错误,也就是说,如果程序不使用这个错误使用符号引用的类,错误永远不会被抛出.

 

3 关于Class.forName()和ClassLoader的区别

(1)Class.forName()可以触发类的初始化,而ClassLoader.load()方法可以指定是否连接(注意resolve意指连接而非解析)

(2)类装载器可以以定制的方式,如从指定路径,加载类文件

(3)每一个加载器拥有一个独立的命名空间

(4)类装载器还负责把装载的代码放到保护域中

 

4 初始类装载器 定义类装载器

(1)双亲委派模型的关系是ClassLoader对象的关系,与类的继承关系没有任何关系.但类装载器的编写者可以自由选择不用把请求委托给parent,但会带来安全的问题.

 

(2)在java术语中,要求某个类装载器去装载一个类型,但是却返回了其他类装载器装载的类型,这种装载器被称为是那个类型的初始类装载器;而实际define那个类型的装载器被称为该类型的定义类装载器,定义类装载器也是初始类装载器.

 

(3)每个类装载器有自己的命名空间,★命名空间由所有以此装载器为初始类装载器的类组成(★一个类装载器对应一个类列表,并且这个列表的类是不重复的)。    不同命名空间的两个类是不可见的,但要注意子装载器命名空间包含父装载器命名空间.

 

       值得注意的是,只要得到类所对应的Class实例的reference,还是可以访问另一命名空间的类,这种情形其实也很常见,就是比如在类A的方法中使用一个类装载器去装载一个特定路径下的类B,因此类A与类B不在同一个命名空间,但是类加载器加载类B后会返回B类型的Class实例,所以还是可以在A的方法中访问B的类型数据(■可否通过newInstance方法创建对象呢).

 

(4)关于虚拟机解析符号引用时选择的类装载器的问题

问题: ClassLoader_A将B装载(define)进来,而在B的字节码中引用到了C,但是C已被ClassLoader_A的父装载器ClassLoader_D装载(define)过了,那么在执行B的字节码时解析C的符号引用时,需不需要两次装载C?

 

分析1: 在双亲委派模型下是不用两次define C 的,并且B也能识别到C,因为符号解析时将选择定义B的装载器ClassLoader_A来装载C,所以ClassLoader_A肯定是C的初始类装载器,而ClassLoader_A也是B的初始类装载器,经过用ClassLoader_A装载C的过程,C被加入了ClassLoader_A的类列表(实际上只是由ClassLoader_D返回了C的Class实例,这★取决于双亲委派模型),于是B和C属于同一命名空间

 

分析2: 因为子类加载器的命名空间包括父类加载器的命名空间,所以B和C是可见的(■包括非双亲委派模型都成立吗?)

 

(5)由同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看类装载器是否相同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况

 

(6)总结: 命名空间并没有完全禁止属于不同空间的类的互相访问,■是不是因为父空间中的类在子空间中是可见的,双亲委托模型加强了Java的安全,运行时包增加了对包可见成员的保护

 

二 常量池解析

 

0 ★意识: 

(1)

当前类装载器是指使用引用的类型的定义类装载器,就是当前类的定义类装载器,也就是运行时常量池包含正在被解析的CONSTANT_Class_info入口的类的类装载器;

 

(2) 

当前命名空间 -> 由认为当前类装载器是自己的初始类装载器的类型名字组成

 

(3)findLoadedClass(name):是在类装载器对应的类列表即同一命名空间里查找对应类的Class实例.

 

(4)注意两个类在同一命名空间,和两个类被同一类装载器定义是不同的概念,后者包含前者的情形

 

(5)defineClass()方法的作用是解析二进制数据到内部数据格式,并且创建一个Class实例,它并不连接和初始化类型

 

(6)resolveClass()方法负责连接类

 

(7)后面的分析都是基于双亲委派模型来讨论的

 

1 解析CONSTANT_Class_info入口(替换成Class实例引用)

(1)数组类符号引用解析:

        指向数组类的符号引用的最终解析结果是一个Class实例,表示该数组类.具体过程是: 如果当前类装载器已经被记录为被解析的数组类的初始装载器(即findLoaded调用成功返回同一命名空间的Class实例),就使用同样的类.否则,虚拟机执行以下步骤:

        如果数组元素类型是一个引用类型,则虚拟机用当前类装载器解析元素类型,也就是确认元素类型被装载到当前类装载器的命名空间中;然后执行下一步:

    (如果数组是关于基本类型的数组,那么虚拟机立即执行下一步而不执行上面那步,因为基本类型是启动类装载器定义的)

    虚拟机创建关于元素类型的新数组类,维数也在此时确定,然后创建一个Class的实例代表这个类型.

    注1: 如果是关于引用的数组,数组会被标记为是由定义它的元素类型的类装载器定义的,如果是关于基本类型的数组,数组类会被标记为是由启动类装载器定义的

    注2: ★使用引用的类的定义类装载器(当前类装载器LA), 数组及数组元素定义类装载器LB 两者的关系: LB可能等于LA,也可能是LA的父装载器

 

    总之,当前类装载器要确保元素类型是在自己的命名空间,如果元素类型还不存在于这个命名空间,那么就在命名空间里加入这个元素类型,并使用定义数组元素的类装载器去定义数组类型,然后把数组类型也加入到当前类装载器的命名空间里.

 

(2)非数组类和接口的符号引用解析

st1: 解析非数组类或者接口的基本要求是确认类型被装载到了当前命名空间(即在当前类装载器的类列表里存在对应的Class实例),如果已经存在于当前命名空间,那么直接返回Class实例

 

st2: 如果被引用的类或接口没有被记录在当前类装载器的命名空间,那么就使用当前类装载器去装载被引用的类或接口.当被引用的类型被装载到当前命名空间时,虚拟机会检查它的二进制数据,看它的超类是否被装载进当前的命名空间,如果没有,那么要先装载超类,如此反复,一直到超类为Object为止(★当虚拟机调用超类时,实际上只是解析另外一个符号引用)

 

st3: 在从Object返回路上,虚拟机检查每个类型的数据,确保其实现的接口也被装载,同样类比装载超类的方式去装载超接口,同样,装载超接口也是解析符号引用的过程.

 

注: 当虚拟机递归地在超类和超接口上应用解析过程时,它使用发起引用的子类型的定义类装载器的loadClass()方法.

 

st4: 随着装载结束,虚拟机将检查访问权限(注意这个检查是解析过程的一部分而不属于类装载阶段的正式检验过程).如果装载或检查发生了错误,符号引用解析就失败了.但是如果在检查前一切正常,这个类总体上来说还是可以使用的,只不过不能被使用引用的类型使用.但如果错误在检查权限之前抛出,那么类型是不可使用的

 

st5: 至此,被解析的,被CONSTANT_Class_info入口引用的类型已经被装载,如果虚拟机因为主动使用一个类而正在解析该类,那么会触发类或接口的初始化,而在初始化之前,类必须被连接.

 

st6: 校验类型,比如,如果一个指向特定类的实例的引用被赋予一个爆豆,而该变量被声明为不同的类类型,虚拟机可能不得不装载这两种类型,以确认其中一个是另一个的子类,其他类可能被装载,甚至被连接了,但是肯定不会被初始化(因为要主动使用时才会初始化)

 

2 解析CONSTANT_Fieldref_info入口

(1)首先必须解析对应的CONSTANT_Class_info入口,如果字段的CONSTANT_Class_info解析成功,那么就搜索需要的字段

(2)虚拟机按照如下步骤执行字段搜索过程:

st1: 在被引用的类型中查找具有指定名字和类型的字段

st2: 如果上一步找不到,那么虚拟机检查类型直接实现或扩展的接口,以及递归地检查它们的超接口

st3: 如果不能从接口中搜索到,就检查类型的直接超类,并且递归地检查类型所有的超类

st4: 否则,字段搜索失败,抛NoSuchFieldError异常

(3)如果字段搜索成功,那么还要检查当前类对字段的访问权限,无权限抛IllegalAccessError异常,检查通过则在对应常量池入口的数据中放上指向这个字段的直接引用.

 

3 解析CONSTANT_Methodref_info入口(对一个类中声明方法的符号引用)

(1)首先必须解析对应的CONSTANT_Class_info入口,如果方法的CONSTANT_Class_info解析成功,那么就搜索需要的方法

(2)如果被解析的类型是一个接口,虚拟机抛出IncompatibleClassChangeError异常

(3)否则,检查被引用的类是否含有一个方法符合指定的名字及描述符,如果找到则成功

(4)否则,如果类有一个直接超类,则检查类的挂接真烦并且递归的检查类的所有超类,如果找到则成功

(5)否则,虚拟机检查是否这个类直接实现了任何接口,并且递归地检查出类型直接实现的接口的超接口,如果找到则成功

(6)否则,方法搜索失败,抛出NoSuchMethodError异常

(7)如果查找成功,则检查是否是抽象方法以及当前的类有无权限访问该方法,抽象方法抛AbstractMethodError异常,无权限抛IllegalAccessError异常,检查成功则在对应常量池入口的数据中放上指向这个方法的直接引用

 

4 解析CONSANT_InterfaceMethodref_info入口

(1)首先必须解析对应的CONSTANT_Class_info入口,如果方法的CONSTANT_Class_info解析成功,那么就搜索需要的方法

(2)如果被解析的类型是一个类,而非接口,那么抛出IncompatibleClassChangeError异常

(3)否则,被解析的类型是一个接口,虚拟机检查被引用的接口是否有方法符合指定的名字和描述符,找到则成功

(4)否则,虚拟机检查接口的直接超接口,并且递归地检查接口的所有超接口以及java.lang.Object类来查找符合指定名字和描述符的方法,找到则成功

(5)没有找到,则抛出NoSuchMethodError异常

(6)否则,在对应常量池入口的数据中放上指向这个方法的直接引用

 

5 解析CONSTANT_String_info入口

 

(1)每个java虚拟机必须维护一张内部列表,它列出了所有在运行程序的过程中已被"拘留"的字符串对象的引用,任何特定的字符序列在这个列表上只出现一次

 

(2)所有字面上表达的字符串都在解析CONSTANT_String_info入口的过程中被拘留了.要拘留CONSTANT_String_info入口所代表的字符序列(即由string_index指明的CONSTANT_Utf8_info入口里面保存着的字符序列),虚拟机要检查内部拘留名单上这个字符序列是否已经存在,如果已经存在,虚拟机使用指向以前拘留的字符串对象的引用,否则,虚拟机按照这个字符序列创建一个新的字符串对象,并把这个对象的引用编入列表.

        要完成CONSTANT_String_info入口的解析过程,虚拟机应把指向被拘留(intern)的字符串对象的引用放置到被解析的CONSTANT_String_info常量表入口数据中去(使用新创建的引用或已存在的引用).

 

(3)在java程序中,可以调用String类的intern()方法来拘留一个字符串,.如果具有相同序列的unicode字符串已经被拘留过,intern方法返回一个指向相符的已经被拘留的字符串对象的引用(★如果此时旧引用被拘留的引用取代,那么对应的字符串可以初垃圾收集器收集).如果字符串对象的intern()方法被调用(该字符串对象包含的字符序列还没有被拘留过),那么这个对象本身就被拘留,intern()方法将返回指向同一个字符串对象的引用(这个引用被加入到列表)

 

6 解析其他类型的入口

    CONSTANT_Integer_info入口,CONSTANT_Long_info入口,CONSTANT_Float_info入口,CONSTANT_Double_info入口本身包含它们所表示的常量值,它们可以直接被解析,要解析这类入口,很多虚拟机的实

 

现什么都不需要做,直接使用那些值就行了.

    CONSTANT_Utf8_info入口,CONSTANT_NameAndType_info入口永远不会被指令直接引用.它们只有通过其他入口类型才能被引用,并且在那些引用入口被解析时才被解析.

 

 

三 类型安全与装载约束。。。。。。。。。。。。。。。。。。未完成,待

 

1 "类型混淆"问题,如下例(包名为mytest):

 

st1 设计两个相同全限定名的类C,如下

 

public class C {//正常类C:

private String password = "123";

 

public String getPassword(){  return "secrete!"; }

 

}

 

public class C {//恶意类C:

private String password = "???";

 

public String getPassword(){  return password; }

 

}

 

st2 设计一个类A,如下:

 

public class A{

 

public static C getC(){

return new C();

}

}

 

 

st3 设计一个类B,如下:

 

public class B{

public void useCTest(){

C c = new C();

c = A.getC();

System.out.println(c.getPassword());

}

 

}

 

 

st4 设计一个类装载器ClassLoaderD,重写loadClass()方法:当传进来的名字是C时,用自定义的方式去成功加载C(从指定路径加载);否则用其父类加载器(系统类加载器)去成功装载类

 

st5 设计一个测试类TestLinkageE,如下:

 

public class TestLinkageE{

 

public static void main(String args[]) throws ClassNotFoundException, InstantiationException,

 

IllegalAccessException{

ClassLoaderD classLoaderD = new ClassLoaderD(args[0]);

Class classB = classLoaderD.loadClass("mytest.B", false);

B b = (B)classB.newInstance();

b.useCTest();

}

 

}

 

st6 关于如何测试:

(1)先将两个相同全限定名的类C分别编译,放在不同路径下的mytest包里,一个放在"恶意类C",另一个放在"正常类C"

(2)编译其他类,B.class放在"恶意类C",其他放在"正常类C"的mytest包里

(3)运行测试: java TestLinkageE E:/知识学习/JVM学习/类混淆测试/恶意类C

 

st7 运行结果分析:

...........................至此,测试还不成功!!!!!!!!!!!!!!!!!!!!!!!!!!p255

 

 

类A由classLoaderA所加载,类B由classLoaderB所加载

执行赋值语句A.a = B.b,由于这两个类型均为X,可以执行,但是有一个要求,这个要求就是在A中所装载类X的装载器必须和在B中装载类X的装载器相同,否则赋值语句失败

 

1 编译时,类型被它的全限定名所惟一确定.但在运行时,要惟一地确定一个类型,需要知道类型的全限定名以及这个类型的定义类装载器,分析如下:

 

(1)类型安全性问题的出现是因为在一个java虚拟机中的多个命名空间可能共享类型,如果某个类装载器委派另外一个类装载器,而后者定义了这个类型,这两个类装载器都会被标记为这个类型的初始类装

 

载器.被委派的类装载器装载的这个类型,在所有被标记为该类型的初始类装载器的命名空间中共享

 

 

(2)假如两个命名空间

 

1 ■如果使用引用的类型和被引用的类型并非由同一个初始装载器装载,虚拟机必须确保在字段或者方法描述符中提及的类型在不同的命名空间中保持一致,即他们指向同一的类型数据(在方法区中).

 

这句话理解起来非常的困难,简单一点理解可以由下面的说明来理解:(http://www.iflym.com/index.php/code/understand-jvm-load-constraint.html)

 

类A中有一个字段a,它的类型为X

类B中有一个字段b,它的类型也为X

类A由classLoaderA所定义,类B由classLoaderB所加载定义

执行赋值语句A.a = B.b,由于这两个类型均为X,可以执行,但是有一个要求,这个要求就是在A中所装载类X的装载器必须和在B中装载类X的装载器相同,否则赋值语句失败

 

2 为了确保java虚拟机实现能够保证类型在不同命名空间保持一致性,java虚拟机规范定义了的几种装载约束.每一个java虚拟机都必须维护一个有关这些约束的内部列表,每一个约束基本上都表明了一个命名空间中的某个名字必须和另一个命名空间中的同一个名字指向同一类型数据.

    

 

 

四 关于直接引用

 

(1)Class文件定义了符号引用的格式,但直接引用的格式是由不同的java虚拟机实现的设计者决定的

(2)指向类型,类变量和类方法的直接引用可能是指向方法区的本地指针.

(3)指向实例变量和实例方法的直接引用是偏移量.实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量,实例方法的直接引用可能是到方法表的偏移量.在任何实现中,对象中字段的顺序和方法表中方法的顺序是被定义好的,是可以预测的.

 

(4)方法表中保存的指向是对象实际的类的方法(动态绑定,★不可能是接口对应的那个方法的指向,因为没有实际可执行的字节码),它们按父类方法,本类方法的顺序排列,如果本类重写了父类方法,那么在方法表中父类方法该放置的地方替换为本类方法的指向,每个方法的指向对应着一个方法表索引(即偏移量),偏移量从0开始,这个索引就是直接引用.方法表中只保存实例方法(■是否所有父类和子类的实例方法,包括final和private方法).

 

(5)给定一个指向对象的引用,每一个虚拟机实现必须有办法找到这个对象的类的类型数据,除此这外,给定一个指向对象的引用,需要非常快地访问方法表(方法表是对象的类的类型数据的一部分,也就是说,一个类的所有对象共享一个方法表).

 

(6)给定接口引用时调用方法总是比给定类引用时调用方法慢得多,因为接口的方法对应的常量池解析出来的直接引用不是一个固定的偏移量,而类的方法对应的常量池解析出来的直接引用是一个固定的偏移量.对于每次调用接口的方法,jvm必须搜索对象的类的方法表来找到一个合适的方法.原理如下:

 

假如ClassA是类,InterfaceA是接口,ca是类的实例方法,ia是接口的方法

 

(1)当程序运行到ClassA.ca(),如果这个符号引用还未被解析,那么要先解析,于是找到对象对应的类型的方法表,这个方法表保存着对象对应的类及父类一直到ClassA的实例方法,这个对象不是ClassA的实例就是其子类的实例,所以每个方法的索引(偏移量)固定,于是该索引为解析后的直接引用

 

(2)当程序两次运行到像ClassA.ca()的语句时,如果符号引用已被替换成直接引用,那么直接使用即可

 

(3)当程序运行到InterfaceA.ia(),如果这个符号引用还未被解析,那么要先解析,于是找到对象对应的类型的方法表,这个方法表保存着对象对应的类及父类的实例方法,但是这个对象的类继承体系是不明确的,所以这个Ia()在方法表的索引也是不明确的,每次程序运行到InterfaceA.ia()这样的语句时,每次都要进行一次符号解析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值