深入理解Java虚拟机-ClassLoader

什么是类装载器ClassLoader

  ClassLoader是一个抽象类

  ClassLoader的实例将读入Java字节码将类装载到JVM中

  ClassLoader可以定制,满足不同的字节码流获取方式(譬如从网络中加载,从文件中加载)

  ClassLoader负责类装载过程中的加载阶段

java中的类大致分为三种: 

    1.系统类 

    2.扩展类 

    3.由程序员自定义的类

类装载方式,有两种

      1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中, 
      2.显式装载, 通过class.forname()等方法,显式加载需要的类

  其实,它们都是通过调用 ClassLoader 类的 loadClass 方法来完成类的实际加载工作的

ClassLoader的重要方法:

  • findLoadedClass:每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,该方法就是在该名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回 null。这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。

  • getSystemClassLoader:Java2 中新增的方法。该方法返回系统使用的 ClassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。

  • defineClass:该方法是 ClassLoader 中非常重要的一个方法,它接收以字节数组表示的类字节码,并把它转换成 Class 实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。

  • loadClass:加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重新实现,我们可以完全控制和管理类的加载过程。

  • resolveClass:链接一个指定的类。这是一个在某些情况下确保类可用的必要方法,详见 Java 语言规范中“执行”一章对该方法的描述。

ClassLoader体系结构:

blob.png

ClassLoader 在加载类时有一定的层次关系和规则。在 Java 中,有四种类型的类加载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的 ClassLoader。这四种类加载器分别负责不同路径的类的加载,并形成了一个类加载的层次结构。

   BootStrapClassLoader: 处于类加载器层次结构的最高层,负责 sun.boot.class.path 路径下类的加载,默认为 jre/lib 目录下的核心 API 或 -Xbootclasspath 选项指定的 jar 包 (启动ClassLoader)

   ExtClassLoader 的加载路径为 java.ext.dirs,默认为 jre/lib/ext 目录或者 -Djava.ext.dirs 指定目录下的 jar 包加载(扩展ClassLoader)

   AppClassLoader 的加载路径为 java.class.path,默认为环境变量 CLASSPATH 中设定的值。也可以通过 -classpath 选型进行指定

    CustomClassLoader 用户自定义 ClassLoader 可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。

    这四种类加载器的层次关系图如上图所示。一般来说,这四种类加载器会形成一种父子关系,高层为低层的父加载器。在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常,

  Java 类的加载过程如图所示:

  blob.png

      java采用了委托模型机制,这个机制简单来讲,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类”, 具体的几个测试用例请参考这里

     每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。

我们看看下面的代码:

1.    package java.lang;  
2.      
3.    public class String {  
4.        public static void main(String[] args){  
5.              
6.        }  
7.    }

    大家发现什么不同了吗?对了,我们写了一个与JDK中String一模一样的类,连包java.lang都一样,唯一不同的是我们自定义的String类有一个main函数。我们来运行一下:

      java.lang.NoSuchMethodError: main    Exception in thread "main"

 这是为什么? 我们的String类不是明明有main方法吗?

 

    其实联系我们上面讲到的双亲委托模型,我们就能解释这个问题了。

       运行这段代码,JVM会首先创建一个自定义类加载器,不妨叫做AppClassLoader,并把这个加载器链接到委托链中:AppClassLoader -> ExtClassLoader -> BootstrapLoader。

      然后AppClassLoader会将加载java.lang.String的请求委托给ExtClassLoader,而 ExtClassLoader又会委托给最后的启动类加载器BootstrapLoader。

      启动类加载器BootstrapLoader只能加载JAVA_HOME\jre\lib中的class类(即J2SE API),问题是标准API中确实有一个java.lang.String(注意,这个类和我们自定义的类是完全两个类)。BootstrapLoader以为找到了这个类,毫不犹豫的加载了j2se api中的java.lang.String。

      最后出现上面的加载错误(注意不是异常,是错误,JVM退出),因为API中的String类是没有main方法的。

 ClassLoad loadClass方法:

