双亲委派模型与类的生命周期

双亲委派模型与类的生命周期

一.双亲委派模型

1.双亲委派模型是一种类加载的时候用到的一种模型,它指定了使用什么样的规则来加载类,指定了按照什么样的顺序来调用类加载器从而加载类。至于叫双亲委派,其实是不对的,不像数据结构中树的双亲那样含义明确;不过叫法就无所谓了,看它的作用吧,它的示意图如下(来自http://blog.csdn.net/p10010/article/details/50448491):


2.三个类加载器的作用如下

启动(Bootstrap)类加载器:是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

标准扩展(Extension)类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

1)实践一下类加载器之间的关系:

Main.java

package day_20161010;

public class Main {
	public static void main(String[] args) {
		System.out.println("Main.main():"+Main.class.getClassLoader());
		
		System.out.println("Main.main():"+Main.class.getClassLoader().getParent());
		
		System.out.println("Main.main():"+Main.class.getClassLoader().getParent().getParent());
	}
}

我们看到了它们之间的关系,但是Boot显示为null,因为Boot这个类加载器是由C++语言编写的,在Java语言体系中没有被识别。

2)我们系统的classPath并没有指定路径,为什么可以加载到?

一般情况下,程序默认的类加载器就是App,它可以加载ClassPath系统变量下所指定的类,实际上我们系统的ClassPath只是指定了这个:


当我们使用Eclipse进行开发时,仍然可以加载到类,是因为Eclipse把项目加入到ClassPath中了,打开项目属性可以看到这个:


3.并且这个双亲模型遵循下面的规则:

1)委托机制:就是上面介绍其运行时所说的。 为什么要有这个机制,其实有两个作用,一个是去掉重复,这样父类加载器加载的类,子类加载器不会加载,另一个是安全问题,为了保护核心类库能够被boot和ext所加载,而不是自定义一个核心类库然后被app加载了,从而覆盖掉真正的类库。

这里有一个问题,待会会解释:

A)如果是自定义的类加载器,而且重写的是loadClass方法(虽然这种方式不推荐,而且没有实际意义),不是findClass方法,这样就破坏了双亲委派模型,那么我们是不是就可以覆盖boot所加载的核心类库了?     

B)如果是自定义的类加载器,而且重写的是loadClass方法(虽然这种方式不推荐,而且没有实际意义),不是 findClass方法,这样就破坏了双亲委派模型,那么我们是不是就可以覆盖ext和app所加载的那些类库了?

2)全盘负责:当一个类加载器加载一个类时,该类所依赖和引用的类,也会由这个类加载器所加载

注:其实所谓的双亲模型,指的就是在加载类的时候要先经过父类的判断是否存在。

4.上面的这个模型,在实际程序中是如何运行的?

1)我们继续上一篇文章的分析,class文件是由java.exe来解释为机器码的,它其实是这样的一个过程,首先java.exe先找到jvm.dll,然后启动这个jvm并且进行初始化,之后就产生了boot,这个时候还未执行类加载器,接着这个boot会先载入核心类库【这就解答了3中A那个问题,因为核心类库只能由boot加载】,然后载入Ext,寻找loadCLass方法并且加载完相关的类库,然后载入App,寻找loadCLass方法并且加载完相关的类库,然后如果有自定义类加载器的话(指定是的重写findClass方法,后面的自定义类加载器都指的是重写findClass方法,而不是loadClass,因为loadClass本身就代表着双亲委派模型,是JDK1.2之后引入的,引入这种模型是一种进步),然后就会再载入自定义的类加载器并且加载类。 另外注意一点:

A)加载ext和app,其实就是执行其中的loadClass方法。

B)载入一种类加载器,这个类加载器如果先查找父类,就是符合双亲的,反之就不符合。

看一下ext和app所在类的源码,它们在jre/lib/rt.jat中,以.class文件的形式存在,我们用jd-gui看下:                                                                                                                                     



它们都继承了URLClassLoader,而Url继承了ClassLoader,而且实现了LoadClass方法,是一个比较好的实现模板 

2)看一下3中的B)问题,我们猜测是可能的(本文第一个猜测),因为重写loadClass,破坏双亲模型,其实是意味着该类在加载的时候没有经过父类判断是否存在,这样可能导致的问题是自定义的类可能会与JVM已有的类冲突。 

3)注意一点,实际应用不会重写LoadClass;当我们自定义类加载器的时候,一般就是继承loadClass类,重写findClass就可以了,然后在执行的时候,载入自定义类加载器的时候,会执行loadClass方法,自然就会执行findClass方法了,这个时候的默认父类加载器是app,就是说boot,ext和app把必须的类库已经加载好了。 

