JVM加载class文件

概述

JVM是一个内存中的虚拟机,也就意味着JVM的存储就是内存,我们所写的所有类常量变量方法都在内存中,这决定着我们程序运行的是否健壮,是否高效。

原理

JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。

类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。类的加载方式分为隐式加载显示加载隐式加载指的是程序在使用 new 等方式创建对象时,会隐式地调用类的加载器把对应的类加载到 JVM 中。显示加载指的是通过直接调用 class.forName() 方法来把所需的类加载到 JVM中。

任何一个工程项目都是由许多类组成的,当程序启动时,只把需要的类加载到 JVM 中,其他类只有被使用到的时候才会被加载,采用这种方法一方面可以加快加载速度,另一方面可以节约程序运行时对内存的开销。此外,在 Java 语言中,每个类或接口都对应一个 .class 文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。

在 Java 语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到 JVM中,至于其他类,则在需要的时候才加载。

类加载的主要步骤(家宴准姐出)

由于Java 的跨平台性,经过编译的Java 源程序并不是一个可执行程序,而是一个或多个类文件。当Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM 对类进行初始化,包括:如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。

加载

加载是类加载的第一个过程,在这个阶段,将完成以下三件事情:

  • 通过一个类的全限定名获取该类的二进制流。

  • 将该二进制流中的静态存储结构转化为方法去运行时数据结构。

  • 在内存中生成该类的 Class 对象,作为该类的数据访问入口。

验证

验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

**元数据验证:**对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的

类型转换是否正确,跳转指令是否正确等。

符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

准备

准备,给类中的静态变量分配存储空间,即准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

解析

解析,将符号引用转换为直接引用(这一步可选),该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

符号引用

符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。

个人理解为:在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。

直接引用

直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。

直接引用可以是:

1.直接指向目标的指针。(个人理解为:指向对象,类变量和类方法的指针)

2.相对偏移量。(指向实例的变量,方法的指针)

3.一个间接定位到对象的句柄。

初始化

初始化。对静态变量和静态代码块执行初始化工作。初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java 程序代码。

使用

卸载

类加载ClassLoader

谈谈ClassLoader

ClassLoader在java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作。

从上图中可以看到成员变量parent也是一个ClassLoader,也就说明ClassLoader不是单一的。

ClassLoader的种类

  • BootStrapClassLoader(启动类加载器):C++编写,加载核心库java.*,负责将 Java_Home/lib 下面的类库加载到内存中(比如 rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  • ExtClassLoader(标准扩展类加载器):java编写,加载扩展库javax.*。是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 Java_Home /lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

    在该类中获取class文件的时候,查看源代码是通过如下方式获取的:

    private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];
    
                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }
            return var1;
        }
    
    System.out.println(System.getProperty("java.ext.dirs"));
    打印如下:
    D:\jdk1.8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
    
    该ClassLoader就会在这些路径下面去查找class文件,如果有就加载进来,这里的加载不是所有的都加载,是用到才加载。
    
  • AppClassLoader(应用程序类加载器):java编写,加载程序所在目录,即classpath下面的内容。

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
                final String var1 = System.getProperty("java.class.path");
                final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
                return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                    public Launcher.AppClassLoader run() {
                        URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                        return new Launcher.AppClassLoader(var1x, var0);
                    }
                });
            }
    
    System.out.println(System.getProperty("java.class.path"));
    打印如下:
    D:\jdk1.8\jre\lib\charsets.jar;
    D:\jdk1.8\jre\lib\deploy.jar;
    E:\production\javaweb;
    ...
    其中E:\production\javaweb是项目生成class文件的路径
    
  • 自定义ClassLoader:Java编写,定制化加载

    实现的关键函数:
    protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
    }
    
    @Deprecated
    protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError
    {
           return defineClass(null, b, off, len, null);
    }
    

    实现

    1. 新建一个java文件

      public class Hello{
      	static{
      		System.out.println("Hello World!!!");
      	}
      }
      
    2. 到文件目录下编译

    3. 定义ClassLoader

      public class MyClassLoader extends ClassLoader {
          private String path;
          private String classLoaderName;
      
          public MyClassLoader(String path, String classLoaderName) {
              this.path = path;
              this.classLoaderName = classLoaderName;
          }
      
          //用于寻找类文件
          @Override
          public Class findClass(String name) {
              byte[] b = loadClassData(name);
              return defineClass(name, b, 0, b.length);
          }
      
          //用于加载类文件
          private byte[] loadClassData(String name) {
              name = path + name + ".class";
              InputStream in = null;
              ByteArrayOutputStream out = null;
              try {
                  in = new FileInputStream(new File(name));
                  out = new ByteArrayOutputStream();
                  int i = 0;
                  while ((i = in.read()) != -1) {
                      out.write(i);
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              } finally {
                  try {
                      out.close();
                      in.close();
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              }
              return out.toByteArray();
          }
      }
      
    4. 加载

      public class ClassLoaderChecker {
          public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
              MyClassLoader m = new MyClassLoader("C:\\Users\\longshine\\Desktop\\", "myClassLoader");
              Class c = m.loadClass("Hello");
              System.out.println(c.getClassLoader());
              System.out.println(c.getClassLoader().getParent());
              System.out.println(c.getClassLoader().getParent().getParent());
              System.out.println(c.getClassLoader().getParent().getParent().getParent());
              //实例化
              c.newInstance();
          }
      }
      
      结果如下:
      com.interview.javabasic.reflect.MyClassLoader@677327b6
      sun.misc.Launcher$AppClassLoader@18b4aac2
      sun.misc.Launcher$ExtClassLoader@7f31245a
      null
      Hello World!!!
      

双亲委派模型

该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

双亲委派模型过程

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

使用双亲委派模型的好处在于 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object ,它存在在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的 Bootstrap ClassLoader 进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将混乱。因此,如果开发者尝试编写一个与 rt.jar 类库中重名的 Java 类,可以正常编译,但是永远无法被加载运行。

双亲委派模型的系统实现

java.lang.ClassLoaderloadClass() 方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。

protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
    //check the class has been loaded or not
    Class c = findLoadedClass(name);
    if(c == null){
        try{
            if(parent != null){
                c = parent.loadClass(name,false);
            }else{
                c = findBootstrapClassOrNull(name);
            }
        }catch(ClassNotFoundException e){
            //if throws the exception ,the father can not complete the load
        }
        if(c == null){
            c = findClass(name);
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
}

注意,双亲委派模型是 Java 设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型,但是 JDK 中也有较大规模破坏双亲模型的情况,例如线程上下文类加载器(Thread Context ClassLoader)的出现。

为什么要使用双亲委派模型

  1. 向上委托给父类加载,父类加载不了再自己加载
  2. 避免重复加载,即避免多分同样字节码的加载,防止Java核心api被篡改
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪了个王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值