理解jvm如何加载类

简介

说道类加载器可能大家第一反应就是啊这个东西我没有接触过很难,而且一般情况下对于app开发应用也用不到类加载器,但是对于框架开发者来说类加载器就是家常便饭一样,那类加载器到底是什么东西,真的难道我们都不敢接触了吗?下面就听菜鸟给你慢慢解答!我会通过小标题的方式一步步让大家理解最终的答案因为小标题是理解最终答案的基础

什么是类加载?

jvm(java虚拟机)将xx.class文件读取到内存中,对其进行校验、解析、初始化最终在内存中(jvm的堆和方法区)形成可以直接被jvm使用的类型的过程,这个过程就是类的加载。

什么样的文件可以被jvm加载?

有且仅有一种文件就是后缀名是.class的文件,那么可能有人问,我写的java文件都是.java呀,为什么它就可以执行?

因为编译工具将.java转换成了jvm可执行的.class文件,如果你用的不是java语法可能使用的是jython或者其他语言也只要能将其转化成.class那就可以被jvm加载。至于加载后能不能被使用,那就要看后面那些验证准备等等步骤了。

JVM运行时内存区域如何划分?

image

怎么从一个.class对象成为jvm可以使用的对象?
  1. 加载

    1. 类加载器通过类的全路径限定名读取类的二进制字节流
    2. 将二进制字节流代表的类结构转化到运行时数据区的 方法区中
    3. 在jvm堆中生成代表这个类的java.lang.Class实例作为对方法区中这些数据的访问入口

    这个阶段已经生成了Class对象用来描述这个类文件了。just描述,并没有给类中变量方法等等分配内存。切记!

  2. 验证

    1. 文件格式的验证
      • 文件格式的验证
      • 元数据的验证
      • 字节码验证和符号引用验证

    验证这个阶段目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求。而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同

  3. 准备

    准备阶段给类变量分配内存并且设置类变量初始值的阶段,注意这些内存都将在方法区中分配。

    1. 只是对类的静态变量(static)修饰的变量
    2. 这里设置初始化的值是一些固定的值包括:0、0L、null、false并不是在代码中被显式赋的值
      image
  4. 解析

    解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

    • 类或接口的解析
    • 字段解析
  5. 初始化

    jvm规定只有四种情况下触发初始化

    • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化,通俗的说:
      1. 使用new关键字实例化对象时
      2. 读取或设置一个类的静态字段(static)时被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外
      3. 以及调用一个类的静态方法时

    但是我们要注意:

    • 对于静态字段,只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
    • 常量在编译阶段会存入调用它的类的常量池中,本质上没有直接引用到定义该常量的类,因此不会触发定义常量的类的初始化比如:
      class Const{  
          public static final String NAME = "我是常量";  
          static{  
              System.out.println("初始化Const类");  
          }  
      }  
      public class FinalTest{  
          public static void main(String[] args){  
              System.out.println(Const.NAME);  
          }  
      }  

    打印结果是:
    我是常量

    • 通过数组定义来引用类,不会触发类的初始化
      class Const{  
          static{  
              System.out.println("初始化Const类");  
          }  
      }  
      public class ArrayTest{  
          public static void main(String[] args){  
              Const[] con = new Const[5];  
          }  
      }  

    结果不输出任何信息

为什么要配置java环境变量?

当初学java的时候,我比较笨,一个环境变量配了三天才好,但是比较好一点就是幸好我坚持下来了,虽然是三天,但是我坚持完成了,如果当初不配置完放弃的话可能我也走不到软件这个行业了哈哈。当时就在想为啥要配置这玩意,学c语言的时候就没配置直接可以运行程序呀。那我们就先回顾下配置环境变量如何配?然后在说说配置干啥?

JAVA_HOME

C:\Program Files\Java\jdk1.7

PATH

PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%PATH%;

CLASSPATH

CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

其实配置这些环境变量就是为了让我们可以使用那些指定目录下的工具代码,比如javac,java等等这些命令使用那些工具就是在这目录下的执行程序。

什么是java类加载器

顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并用 对应的java.lang.Class类进行描述。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

类加载器中的核心方法介绍
方法说明
getParent()返回该类加载器的父类加载器。
loadClass(String name)加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name)查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name)查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len)把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class c)链接指定的 Java 类。
类加载器的分类
  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  • 自定义类加载器开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

image

如果我们打印类加载器会有什么结果?
  • 如果我们是自己写的类
public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader cl = Test.class.getClassLoader();
        System.out.println("ClassLoader is:"+cl.toString());
    }
}

打印的结果是:
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93

若是系统那些原有的对象

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader cl = = int.class.getClassLoader();
        System.out.println("ClassLoader is:"+cl.toString());
    }
}

打印的结果是:


Exception in thread “main” java.lang.NullPointerException
at ClassLoaderTest.main(ClassLoaderTest.java:15)

难道没有类加载器加载它吗?肯定不是。

