理解Java虚拟机类加载器
java中的类,想要运行就必须把类对应的class文件加载到内存,JVM中真正负责加载class文件内容的是类加载器**
在java中,负责把class文件加载到内存的是类加载器(ClassLoader)
JavaSE-API中,有这么一个类: java.lang.ClassLoader ,它就表示JVM中的类加载器。
JVM启动后,默认会有几种类加载器:
启动类加载器 bootstrapClassLoader,非java语言实现
作用:加载指定路径中jar里面的class文件
路径1:C:\Program Files\Java\jdk1.8.0_74\jre\lib
路径2:C:\Program Files\Java\jdk1.8.0_74\jre\classes\ ( 如果有这个目录的话)
例如:rt.jar
扩展类加载器 ExtClassLoader,java语言实现,是ClassLoader类型的对象
作用:加载指定路径中jar里面的class文件( 只能是jar中存在的class)
路径:C:\Program Files\Java\jdk1.8.0_74\jre\lib\ext
例如:ext中默认存在的jar,或者用户放到ext目录下的jar包
应用类加载器 AppClassLoader,java语言实现,是ClassLoader类型的对象
作用:加载指定路径中class文件或者jar里面的class文件
路径:CLASSPATH中配置路径,这个是用户自己配置的
例如:.:bin:hello.jar
我们最常使用的就是应用类加载器,因为它可以通过CLASSPATH中的路径,去加载程序员自己编写并编译的class文件到内存中。
我们也可以把自己最常用的jar包,放到ext目录中,让扩展类加载器去自动加载这个jar中的class文件到内存中,这样我们的代码就可以直接使用到这个jar中的类了
但是其实,大多数情况下,即使我们需要用到其他jar中的代码,也一般会把jar所在的路径配置到CLASSPATH中,让应用类加载器进行加载,这样会更加方便统一管理项目中使用的所有jar
关于启动类加载器,它不是java语言编写的,我们一般也不要去动它的路径或者jar,它是负责在JVM启动的时候,把JRE环境中最重要的一些library加载到内存,一旦出问题,JVM就无法正常运
行。
在代码程序中,我们也可以看到这些ClassLoader:
import java.util.Properties;
//在java中,使用这个变量来代表启动类加载器加载的路径:sun.boot.class.path
//在java中,使用这个变量来代表扩展类加载器加载的路径:java.ext.dirs
//在java中,使用这个变量来代表应用类加载器加载的路径:java.class.path
public class ClassLoaderTest{
//main方法中的代码,是查看java运行中可以拿到哪些环境变量和对应的值
public static void main(String[] args){
Properties p = System.getProperties();
p.forEach((k,v)->System.out.println(k+"\t"+v));
}
}
import java.util.Properties;
public class ClassLoaderTest{
public static void main(String[] args){
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
//sun.misc.Launcher$AppClassLoader@2a139a55
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println(extClassLoader);
//sun.misc.Launcher$ExtClassLoader@7852e922
ClassLoader bootClassLoader = extClassLoader.getParent();
System.out.println(bootClassLoader);
//null
//注意:启动类加载在java中无法表示,因为它不是java语言实现的。所有这里输出null
}
}
JVM的类加载机制主要有如下3种。
全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
双亲委派机制(如上图),当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader(如果存在UserClassLoader则从UserClassLoader开始)中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException异常。
双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。