您的位置 首页 java

是的!又一篇Java类加载介绍

是的!又一篇Java类加载介绍

本文大纲

类加载基础概念

尝试用5W1H模型来聊聊 Java 的类加载。

什么是类加载? 简单的说,把 字节码 加载到 JVM 中的过程,我们就称之为类加载。输入是某个类的. class文件 的字节流,输出是JVM所管理的方法区中关于该类的信息。

为什么要有类加载? 我的理解是为了更好的支持动态特性,比如说热部署,就是利用了JVM可以动态加载字节码的机制实现的。

什么时候进行类加载? 总的来说,JVM需要某个类的信息,而又没有的时候,就会触发类加载。具体来说分了以下几个场景:

  1. 遇到new、get static 、putstatic、invokestatic等指令时,如果类还没有加载过就会触发类加载;
  2. 子类进行类加载时,如果父类还没有加载过,会先触发父类的加载;
  3. 使用反射进行各种操作时,如果类还没有加载过,会先进行类加载。
  4. 虚拟机 启动时,会首先加载含有main方法的类。
  5. 其他情况,这里不是抄书,所以我们先不再枚举。

谁来负责类加载? 类加载有专门的类加载器来完成,类加载器又有等级森严的层级关系,爷爷辈的类加载器叫启动类加载器,然后是爸爸辈,叫拓展类加载器,最后是应用程序类加载器。这里涉及到一个类加载过程中各个类加载器是如何分工合作的,会在双亲委派模型中提到。

怎样进行类加载? 前面提到过类加载就是把类的字节码塞进虚拟机的过程,那么具体怎么做呢?

首先,类加载器需要从某处获得字节码的 二进制 字节流。为什么不说字节码文件?因为除了从.class文件中获取,还可以从压缩包中解压获取,从网络中获取(比如 Applet ),甚至是动态生成一个(想想动态代理)都是可以的。这个动作,我们称为 加载 。( TO-DO 这个时候生成Class对象了吗?

接着,这个对象还不能直接使用,我们需要把针对它做各种校验,比如字节码本身是否合规,是否是该版本的虚拟机支持,如果都通过了,就需要给静态变量开辟一块内存区域,然后赋零值,这里的零值指的是,当内存中没有数据时,变量的值,比如对于int型来说零值是0,对于 boolean 型来说,零值是false。最后,如果这个类中存在符号引用,还需要把符号引用解析为具体的内存地址。以上所有的动作,我们合并起来,称之为 链接

最后,终于到了给静态字段赋值的时候了,无论是直接赋值还是通过静态块来完成,编译器都会把这些 赋值语句 收集到一起,并且按程序书写的顺序,然后放在一个叫 clinit 的方法中,依次执行。这个动作,我们称为 初始化

类加载进阶

以上是针对类加载机制的一个简单介绍,下面我们进行一些更加高阶的讲解。

一种优雅的单例实现

实现单例有多种不同的写法,也个有优缺点,其中“饿汉式”的写法最为简洁,但缺点是一旦触发了类加载就会同步进行实例化。触发的机制前面的基础篇中已经提到过,比如调用 Resource 中存在的任意一个static字段或者方法,就会触发Resource类的类加载。

而下面的写法通过引入静态内部类完美的解决了这个问题。结合刚才提到的类加载机制,说说这是为什么?

 public class Resource {

    private Resource() {}

    public static Resource  getInstance () {
        return Holder.resource;
    }

     private  static class Holder {
        public static final Resource resource = new Resource();
    }
}  

关于双亲委派模型

刚才介绍了几个不同的类加载器,那么他们之间是怎样合作的?我们结合ClassLoader类中的loadClass方法来看:

 protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException {

   synchronized  (getClassLoadingLock(name)) {

    // 首先,检查该类是否已经被加载过
    Class<?> c = findLoadedClass(name);
    if (c == null) {

      // 针对未加载过的类,先尝试让父类加载器进行加载
      try {
        // 启动类加载器是通过C++实现的,只能表示为null
        // 因此这里有2个逻辑分支
        if (parent != null) {
          c = parent.loadClass(name, false);
        } else {
          // 返回一个启动类加载器加载的类,如果没有则返回null
          c = find Bootstrap ClassOrNull(name);
        }
      } catch (ClassNotFoundException e) {
        
      }

      // 如果父类加载器无法加载,再尝试自己加载
      if (c == null) {
        c = findClass(name);
      }
    }

    // 其他实现细节...

    return c;
  }
}  

通过自定义类加载器实现热部署

热部署指的是不需要重启应用就可以动态的替换掉其中的一些功能,类加载器给我们提供了这样一种实现的思路。

首先,我们说在Java的世界里, 通过一个类的全限定名 + 类加载器,可以唯一的定位一个类 ,也就是说,哪怕是同一个.class文件,通过不同的类加载器进入JVM,它们之间也是互相隔离的。

基于上述事实,当我们希望只是升级某个类的功能时,就可以通过这样的机制来实现:为该类实例化一个新的类加载器,并重新加载该类,最后替换掉之前旧的版本。

根据这样的思路,我们可以定义一个MyTest类,拥有一个showVersion()方法,在第一个版本中会打印1.0,在第二个版本中会打印2.0,代表功能进行了升级。

 public class MyTest {
  public  void  showVersion() {
    System.out.println("1.0版本");
  }
}  

接着,需要自定义一个类加载器,重写部分方法,简单来说,它会根据类的全限定名,在/tmp目录下找对应的字节码文件,针对特定的类,如MyTest,不经过双亲委派模型,直接加载进内存中。

 public class MyClassLoader  extends  ClassLoader {

  // 指定那些类可以通过自定义类加载器的方式加载
  private Set<String> classNamesLoadMyself = new HashSet<>();

  public MyClassLoader(String ... classNames) {
    for (String className : classNames) {
      classNamesLoadMyself.add(className);
    }
  }

  @Override
  protected Class<?> findClass(String name) {
    // 根据路径和类名找到对应的文件并转化为相应的字节流
     byte [] bytes =  File Util.getClassByte("/tmp", name);
    return  define Class(name, bytes, 0, bytes.length);
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    // 如果是指定了要自定义类加载的类,则绕开双亲委派模型
    if (classNamesLoadMyself.contains(name)) {
      return findClass(name);
    }
    return super.loadClass(name);
  }
}  

最后,我们对这个自定义的类加载器做一个测试。

 public class MyClassLoaderClient {

  public static void main(String[] args) throws  Exception  {
    for (int i = 0; i < 10; i++) {
        // 实例化一个类加载器
        MyClassLoader myClassLoader = new MyClassLoader("MyTest");

        // 注意这里不能直接强制类型转化为MyTest
        Object myTest = myClassLoader.loadClass(className).newInstance();
        myTest.getClass().getMethod("showVersion").invoke(myTest);

        // 休眠1秒
        TimeUnit.SECONDS.sleep(1);
    }
  }
}  

在这个测试类中有一行注释,不能将实例化的MyTest做强制类型转换,请问这是为什么呢?

文章来源:智云一二三科技

文章标题:是的!又一篇Java类加载介绍

文章地址:https://www.zhihuclub.com/199358.shtml

关于作者: 智云科技

热门文章

网站地图