5.仔细思考一下,感觉上面4中的3)还会有一个问题,单独拿出来讲。 在重写findClass后,我们使用自定义的类加载器,然后利用上一篇文章中的内容做分析:

1)loader1没有指定父类加载器,app就成为了默认的加载器,这是常用的做法,没有问题。

2)loader2指定loader1为父类加载器,也没有问题。

3)loader3指定boot为父类加载器,那这个和破坏双亲模型有什么区别,我们猜测(本文第二个猜测)可能会出问题的! 这就是要说的问题,所以还是正常点,想1)中的写法就好了,

6.然后再思考下,发现还有一个问题:

当我们自定义类加载器的时候一般遵循双亲模型,这样不会JVM中不会出现完全限定类名相同的类,仔细看上面的第一张图,发现两个自定义类加载器(分别成为A和B)的父类是App,而A和B之间是没有关系的,所以大胆的猜测,自定义的类加载器A和B是可以加载相同完全限定名的类。那这样全限定类名还是类是JVM中的唯一标识么?

找找资料发现,原来这种情况是存在的,因为不同自定义类加载可能代表不用应用,这些应用之间不知道对方,可能出现全限定类名相同的类,为了这种情况,所以把类加载名+全限定类型作为JVM中类的唯一标识符。

双亲委派模型总结:

自定义类加载器时,最好重写findClass,而且使用时最好指定父类加载器为App

二.类的生命周期

本来这篇文章是准备总结类加载器与类生命周期的关联,所以下面在总结的时候,将它们对应起来。

1.什么是类的生命周期?

当我们编写一个java的源文件后,经过编译会生成一个名为class的文件,java类的生命周期就是指一个class文件从加载到卸载的全过程。

2.一个java类的完整的生命周期会经历加载、连接、初始化、使用、卸载五个阶段,也有在加载或者连接之后没有初始化就直接使用的情况,如图所示:


1)加载

其实就是把class文件转化为字节数组(还有其他方式),体现在自定义类加载器就是可以从本地获得class文件或者网络中获取class文件。这个阶段在上一篇文章中就是findClass方法中的loadClassDate方法,把class文件保存在字节数组中。

2)连接

主要是做一些初始化前的准备工作,包括:

A)验证:当一个类被加载后,必须要验证一下这个类是否合法,目的就是保证加载的类是能够被jvm所运行。

这个阶段在上一篇文章中就是findClass方法中的defineClass方法。同时也解释了上篇文章中的一个问题,就是使用自定义加载器时候为什么要使用全限定类型,因为这是类在未加载前的唯一标识(package会被读取),如果不加上package的名字,在defineClass方法中的defineClass1会对其进行检验,发现没有这个类,报如下错误:

<span style="font-family:SimSun;font-size:18px;">Exception in thread "main" java.lang.NoClassDefFoundError:</span>
关于这个错误,还有与之相类似的另一个错误,后面再梳理。


B)准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,不会为它们赋值。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值,针对这一点后面有一个题目来说明。jvm默认的初值是这样的:
--基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
--引用类型的默认值为null。
--常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。

这个阶段在上一篇文章中也是findClass方法中的defineClass,具体来说是postDefineClass方法。


C)解析

这一阶段的任务就是把常量池中的符号引用转换为直接引用。那么什么是符号引用,什么又是直接引用呢?我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

这个阶段在上一篇文章中是loadClass方法中的resolveClass方法,resolveClass就像它的字面意思一样,是用来解析类的。

总结:因为loadClassDate,defineClass和resolveClass都是在类加载器加载的时候执行的,所以说类加载器的执行其实已经是正在完成类的加载和连接(验证,准备,解析)阶段。

3)初始化

一般虚拟机的实现是在使用阶段的时候会触发初始化;使用阶段包括主动引用和被动引用,主动引用会引起类的初始化,而被动引用不会引起类的初始化。

4)使用

主动引用:

通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
通过反射方式执行以上三种行为。
初始化子类的时候,会触发父类的初始化。
作为程序入口直接运行时(也就是直接调用main方法)。

被动引用

引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
定义类数组,不会引起类的初始化。
引用类的常量,不会引起类的初始化。

5)卸载

当使用阶段完成之后,java类就进入了卸载阶段。在类使用完之后,如果满足下面的情况,类就会被卸载:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

看一个相关的题目,本来是要贴上的,后来发现以前文章写过,这里链接一下:http://blog.csdn.net/Jintao_Ma/article/details/51586754

三.总结

到此,自己就基本就学习和总结了

1)类加载器模型,自定义类加载器,类的生命周期

2)它们之前的对应关系

3)同时解决了上篇提到的五个问题

4)想到的一些问题,和对应的两个猜测,由于没有时间去验证,如果有人去实践过,还请告知正确的答案。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值