blob.png

    结论:我们当然可以自定义一个和API完全一样的类,但是由于双亲委托模型,使得我们不可能加载上我们自定义的这样一个类。所以J2SE规范中希望我们自定义的包有自己唯一的特色(网络域名)。还有一点,这种加载器原理使得JVM更加安全的运行程序,因为黑客很难随意的替代掉API中的代码了

 

  双亲模式的问题:

 顶层ClassLoader,无法加载底层ClassLoader的类

 

   怎么解决了??

   可以通过Thread. setContextClassLoader()   上下文加载器只是是一个上下文的角色

         用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题

         基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例

    如:javax.xml.parsers.FactoryFinder

blob.png

 双亲模式的破坏

  双亲模式是默认的模式,但不是必须这么做

   Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent

   OSGi的ClassLoader形成网状结构,根据需要自由加载Class

通过定义ClassLoad实现热替换:

      原理是通过自定义的ClassLoad,加载和卸载类,不直接通过双亲委托机制:我们为该类加载器指定一些必须由该类加载器直接加载的类集合,在该类加载器进行类的加载时,如果要加载的类属于必须由该类加载器加载的集合,那么就由它直接来完成类的加载,否则就把类加载的工作委托给系统的类加载器完成。

     在给出示例代码前,有两点内容需要说明一下:1、要想实现同一个类的不同版本的共存,那么这些不同版本必须由不同的类加载器进行加载,因此就不能把这些类的加载工作委托给系统加载器来完成,因为它们只有一份。2、为了做到这一点,就不能采用系统默认的类加载器委托规则,也就是说我们定制的类加载器的父加载器必须设置为 null。该定制的类加载器的实现代码如下:

清单 1. 定制的类加载器的实现代码

public class HotSwapURLClassLoader extends URLClassLoader {
 //缓存加载class文件的最后最新修改时间
  public static Map<String,Long> cacheLastModifyTimeMap = new HashMap<String,Long>();  
  
  //工程class类所在的路径
  public static String projectClassPath = "F:\\viemall_workspace\\viemall\\WebRoot\\WEB-INF\\classes\\";
   
  //所有的测试的类都在同一个包下
  public static String packagePath = "jvm\\viemall\\test\\testclassloader\\";
  
  private static HotSwapURLClassLoader hcl = new HotSwapURLClassLoader(); 
  public HotSwapURLClassLoader() {
       //设置ClassLoader加载的路径
      super(getMyURLs());
  }
  
  public static HotSwapURLClassLoader  getClassLoader(){
     return hcl;
  }
  
     private static URL[] getMyURLs() {
        URL url = null;
        try {
          url = new File(projectClassPath).toURI().toURL();
          //file:/F:/viemall_workspace/viemall/WebRoot/WEB-INF/classes/
        } catch (MalformedURLException e) {
           e.printStackTrace();
        }
        return new URL[] { url };
  }
  
  
/**
 * 重写loadClass,不采用双亲委托机制("java."开头的类还是会由系统默认ClassLoader加载)
 */
@Override
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
    Class clazz = null;
    //查看HotSwapURLClassLoader实例缓存下,是否已经加载过class
     //不同的HotSwapURLClassLoader实例是不共享缓存的
    clazz = findLoadedClass(name);
     if (clazz != null ) {
     if (resolve){
     resolveClass(clazz);
     }
     //如果class类被修改过,则重新加载
    if (isModify(name)) {
    System.out.println("文件被修改需要重新加载......");
     hcl = new HotSwapURLClassLoader();
     clazz = customLoad(name, hcl);
    }
        return (clazz);
 }
//如果类的包名为"com."开始,则有系统默认加载器AppClassLoader加载
 if(name.startsWith("java.")){
try {
//得到系统默认的加载cl,即AppClassLoader
 ClassLoader system = ClassLoader.getSystemClassLoader();  
 clazz = system.loadClass(name);
 if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
 }
} catch (ClassNotFoundException  e) {
e.printStackTrace();
}
 }
    return customLoad(name,this);
}
 public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException {
 return customLoad(name, false,cl);
 }
 
