java笔记--理解java类加载器以及ClassLoader类

时间:2023-12-16 19:20:02

类加载器概述:

  java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制.JVM中用来完成上述功能的具体实现就是类加载器.类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例.每个实例用来表示一个java类.通过该实例的newInstance()方法可以创建出一个该类的对象.

类的生命周期:

  类从加载到虚拟机内存到被从内存中释放,经历的生命周期如下:

java笔记--理解java类加载器以及ClassLoader类

加载:"加载"是"类加载"过程的一个阶段,此阶段完成的功能是:

  通过类的全限定名来获取定义此类的二进制字节流

  将此二进制字节流所代表的静态存储结构转化成方法区的运行时数据结构

  在内存中生成代表此类的java.lang.Class对象,作为该类访问入口.

验证:连接阶段第一步.验证的目的是确保Class文件的字节流中信息符合虚拟机的要求,不会危害虚拟机安全,使得虚拟机免受恶意代码的攻击.大致完成以下四个校验动作:

  文件格式验证

  源数据验证

  字节码验证

  符号引用验证

准备:连接阶段第二步,正式为类变量分配内存并设置变量的初始值.(仅包含类变量,不包含实例变量).  

解析:连接阶段第三步,虚拟机将常量池中的符号引用替换为直接引用,解析动作主要针对类或接口,字段,类方法,方法类型等等..

初始化:类的初始化是类加载过程的最后一步,在该阶段,才真正意义上的开始执行类中定义的java程序代码.该阶段会执行类构造器.

使用:使用该类所提供的功能.

卸载:从内存中释放.

获取Class文件途径:

  java类可以动态被加载到内存,这是java的一大特点,也称为运行时绑定,或动态绑定.

     1.从ZIP包中读取,很常见,最终成为日后JAR,WAR,EAR格式的基础.

     2.从网络中获取,这种场景典型的就是Applet.

     3.运行时计算生成,典型的情景就是java动态代理技术.

     4.从其他文件中生成,典型场景是JSP应用,即由JSP文件生成对应的Class类.

java.lang.ClassLoader类概述:

  中文文档中对ClassLoader类的定义如下:

   java笔记--理解java类加载器以及ClassLoader类

   从文档中对ClassLoader类的介绍可以总结出这个类的作用就是根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例.

类加载器的划分:

   大部分java程序会使用以下3中系统提供的类加载器:

   启动类加载器(Bootstrap ClassLoader):

    这个类加载器负责将<JAVA_HOME>\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分.

   扩展类加载器(Extendsion ClassLoader):
   
 这个类加载器负责加载<JAVA_HOME>\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器.

   应用程序类加载器(Application ClassLoader):

    这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.

  除此之外,我们还可以加入自己定义的类加载器,以满足特殊的需求,需要继承java.lang.ClassLoader类.

  类加载器之间的层次关系如下图:

   java笔记--理解java类加载器以及ClassLoader类

使用代码观察一下类加载器:

package com.wang.test;

public class TestClassLoader {

    public static void main(String[] args) {
ClassLoader loader = TestClassLoader.class.getClassLoader();
System.out.println(loader.toString());
System.out.println(loader.getParent().toString());
System.out.println(loader.getParent().getParent());
}
}

  观察打印结果:

sun.misc.Launcher$AppClassLoader@500c05c2
sun.misc.Launcher$ExtClassLoader@454e2c9c
null

  第一行打印的是应用程序类加载器(默认加载器),第二行打印的是其父类加载器,扩展类加载器,按照我们的想法第三行应该打印启动类加载器的,这里却返回的null,原因是getParent(),返回时null的话,就默认使用启动类加载器作为父加载器.

类加载器的双亲委派模型:

  双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载.

  这样的好处是:java类随着它的类加载器一起具备了带有优先级的层次关系.这是十分必要的,比如java.langObject,它存放在\jre\lib\rt.jar中,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了.

Class.forname()与ClassLoader.loadClass():

  Class.forname():是一个静态方法,最常用的是Class.forname(String className);根据传入的类的全限定名返回一个Class对象.该方法在将Class文件加载到内存的同时,会执行类的初始化.

  如: Class.forName("com.wang.HelloWorld");

  ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.

  如:ClassLoader cl=.......;cl.loadClass("com.wang.HelloWorld");

  


注:整理这篇笔记,参考了<深入理解java虚拟机>这本书,以及深入探讨 Java 类加载器这篇文章,想深入了解可以去看看.