我们讲解一个过程,我们打印parent输出的并不是我们理解的子父类之间的关系,而是组合关系,我们看代码

public class Launcher {
    ...
    private ClassLoader loader;
    public Launcher() {
        ClassLoader extcl;
        ...
        extcl = ExtClassLoader.getExtClassLoader();
        ...
        //将ExtClassLoader对象实例传递进去
        loader = AppClassLoader.getAppClassLoader(extcl);
        ...
    }
}

AppClassLoader在创建的时候将ExtClassLoader作为参数传递进去

public ClassLoader getClassLoader() {
    return loader;
}

我们得到的就是loader对应的对象

static class ExtClassLoader extends URLClassLoader {
    public static ExtClassLoader getExtClassLoader() throws IOException{
            final File[] dirs = getExtDirs();
            try {
                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            //ExtClassLoader在这里创建
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);

        }
    }
}

这段代码说明AppClassLoader的parent是一个ExtClassLoader实例。但是ExtClassLoader创建的时候并没有传递一个parent进去只是将一个文件数组传递进去。


AppClassLoader的parent是一个ExtClassLoader实例,ExtClassLoader的parent是null

需要知道的是Bootstrap ClassLoader是由C++编写的它本身是虚拟机的一部分并不是一个java类那些int,String等等都是由它进行加载,那为什么就是它加载,为什么我们写的Test.java就是AppClassLoader加载呢?jvm如何识别的?就是下个小标题讲的内容。

jvm类加载器加载模式(双亲委托模式)

首先我们介绍一下这种模式的运作:

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

image

规律就是老婆想吃饭能花老公的钱就不花她的,自己能花领导的钱就不用自己的,对于领导来说能花公司的钱就不用自己的。

  • 公司就代指:Bootstrap
  • 领导就代指:ExtClassLoader
  • 老公就代指:AppClassLoader
  • 老婆就代指:自定义ClassLoader

可能例子不太贴切,将就着看

所以对于int,String那些类型,由于我们没有定义,ExtClassLoader没有定义,是Bootstrap 定义了,所以打印出来就是空指针了。

有一段相对权威的话:

JVM有三种类加载器:bootstrap负责加载系统类,extclassloader负责加载扩展类,appclassloader负责加载应用类。他们主要是分工不一样,各自负责不同的区域,另外也是为了实现委托模型。什么是委托模型呢,其实就是当类加载器有加载需求的时候,先请示他的父类使用父类的搜索路径来加入,如果没有找到的话,才使用自己的搜索路径来来搜索类。
当执行 java *.class 的时候, java.exe 会帮助我们找到 JRE ,接着找到位于 JRE 内部的 jvm.dll ,这才是真正的 Java 虚拟机器 , 最后加载动态库,激活 Java 虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰写而成,这个 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的 ExtClassLoader ,并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoader 。然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。这里要请大家注意的是, Launcher ExtClassLoader.classLauncher AppClassLoader.class 都是由 Bootstrap Loader 所加载,所以 Parent 和由哪个类加载器加载没有关系。

  • BootstrapLoader : sun.boot.class.path
  • ExtClassLoader: java.ext.dirs
  • AppClassLoader: java.class.path
如何实现自定义类加载器呢?

笼统来说,不管是什么类型加载器都是加载某些路径下的.jar或者文件或者资源。所以当我们需要动态也就是程序运行时候加载一些东西的话,比如从网络上加载一个class或者从本地磁盘中加载一个jar等这种情况下,我们就需要自定义类加载器。

首先我们必须如下步骤:
- 编写一个类继承自ClassLoader抽象类
- 复写它的findClass()方法
- 在findClass()方法中调用defineClass() 这个方法将.class二进制转化成Class对象

如果一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader

代码案例
public class Test {
    public void say(){
        System.out.println("我叫王菜鸟");
    }
}
public class MyClassLoader extends ClassLoader {

    private String mPath;

    public MyClassLoader(String path) {
        mPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);
        File file = new File(mPath,fileName);
        try {
            FileInputStream is = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            return defineClass(name,data,0,data.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    //获取需要加载 的class文件名
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){ 
            return name+".class";
        }else{
            return name.substring(index)+".class";
        }
    }
}
public class ClassLoaderTest {
    public static void main(String[] args) {
        //创建自定义classloader对象。
        DiskClassLoader diskLoader = new DiskClassLoader("C:\\myclass");
        try {
            //加载class文件
            Class c = diskLoader.loadClass("com.test.Test");
            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //通过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (Exception  e) {
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

最后结果打印:我叫王菜鸟

java自带的有三种类加载器

因为每一种对应的加载路径不一样,同时为了实现双亲委托模式。

拓展思维

我们自定义类加载器过程是通过.class文件最后到内存可使用类,那我们可以在这个过程中动手脚,比如我们对class里面的数据加密,然后在类加载器中读的时候解密,最后defineClass(),

引用:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值