文章目录
什么是ClassLoader
类加载器。用于加载.class文件的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。也就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。
class Class<T> {
...
private final ClassLoader classLoader;
...
}
理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那启动时间都要好半天。
延迟加载
JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。
比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。
三个重要的ClassLoader
JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的服务地址来加载。
Bootstrap ClassLoader。可以看到,这些全是JRE目录下的jar包或者是class文件。 最顶层的加载类,主要加载核心类库。负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C/C++ 代码实现的,我们将它称之为「根加载器」。Java 中所有的基本数据类型都是由根加载器加载的!
Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头。
Appclass Loader 加载的路径路径其实就是当前java工程目录bin(src的镜像),里面存放的是编译生成的class文件。直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
实际上,这几个不同的加载器就是通过加载的不同路径进行区分。这也就说明了加载路径对加载器的重要性,同时方便后面引出为工程方便的自定义加载器。
三个ClassLoader的启动过程
我们来看是怎样启动的。当虚拟机启动时就会启动BootstrapLoader,它负责加载Java的核心API,然后bootStrapClassLoader 会装载Launcher.java 之中的 ExtClassLoader(扩展类装载器),并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoaderExtClassLoader 再有 ExtClassLoader 去装载 ext 下的拓展类库,然后Bootstrap Loader 再要求加载Launcher.java 之中的 AppClassLoader(用户自定义类装载器) ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。
总结下来就是这样的图。
为什么Extention ClassLoader的parent为null,就指的是Bootstrap ClassLoader
因为根加载器是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个Java类,也就是无法在java代码中获取它的引用。JVM的API没有暴露根加载器,程序员无法在Java代码中获取根加载器。
如果某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的,也就是说最后追根溯源就是Bootstrap ClassLoader。
用户自定义类加载器是用户自己写的类加载器,但是必须继承 java.lang.ClassLoader 这个类。用户自定义的classLoader的parent是谁呢?答案是AppClassLoader。原因等会再说。
双亲委托模式
接下来,我们先不具体谈类加载过程是什么样的,先来讨论清楚三个类加载器如何打配合的。也就是现在要说的双亲委托。
前面我们提到 AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和 ExtensionClassLoader 来做,这就是我们常说的「双亲委派」。
AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath 。
而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包。
首先伪代码描述当虚拟机去装载一个类的时候会先调用一个叫 loadClass 的方法,接着在这个方法里它会先调用 findLoadedClass 来判断要装载的类字节码是否已经转入了内存,如果没有的话,它会找到它的 parent(这里的 parent 指装载自己的那个类加载器,一般我们的应用程序类的 parent是 AppClassLoader),然后调用 parent 的 loadClass,重复自己 loadClass 的过程,如果 parent 没有装载过着这个类,就调用 findBootstrapClass(这里是指 bootStrap,启动装载器)来尝试装载这个类的字节码,如果 bootStrap 也没有办法装载这个类,则调用自己的 findClass 来尝试装载这个类,如果还是没办法装载则抛出异常。
AppClassLoader查找类时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器(ExtClassLoader)。ExtClassLoader首先也看自己的缓存是否拥有,有则从缓存取得然后返回;没有的话由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下(JAVA_HOME),找到就返回,没找到由让子加载器自己在自己的路径(ExtClassLoader的扩展路径)下去找;查找成功就返回,查找不成功,再向下让子加载器去(AppClassLoader)路径(bin)找。找到就返回,没找到就报异常。
总结就是先在自己的缓存找,没找到向上委托,向下的路径依次查找,找到存到缓存以便下次查找。
双亲委托的好处
上面就是对双亲模式的简单描述,那么双亲委托描述有什么好处?
你尝试一下自己写个 java.lang.String 的类,然后在 ecplise 跑一下,有没有发现抛出了异常,来看看这个异常java.lang.NoSuchMethodError: main运行这个我们自己定义的类的 java.lang.String 的双亲委托模式加载过程如下 AppClassLoader-> ExtClassLoader -> BootstrapLoader,由于 BootstrapLoader 只会加载核心 API 里的类,它匹配到核心 API(JAVA_HOME\jre\lib)里的 String 类,所以它以为找到了这个类就直接去寻找核心 API 里的 String 类里的 main 函数,所以就抛出异常了,而我们自己写的那个 String 根本就没有机会被加载入内存,这就防止了我们自己写的类对 java 核心代码(int,long,String)的破坏。
类加载机制
上面讲的都是classLoader之间的配合。现在来说每个classloader都要遇到的基本的类的加载过程。分成三个部分。
加载->连接->初始化
1.加载:查找并加载类的二进制数据
加载就是将二进制的字节码(.class文件)通过IO输入到JVM中,我们的字节码是储存在硬盘上面的,而所用的类必须加载到内存中才能运行起来,加载就是通过IO把字节码文件从硬盘迁移到内存中。
2.连接
2.1确保被加载的类的正确性
javac编译出来的类都是正确的,但是如果是通过其他途径生成的字节码呢?例如你自己建一个文本文件,然后重命名该文件为 Test.class,然后让 JVM 来运行这个类,显然是不行的。
因此此过程会进行一些验证。类文件的结构检查:该class文件的格式是否符合要求。语义检查:确保类本身符合 Java 语言的语法规定,比如验证 final 类型的类没有子类,被 final修饰的方法不能被覆盖。等一系列的规定。
2.2为类的静态变量分配内存,赋默认值
为类的静态变量分配内存,并将其初始化为默认值,这里我们一定要看清楚是为静态变量分配内存,而不是我们的实例变量,强调静态变量,因为实例变量是什么时候产生的,是生成实例的时候产生的,而我们一般是在 new 一个对象的时候才对这个类进行实例化(前提是这个类已经被加载),而我们现在还没有加载完类,所以这个时候只能对静态变量分配内存空间(静态变量是属于这个类的而不属于某个对象)。
3.初始化
为类的静态变量赋予正确的初始值,上面是赋予默认值,这里是赋予正确的初始值,什么是正确的初始值,就是用户给赋予的值。
自定义类加载器
ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。
loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我使用伪代码表示一下基本过程
class ClassLoader {
// 加载入口,定义了双亲委派规则
Class loadClass(String name) {
// 是否已经加载了
Class t = this.findFromLoaded(name);
if(t == null) {
// 交给双亲
t = this.parent.loadClass(name)
}
if(t == null) {
// 双亲都不行,只能靠自己了
t = this.findClass(name);
}
return t;
}
// 交给子类自己去实现
Class findClass(String name) {
throw ClassNotFoundException();
}
// 组装Class对象
Class defineClass(byte[] code, String name) {
return buildClassFromCode(code, name);
}
}
class CustomClassLoader extends ClassLoader {
Class findClass(String name) {
// 寻找字节码
byte[] code = findCodeFromSomewhere(name);
// 组装Class对象
return this.defineClass(code, name);
}
}
自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。
自定义类加载器的好处
不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?
如果要这样做的话,需要我们自定义一个classloader。
突破了JDK系统内置加载路径的限制之后,我们就可以编写自定义ClassLoader,然后剩下的就叫给开发者你自己了。你可以按照自己的意愿进行业务的定制,将ClassLoader玩出花样来。例如:Class解密类加载器
线程上下文加载器
当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。
如果你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别
class Thread {
...
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}
contextClassLoader「线程上下文类加载器」,这究竟是什么东西?
其次线程的 contextClassLoader 是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是AppClassLoader。
那这个 contextClassLoader 究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。
它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。
如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。
如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。
如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。
ClassLoader 传递性
程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。
因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。
再回首讨论类加载器
这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的集装箱。
不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。
因此类加载器的作用就有。1.屏蔽不同的命名空间,2.保护信任类库的边界外。 集装箱。这也就有了自定义类加载器的出现,为了工程更快的加载。
Java 中编译和运行的区别
编译:编译时是调用检查你的源程序是否有语法错误,如果没有就将其翻译成字节码文件。即.class 文件。
Java 编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用。如果 java 编译器在指定目录下找不到该类所其依赖的类的.class 文件或者.java 源文件的话,编译器话报“cant find symbol”的错误。
编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是一些类名,成员变量名等等 以及 符号引用(方法引用,成员变量引用等等);方法字节码放的是类中各个方法的字节码。
运行:运行时是 java 虚拟机解释执行字节码文件。java 类运行的过程大概可分为两个过程:1、类的加载 2、类的执行。
需要说明的是:JVM 主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,
JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
参考文献
https://blog.youkuaiyun.com/briblue/article/details/54973413
https://www.cnblogs.com/makai/p/11081879.html
http://blog.itpub.net/31561269/viewspace-2222522/