Java类加载机制
Java类加载器是什么
Java类加载器(Java Classloader)负责动态地将Java类加载到Java虚拟机的内存空间内,Java提供了3种类加载器,每个类加载器负责加载特定位置的Java类
分类
1、Bootstrap ClassLoader(引导类加载器)
2、ExtClassLoader(扩展类加载器)
3、AppClassLoader(系统类加载器)
从Java虚拟机的角度,只存在两种不同的类加载器:
1,启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
2,其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader, 这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
2,从Java开发人员的角度来看,类加载器可以大致划分为以下三类:
复制代码
1,启动类加载器:Bootstrap ClassLoader,跟上面相同。
它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,
并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。
启动类加载器是无法被Java程序直接引用的。
2,扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,
它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
3,应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,
它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,
如果应用程序中没有自定义过自己的类加载器,
一般情况下这个就是程序中默认的类加载器。
Bootstrap ClassLoader(引导类加载器)
该类加载器通常由C++语言实现,不继承任何Java类,负责加载System.getProperty(“sun.boot.class.path”)所指定核心Java库,也可以通过java -Xbootclasspath指定其搜索路径;
public class Test {
public static void main(String[] args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
1
2
3
4
5
会在控制台输出
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\resources.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\rt.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\sunrsasign.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jce.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\classes
1
很长,不方便看
使用数组
public class Test {
public static void main(String[] args) {
String path = System.getProperty("java.ext.dirs");
String [] paths = path.split(";");
for (String string : paths) {
System.out.println(string);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
输出如下
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\resources.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\rt.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\sunrsasign.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jce.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\classes
1
2
3
4
5
6
7
8
ExtClassLoader(扩展类加载器):该类加载器由sun.misc.Launcher$ExtClassLoader类实现,负责加载System.getProperty(“java.ext.dirs”)所指定的Java的扩展库,也可以通过java -Djava.ext.dirs指定其搜索路径,例如:java -Djava.ext.dirs=d:\classes HelloWorld;注意:如果将自己开发的 jar 文件放在System.getProperty(“java.ext.dirs”)所指定的目录中,也会被 ExtClassLoader类加载器加载;
代码
public class Test {
public static void main(String[] args) {
System.out.println(System.getProperty("java.ext.dirs"));
}
}
1
2
3
4
5
输出
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
1
需要说明的是
如过我们将自己导出的jar文件,放在java.ext.dirs路径下,会被加载,但是
AppClassLoader(系统类加载器)
该类加载器由sun.misc.Launcher$AppClassLoader类实现,负责加载System.getProperty(“java.class.path”)或CLASSPATH环境变量所指定的Java类,也可以加上-cp来覆盖原有的classpath设置,例如: java -cp ./classes HelloWorld;说明:默认情况下自定义类都由该类加载器加载。
public class Test {
public static void main(String[] args) {
System.out.println(System.getProperty("java.class.path"));
}
}
1
2
3
4
5
输出
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\resources.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\rt.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jce.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-32.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;D:\Program_Files\workspace\ClassLoa\bin
1
为了便于观察我们我们使用数组,
public class Test {
public static void main(String[] args) {
String path = System.getProperty("java.class.path");
String [] paths = path.split(";");
for (String string : paths) {
System.out.println(string);
}
}
}
1
2
3
4
5
6
7
8
9
10
输出如下
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\resources.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\rt.jar
;D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jce.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-32.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;
D:\Program_Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;
D:\Program_Files\workspace\ClassLoa\bin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到,最后一个就是我们自定义的eclipse的工作环境,项目名,根目录
加载器之间的关系
Java虚拟机中的所有类加载器采用具有父子关系的树形结构进行组织
加载机制的证明,代码如下
public class Test{
public static void main(String[] args) {
ClassLoader classLoader = Test.class.getClassLoader();
//输出加载Test类的加载器类的名字:sun.misc.Launcher$AppClassLoader
System.out.println(classLoader.getClass().getName());
//输出加载Test类的加载器类的父加载器类的名字:sun.misc.Launcher$ExtClassLoader
System.out.println(classLoader.getParent().getClass().getName());
//输出加载Test类的加载器类的父加载器类的父加载器类:null
System.out.println(classLoader.getParent().getParent());
}
}
1
2
3
4
5
6
7
8
9
10
11
输出
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null
1
2
3
//由于最顶层的加载器引导类加载器是由C++写的,所以在JDK中输出为null;
说明:
a、sun.misc.Launcher源码:openjdk\jdk\src\share\classes\sun\misc目录
b、从上面的结果可以看出,并没有获取到ExtClassLoader的父ClassLoader,因为Bootstrap ClassLoader(启动类加载器)是用C语言实现的,getParent()找不到一个确定的返回父ClassLoader的方式,于是就返回null。
类加载器实例化过程:
通过java命令执行Java程序时,首先初始化JVM,产生Bootstrap ClassLoader(启动类加载器)——>Bootstrap ClassLoader自动加载Extended ClassLoader(扩展类加载器),并将其父ClassLoader设为Bootstrap Loader——>Bootstrap ClassLoader自动加载AppClass ClassLoader(系统类加载器),并将其父ClassLoader设为Extended ClassLoader。
java.exe源码:openjdk\jdk\src\share\bin目录
类加载器运行原理
一个类加载器接到加载某个类的任务时,不会自己去尝试加载这个类,而是先将加载任务委托给父类加载器去完成, 每一个层次的类加载器都是如此,因此加载任务最终传递到最顶层的启动类加载器,然后自上而下从最顶层类加载器开始,每级类加载器在其搜索范围内尝试完成加载任务,如果某级类加载器完成类加载任务,就成功返回,类加载结束,否则交给下级类加载器尝试完成加载任务,以此类推,当所有父类加载器都无法完成类加载任务时就会回退到最初的类装载器时,如果该加载器也不能完成类的装载,则抛出ClassNotFoundException异常,这种加载类的机制称为双亲委派机制。
ClassLoader抽象类中loadClass(String name, boolean resolve)方法实现了双亲委派机制,代码如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
//检查是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) { //如果没有加载,则调用父类加载器
long t0 = System.nanoTime();
try {
if (parent != null) {//父类加载器不为空
c = parent.loadClass(name, false);
} else {//父类加载器为空,则使用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果父类加载失败,则使用自己的findClass方法进行加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
双亲委派机制有效避免了类被重复加载!
自定义类加载器
无法改变JDK自带类加载器的搜索路径,因此如果在程序运行时从特定搜索路径加载类,就要自定义类加载器,其定义步骤如下:
1、自定义一个继承自ClassLoader抽象类的Java类;
2、重写ClassLoader抽象类中findClass方法;
代码如下:
public class Test {
public static void main(String[] args) {
String className="com.loader.vo.Student";//待加载的类全名
String classPath = "D:\\Program_Files\\workspace\\ClassLoa\\bin";//待加载的类的路径
OtherClassLoader classLoader = new OtherClassLoader(classPath);
try {
Class clazz = classLoader.findClass(className);//类加载
System.out.println(clazz.getName());//加载成功,输出类名
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class OtherClassLoader extends ClassLoader {
private String classPath;
public OtherClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String className = classPath+"/"+name.replace(".", "/")+".class";//拼接完整路径
InputStream inputStream;
try {
inputStream = new FileInputStream(className);//获取class文件创建输入流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();//创建字节数组输出流
byte [] car = new byte[1024];
int length =0;
while((length=inputStream.read(car))!=-1) {
outputStream.write(car, 0, length);
}
byte[] b = outputStream.toByteArray();//转换为字节码数组
outputStream.flush();
outputStream.close();
return defineClass(name, b, 0, b.length);// 将字节码转化为Class对象
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
最后输出如下
com.loader.vo.Student
1
Class.forName()与ClassLoader区别:
Java中Class.forName()和ClassLoader都可用来对类进行加载,但是它们之间也有不同的区别:
1、Class.forName()除了将类的.class文件加载到JVM中之外,还会对类进行解释,执行类中的static块,还会执行给静态变量赋值的静态方法;
2、ClassLoader仅仅是将.class文件加载到JVM中,只有在newInstance才会去执行类中的static块;
代码:
a、Student类
package com.jd.vo;
public class Student {
static {
System.out.println("静态代码块");
}
}
1
2
3
4
5
6
7
8
b、Test类
public class Test{
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Test.class.getClassLoader();
//classLoader.loadClass("com.jd.vo.Student");//使用ClassLoader.loadClass()来加载类,不会执行初始化块
//Class.forName("com.jd.vo.Student");//使用Class.forName()来加载类,默认会执行初始化块
Class.forName("com.jd.vo.Student", false, classLoader); //通过Class.forName(String name, boolean initialize, ClassLoader loader)加载类时,如果initialize参数为true,则执行静态代码块,否则不执行静态代码块,此时只有创建对象时才会执行
}
}
1
2
3
4
5
6
7
8
9
10
11
①通过运行java代码可以看到两个类加载器
②证明类加载器在特定位置加载
③双亲委派机制
④如何自定义类加载器
⑤可不可以自己写个String类
不能自己写以"java."开头的类,其要么不能加载进内存,要么即使你用自定义的类加载器去强行加载,也会收到一个SecurityException。
---------------------