首页 > 代码库 > Java基础知识之类加载器

Java基础知识之类加载器

1.类加载器定义

1.1类加载器概述:

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

1.2类的生命周期:

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

技术分享

 

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

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

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

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

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

  文件格式验证

  源数据验证

  字节码验证

  符号引用验证

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

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

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

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

卸载:从内存中释放.

 

1.3 类加载器的作用

类的加载

       类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的Java.lang.Class对象,用来封装类在方法区类的对象。看下面图

技术分享


技术分享

   类的加载的最终产品是位于堆区中的Class对象


   Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类BootStrap,ExtClassLoader,AppClassLoader
类加载器也是Java类,因为其他是java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这正是BootStrap
Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或
者默认采用系统类装载器
为其父级类加载。


1.4获取Class文件途径:

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

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

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

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

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

 

1.5 java.lang.ClassLoader类概述:

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

   技术分享

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


1.6类加载器的划分:

   大部分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类.

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

   技术分享

 

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

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的话,就默认使用启动类加载器作为父加载器.


2.类加载器的委托机制

 双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是:

   如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,

   最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,

   才会交给子类加载器去尝试加载.

  这样的好处是:java类随着它的类加载器一起具备了带有优先级的层次关系.这是十分必要的,比如java.langObject,它存放在\jre\lib\rt.jar中,

它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,

所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了.


Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类BJava虚拟机将使用加载类A的类装载器来加载类B
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器。
当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException,不是再去找发起者类加载器的儿子,因为没有getChild方法,即使有,那有多个儿子,找哪一个呢?


每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的委托模式。类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装载,那就应报告ClassNotFoundException异常。

有一道面试,能不能自己写个类叫java.lang.System,为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,也就是总是使用爸爸们能找到的类,这样总是使用java系统提供的System

把先前编写的类加入到jdkrt.jar中,会有怎样的效果呢?不行!!!看来是不能随意将自己的class文件加入进rt.jar文件中的

     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.lang.System,来替换java提供的System类?

不行,由于类加载的委托机制,父加载器会优先查找类,如找到就加载。


3.编写自己的类加载器

知识讲解:
1.自定义的类加载器的必须继承ClassLoader
2.重写findClass方法
3.findClass中调用defineClass方法
编程步骤:
编写一个对文件内容进行简单加密的程序。
编写了一个自己的类装载器,可实现对加密过的类进行装载和解密。
编写一个程序调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类。程序中可以除了使用ClassLoader.load方法之外,还可以使用设置线程的上下文类加载器或者系统类加载器,然后再使用Class.forName
实验步骤:
对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如:java MyClassLoader MyTest.classF:\itcast
运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoaderjava MyClassLoader MyTest F:\itcast
用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。
删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。


importjava.io.*;

importjava.lang.reflect.*;

publicclass MyClassLoader extends ClassLoader

{

  private String path = null;

  public MyClassLoader(String path) throwsException//检查文件是否存在

  {

  File f = new File(path);

  if(!f.isDirectory())

  {

  throw new RuntimeException(path + " isnot a directory");

  }

  this.path = path;

  }

 

  public Class findClass(String name) //throwsException //为什么不能抛出

  {

  try

  {

  File f = newFile(path,name.substring(name.lastIndexOf(‘.‘)+1) + ".class");

  FileInputStream fis = new FileInputStream(f);

  ByteArrayOutputStream bos = newByteArrayOutputStream();

  cypher(fis,bos);

  byte [] buf = bos.toByteArray();

  fis.close();

  bos.close();

  return defineClass(name,buf,0,buf.length);

  }catch(Exception e)

  {

  throw new ClassNotFoundException(name + "is not found!");

  }

  return null;

  }

 

  public static void cypher(InputStreamistream,OutputStream ostream) throws Exception

  {

  //下面这段代码可能遇到255的字节,当成byte就成了-1

  /*byte b = 0;

  while((b = (byte)istream.read()) != -1)

  {

  ostream.write(b ^ 0xff);

  }*/

 

  int b = 0;

  while((b = istream.read()) != -1)

  {

  ostream.write(((byte)b) ^ 0xff);

  }

  }

  public static void main(String [] args) throwsException

  {

  //下面省略了错误检查

  if(!args[0].endsWith("class"))

  {

  ClassLoader loader = newMyClassLoader(args[1]);

  Class cls = loader.loadClass(args[0]);

 

  /*

  让自定义类继承Date类

  System.out.println(cls.getClassLoader().getClass().getName());

  java.util.Date d =(java.util.Date)cls.newInstance();

  System.out.println(d.toString());

  */

 

  //Method m =cls.getMethod("test",null);//在jdk1.5中报警告,为什么?

  Methodm = cls.getMethod("test");

  //m.invoke(cls.newInstance(),null);

  m.invoke(cls.newInstance());

  //((Test)cls.newInstance()).test();

  return;

  }

  else

  { 

  FileInputStream fis = newFileInputStream(args[0]); 

  File f = new File(args[1], newFile(args[0]).getName());//不用检查目录最后是否有目录分割符

  FileOutputStreamfos = new FileOutputStream(f); 

  cypher(fis,fos);

  fis.close();

  fos.close();

  }

  }

}

//类加载器不能加载这种非public的类

/*

Exceptionin thread "main" java.lang.IllegalAccessException: ClassMyClassLoader

 can not access a member of class MyTest withmodifiers ""

*/

/*

classMyTest

{

  public void test()

  {

  System.out.println("hello,www.it315.org");

  }

}

*/

l编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassloader。
l把MyServlet.class文件打jar包,放到ext目录中,重启tomcat,发现找不到HttpServlet的错误。
l把servlet.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader 。
l父级类加载器加载的类无法引用只能被子级类加载器加载的类,原理如下图:

Ext目录是指JDK中的ext目录D:\javaprograms\Java\jdk1.6.0_01\jre\lib\ext。

放在tomcat中的servlet是由tomcat中的类加载器org.apache.catalina.loader.WebappClassLoader加载的。



Java基础知识之类加载器