ClassLoader&双亲委派&类初始化过程
1.class sycle
类加载的生命周期:加载(loading)–>验证(verification)–>准备(preparation)–>解析(resolution)–>初始化(initialization)–>使用(using)–>卸载(unloading)。
关注点1: loading 将class 二进制文件加载到内存中
-
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在java堆中生成一个代表这个类的java.lang.class对象,做为方法区这些数据的访问入口。
加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中。
关注点2: verifaction 这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求
-
- 文件格式验证:验证字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理
- 元数据验证:对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范的要求。
- 字节码验证:这个阶段的主要工作是进行数据流和控制流的分析。任务是确保被验证类的方法在运行时不会做出危害虚拟机安全的行为。
- 符号引用验证:这一阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要是对类自身以外的信息进行匹配性的校验。目的是确保解析动作能够正常执行。
关注点3: preparation 对静态变量赋默认值,而不是初始值(目标指),准备阶段是正式为静态变量分配内存并设置初始值,这些内存都将在方法区中进行分配,这里的变量仅包括类变量(静态变量)不包括实例(成员)变量。
关注点4: resolution :解析是虚拟机将常量池的符号引用替换为直接引用的过程
-
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接饮用是与内存布局相关的。
- 类或接口的解析
- 字段的解析
- 类方法解析
- 接口方法解析
关注点5: initializing 负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化(目标值)initializing 负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化(目标值)
- 遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,如果类没有进行过初始化,出发初始化操作。 访问final 变量除外 ??
- 使用java.lang.reflect包的方法对类进行反射调用时。
- 当初始化一个类的时候,如果发现其父类还没有执行初始化则进行初始化。
- 虚拟机启动时用户需要指定一个需要执行的主类,虚拟机首先初始化这个主类。
- 动态语言支持java,lang.invoke.methodhandle解析结果为ref_getstatic ref_invokestatic的方法句柄时,该类必须初始化。
注意:接口与类的初始化规则在第三点不同,接口不要气所有的父接口都进行初始化。
2 不同类加载器说明
引导类加载器(bootstrap) :
主要负责加载jvm自身需要的类,该加载器由c++实现,加载的是<java_home>/lib 下的class文件,或者 -xbootclasspath 参数指定的路径下的jar包,注意必须由虚拟机按照文件名识别加载jar包,如rt.jar,如果文件名不被虚拟机识别,即使把jar丢到lib目录下也是没有最用的(出于考虑,bootstrap 启动类加载器只加载java、javax、sun开头的类),引导类加载器在hotspot 虚拟中使用c++语言实现,它是虚拟机的一部分。除了引导类加载器之外,其他类加载器都是由java语言实现,并且全部集成自java.lang.classloader,他们是独立于虚拟机外部的。
扩展类加载器(extension) :
扩展类加载是指sun公司实现的类,它是由sun的extclassloader实现的,是lancher类的静态内部类。他负责加载<java_home>/lib/ext目录下或有系统变量-djava.ext.dir指定路径中的类库,开发者可以直接使用标准扩展类加载器
1 public class launcher { 2 ...... 3 static class extclassloader extends urlclassloader { 4 private static volatile launcher.extclassloader instance; 5 6 public static launcher.extclassloader getextclassloader() throws ioexception { 7 if (instance == null) { 8 class var0 = launcher.extclassloader.class; 9 synchronized(launcher.extclassloader.class) { 10 if (instance == null) { 11 instance = createextclassloader(); 12 } 13 } 14 } 15 16 return instance; 17 } 18 } 19 ..... 20 } 21
系统类加载器(应用程序加载器appclassloader):
它是由sun的appclassloader实现的,它负责加载系统路径 java -classpath 或者-d java.class.path指定路径下的类库,也就是我们经常使用到的classpath路径,开发者直接使用系统类的加载器,一般情况下该类加载器是程序组中默认的类加载器,通过classload.getsystemclassloader()方法可以获取到该类的加载器。
自定义类加载器(custom classloader ):
在程序运行期间, 通过java.lang.classloader的子类动态加载class文件, 体现java动态实时类装入特性
3.classloader加载类过程(双亲委派)
jvm在加载类时默认采用的双亲委派机制。通俗讲,就是某个特定的类加载器在接到类加载器的请求时,受限将加载任务委传给父类加载器, 依次递归,如果父类加载器可以完成类的加载任务,就返回成功;只有父类加载器无法完成此加载器任务时,才去自己加载
4.classloader加载类过程(双亲委派流程图)
5.为什么需要双亲委派机制?
为了系统类的安全,类似“java.lang.object”这种核心类,jvm需要保证他们生成的对象都会被认定为同一类型 ,如果用户编写了一个lava.lang.object的同名类并放在classpath中,多个类加载器都去加载这个类到内存中,系统中会出现多个不同的object类,那么类之间的比较傲结果以及唯一性将无法保证,并且如果不使用这种双亲委派模型将会给虚拟机的安全带来安全隐患。所以要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。即“通过代理模式,对于java核心类库的类的加载工作由引导类加载器统一完成,保证了java应用所使用的都是同一个版本的java 核心库的类,是相互兼容的”
好处是防止内存中出现多份相同的字节码。
6.能不能自己写个类叫java.lang.system?答案:通常不可以,但可以采取另类方法达到这个需求。
解释:为了不让我们写system类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而system类是bootstrap加载器加载的,就算自己重写,也总是使用java系统提供的system,自己写的system类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
7.如何自定义类加载器
- 继承 classloader
- overwrite findclass()
8.如何打破双亲委派?
- 集成classloader
- 重写loadclass 方法