public Class customLoad(String name, boolean resolve, ClassLoader cl)
throws ClassNotFoundException {
 //findClass()调用的是URLClassLoader里面重载了ClassLoader的findClass()方法
Class clazz = ((HotSwapURLClassLoader)cl).findClass(name);
if (resolve)
 ((HotSwapURLClassLoader)cl).resolveClass(clazz);
 //缓存加载class文件的最后修改时间
long lastModifyTime = getClassLastModifyTime(name);
cacheLastModifyTimeMap.put(name,lastModifyTime);
return clazz;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
return super.findClass(name);
}
/**
 * 判断这个文件更上次是否修改过
 * @param name
 * @return
 */
private boolean isModify(String name) {
long lastmodify = getClassLastModifyTime(name);
long previousModifyTime = cacheLastModifyTimeMap.get(name);
if(lastmodify>previousModifyTime){
return true;
}
return false;
}
private long getClassLastModifyTime(String name) {
String path = getClassCompletePath(name);
File file = new File(path);
if(!file.exists()){
 throw new RuntimeException(new FileNotFoundException(name)); 
    }
return file.lastModified();
}
/**
 * 获取固定Class的文件
 * @param name
 * @return
 */
private String getClassCompletePath(String name) {
    String simpleName = name.substring(name.lastIndexOf(".")+1);
    StringBuffer buffer=new StringBuffer();
    if(!projectClassPath.endsWith("/")){
    buffer.append(projectClassPath).append(File.separator).append(packagePath);
    }else{
    buffer.append(projectClassPath).append(packagePath);
    }
    if(!packagePath.endsWith("/")){
    buffer.append(File.separator).append(simpleName).append(".class");
    }else{
    buffer.append(simpleName).append(".class");
    }
    return buffer.toString();
}
public static void main(String[] args) {
URL [] url=getMyURLs();
for(URL u:url){
//file:/F:/diamond-304_workspace/viemall/WebRoot/WEB-INF/classes/
System.out.println(u.toString());
}
}
 
}

在该类加载器的实现中,所有指定必须由它直接加载的类都在该加载器实例化时进行了加载,当通过 loadClass 进行类的加载时,如果该类没有加载过,并且不属于必须由该类加载器加载之列都委托给系统加载器进行加载。

清单 2. 定义热替换实现类

public class Hot {
public void hot() {
   System.out.println(" version 2 : " + this.getClass().getClassLoader());
    System.out.println("重新编译Class");
}
}
清单 3. 实现定时任务的部分代码

public class TestHotSwap {
  public static void main(String[] args) throws Exception {
  
    //开启线程,如果class文件有修改,就热替换
     Thread t = new Thread(new MonitorHotSwap());
    t.start();
 
  }
}
class MonitorHotSwap implements Runnable {
    // Hot就是用于修改,用来测试热加载
    private String className = "jvm.viemall.test.testclassloader.Hot";
    private Class hotClazz = null;
    private HotSwapURLClassLoader hotSwapCL = null;
    @Override
    public void run() {
        try {
            while (true) {
             initLoad();
             Object hot = hotClazz.newInstance();
             Method m = hotClazz.getMethod("hot");
             m.invoke(hot, null); //打印出相关信息
             // 每隔10秒重新加载一次
             Thread.sleep(10000);
            }
            } catch (Exception e) {
            e.printStackTrace();
        }
    }
    void initLoad() throws Exception {
         hotSwapCL = HotSwapURLClassLoader.getClassLoader();
        // 如果Hot类被修改了,那么会重新加载,hotClass也会返回新的
        hotClazz = hotSwapCL.loadClass(className);
    }
}

编译、运行我们的系统,会出现如下的打印:

blob.png

在系统仍在运行的情况下,编译,重新修改Hot(我是直接Myeclise编译)或者手段编译替换目录下原来的Hot.class 文件,我们再看看屏幕的打印,奇妙的事情发生了,新更改的类在线即时生效了,我们已经实现了 Hot 类的热替换。屏幕打印如下:

blob.png

 

参考文章:

     http://alaric.iteye.com/blog/2262566?utm_source=tuicool&utm_medium=referral

   JVM性能调优参考资料: 《深入理解Java虚拟机》-周志明

   

发表评论