1、Java的类加载的过程
jvm类加载过程:加载、连接(验证、准备、解析)、初始化。
1.1、加载
- 1)通过一个类的全限定名来获取定义此类的二进制字节流。
- 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 3)在内存中生成一个代表这个类的java.lang.Class对象 ,作为方法区这个类的各种数的访问入口。
加载完类后,JVM外部的二进制字节流就按照虚拟机所需要的格式存储在了方法区或元数据区的内存中了。
1.2 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
这一阶段包括
-
文件格式验证
验证字节流是否符合Class文件的规范,是否能被当前版本的虚拟机处理。如果是否定义了不支持的常量类型,Class各部分及文件本身是否有被删除的部分。 -
元数据验证
对元数据信息进行语义校验,比如:是否继承了不允许被继承的类(final类),是否实现在父类接口要求实现的方法等 -
字节码验证
通过数据流和和控制流,确认程序语义是否合法、符合逻辑,对方法体的校验。比如:对象的转换 -
符号引用验证
符号引用验证可以看做是对类自身以外(主要是常量池中的各种符号引用)的信息进行匹配性校验。目的是确保后面进入解析阶段后,解析动作能够正常执行。如果无法通过符号引用验证,就会抛出如“java.lang.IllegalAccessError”、“java.lang.NoSuchFileIdError”、“java.lang.NoSuchMethodError”等这样的异常信息。
1.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时内存分配的只有类变理,没有实例变量
1.4 解析
在class文件被加载到JVM之前,这个类是无法知道其他类及方法、字段所对应的具体地址的,我们程序定义的都是逻辑地址(符号引用),解析阶段的目地就是用这些符号引用解析成实际引用(物理地址)。
1.5初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
2、3种加载器
2.1、启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
2.2、扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
2.3、应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
3、类加载机制(双亲委派机制)
什么是双亲委派机制?
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
为什么要采用双亲委派机制?
即使是同一个类但被不同的类加载器加载也会不同,所以需要保证同一个类被同一个类加载器加载,所以需要双亲委派机制。
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
即是:为了防止类重复加载。
4、如何定义自己的类加载器
1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可
2、如果想打破双亲委派模型,那么就重写整个loadClass方法
当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。
5、为什么要定义自己的类加载器
因为虽然Java中给用户提供了很多类加载器,但是和实际使用比起来,功能还是匮乏。比如,主流的Java Web服务器,比如Tomcat,都实现了自定义的类加载器(一般都不止一个)