读深入理解Java虚拟机笔记--类加载机制

  已经读到《深入理解java虚拟机》第三部分了,感觉开始飘了,太枯燥了这部分,不过还是跟着书上走了一遍,大概了解了其内容,这部分内容主要类文件结构,类加载机制,执行引擎等组成。本次笔记主要记录类加载过程。

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

1、类加载步骤

  类从被加载到内存到使用完成被卸载出内存,需要经历加载、连接、初始化、使用、卸载这几个过程,其中连接又可以细分为验证、准备、解析。


类的生命周期

(1)加载

  在加载阶段,虚拟机主要完成三件事情:

① 通过一个类的全限定名(比如com.danny.framework.t)来获取定义该类的二进制流;

② 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构;

③ 在内存中生成一个代表这个类的java.lang.Class对象,作为程序访问方法区中这个类的外部接口。

(2)验证

  验证的目的是为了确保class文件的字节流包含的内容符合虚拟机的要求,且不会危害虚拟机的安全。从整体上看,验证阶段大致会完成下面四个阶段的检验动作:

①文件格式验证:主要验证class文件中二进制字节流的格式,比如魔数是否已0xCAFEBABY开头、版本号是否正确等。

②元数据验证:主要对字节码描述的信息进行语义分析,保证其符合Java语言规范,比如验证这个类是否有父类(java.lang.Object除外),如果这个类不是抽象类,是否实现了父类或接口中没有实现的方法,等等。

③字节码验证:字节码验证更为高级,通过数据流和控制流分析,确保程序是合法的、符合逻辑的。

④符号引用验证:对类自身以外的信息进行匹配性校验,举个栗子,比如通过类的全限定名能否找到对应类、在类中能否找到字段名/方法名对应的字段/方法,如果符号引用验证失败,将抛出“java.lang.NoSuchFieldError”、“java.lang.NoSuchMethodError”等异常。

(3)准备

  正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都分配在方法区。注意分配内存的对象是“类变量”而不是实例变量,而且为其分配的是“初始值”,一般数值类型的初始值都为0,char类型的初始值为’\u0000’(常量池中一个表示Nul的字符串),boolean类型初始值为false,引用类型初始值为null。

  但是加上final关键字比如public static final int value=998;在准备阶段会初始化value的值为998;

(4)解析

  解析是将常量池中符号引用替换为直接引用的过程。

  符号引用是以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。

  直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局有关,如果有了直接引用,那引用的目标一定在内存中存在。

  解析的时候class已经被加载到方法区的内存中,因此要把符号引用转化为直接引用,也就是能直接找到该类实际内存地址的引用。

(5)初始化

  类初始化阶段是类加载阶段的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段才真正执行类中定义的Java程序代码(或者说字节码)。

  在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度表达:初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.

  下面看段代码来理解下:


  运行结果如下:


  上面的例子中可以看到一个类从加载到实例化的过程中,静态代码块、构造方法、非静态代码块的加载顺序。

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


  运行结果如下,即一条线程在死循环以模拟长时间操作,另外一条线程在阻塞等待。


2、类加载器

2.1 类与类加载器

  虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为“类加载器”。类加载器除了有加载类的作用,还有一个举足轻重的作用,对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在Java虚拟机中的唯一性。也就是说,两个相同的类,只有是在同一个加载器加载的情况下才“相等”,这里的“相等”是指代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括instanceof关键字对对象所属关系的判定结果。下面是演示代码:


  运行结果:

  可以看到 ,这个对象与类test.Test做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个Test类,一个是由系统应用程序类加载的,另一个是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然为false。

2.2 双亲委派模型

  从虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。但是从java开发人员的角度来看,类加载器还可以划分的更细一些。

① 启动类加载器(Bootstrap ClassLoader)是由C/C++编译而来的,主要负责加载JAVA_HOME\lib目录或者被-Xbootclasspath参数指定目录中的部分类,具体加载哪些类可以通过“System.getProperty(“sun.boot.class.path”)”来查看。

② 扩展类加载器(Extension ClassLoader)由sun.misc.Launcher.ExtClassLoader实现,负责加载JAVA_HOME\lib\ext目录或者被java.ext.dirs系统变量指定的路径中的所有类库,可以用通过“System.getProperty(“java.ext.dirs”)”来查看具体都加载哪些类。

③ 应用程序类加载器(Application ClassLoader)由sun.misc.Launcher.AppClassLoader实现,负责加载用户类路径(我们通常指定的classpath)上的类,如果程序中没有自定义类加载器,应用程序类加载器就是程序默认的类加载器。

  应用程序都是由这3种类加载相互配合进行加载的,如果有必要,还可以加入自定义的类加载器。

  类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父类加载器的代码。

  双亲委派模型的工作方式是:如果一个类加载器收到了子类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最后都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子类加载器才会尝试去自己加载。


  再来看它的实现代码,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,如下面代码所示:


  这段代码的主要意思就是先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

全部评论

相关推荐

评论
点赞
收藏
分享
牛客网
牛客企业服务