今天我们学习类加载器,关于类加载器其实和jvm有很大关系,在这里这篇文章只是简单的介绍下类加载器,后面学习到jvm的时候还会详细讲到类加载器,本文分为下面几个小节讲解:
一、认识类加载器
1.什么是类加载器?
所谓的类加载器可以从其作用来理解,其功能就是将classpath目录下.class文件,加载到内存中来进行一些处理,处理完的结果就是一些字节码.那是谁把这些class类加载到内存中来的呢?就是类加载器。
2.jvm中默认的类加载器有哪些?
java虚拟机中可以安装多个类加载器,系统默认三个主要的类加载器,每个类加载器负责加载不同位置的类:bootstrap,extclassloader,appclassloader
注意的是:
1.类加载器本身也是一个java类,因为类加载器本身也是一个java类,那么这个特殊的java类【类加载器】是有谁加载进来的呢?这显然要有第一个类加载器,这第一个类加载器不是一个java类,它是bootstrap。
2.bootstrap不是一个java类,不需要类加载器java加载,他是嵌套在java虚拟机内核里面的。java 虚拟机内核已启动的时候,他就已经在那里面了,他是用c++语言写的一段二进制代码。他可以去加载别的类,其中别的类就包含了类加载器【如上面提到的ext 和 app】。
案例:
下面我们写个例子来获取classloadertest这个类的类加载器的名字,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package study.javaenhance;
import java.util.arraylist;
public class classloadertest
{
public static void main(string[] args) throws exception
{
//获取类加载器,那么这个获取的是一个实例对象,我们知道类加载器也有很多种,那么因此也有其对应的类存在,因此可以获取到对应的字节码
system.out.println(classloadertest. class .getclassloader());
//获取类加载的字节码,然后获取到类加载字节码的名字
system.out.println(classloadertest. class .getclassloader().getclass().getname());
//下面我们看下获取非我们定义的类,比如system arraylist 等常用类
system.out.println(system. class .getclassloader());
system.out.println(arraylist. class .getclassloader());
}
}
|
结果如下:
sun.misc.launcher$appclassloader@1c78e57
sun.misc.launcher$appclassloader
null
null
结果分析:
classloadertest的类加载器的名称是appclassloader。也就是这个类是由appclassloader这个类加载器加载的。
system/arraylist的类加载器是null。这说明这个类加载器是由bootstrap加载的。因为我们上面说了bootstrap不是java类,不需要类加载器加载。所以他的类加载器是null。
==================================
我们说了java给我们提供了三种类加载器:bootstrap,extclassloader,appclassloader。这三种类加载器是有父子关系组成了一个树形结构。bootstrap是根节点,bootstrap下面挂着extclassloader,extclassloader下面挂着appclassloader.
代码演示如下:
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
|
package study.javaenhance;
import java.util.arraylist;
public class classloadertest
{
public static void main(string[] args) throws exception
{
//获取类加载器,那么这个获取的是一个实例对象,我们知道类加载器也有很多种,那么因此也有其对应的类存在,因此可以获取到对应的字节码
system.out.println(classloadertest. class .getclassloader());
//获取类加载的字节码,然后获取到类加载字节码的名字
system.out.println(classloadertest. class .getclassloader().getclass().getname());
//下面我们看下获取非我们定义的类,比如system arraylist 等常用类
system.out.println(system. class .getclassloader());
system.out.println(arraylist. class .getclassloader());
//演示java 提供的类加载器关系
classloader classloader = classloadertest. class .getclassloader();
while (classloader != null )
{
system.out.print(classloader.getclass().getname()+ "-->" );
classloader = classloader.getparent();
}
system.out.println(classloader);
}
}
|
输出结果为:
sun.misc.launcher$appclassloader-->sun.misc.launcher$extclassloader-->null
通过这段程序可以看出来,classloadertest由appclassloader加载,appclassloader的父类节点是extclassloader,extclassloader的父节点是bootstrap。
每一个类加载器都有自己的管辖范围。 bootstrap根节点,只负责加载rt.jar里的类,刚刚那个system就是属于rt.jar包里面的,extclassloader负责加载jre/lib/ext/*.jar这个目录文件夹下的文件。而appclassloader负责加载classpath目录下的所有jar文件及目录。
最后一级是我们自定义的加载器,他们的父类都是appclassloader。
二、类加载器的双亲委派机制
除了系统自带了类加载器,我们还可以自定义类加载器。然后把自己的类加载器挂在树上。作为某个类加载器的孩子。所有自定义类加载器都要继承classloader。实现里面的一个方法classloader()如下:
通过上面的知识,我们知道java提供了三个类加载器,而且我们也可以自定义类加载器,并且通过上面的类加载图也看到了之前的关系,那么对于一个类的.class 到底是谁去加载呢?
当java虚拟机要加载第一个类的时候,到底派出哪个类加载器去加载呢?
(1). 首先当前线程的类加载器去加载线程中的第一个类(当前线程的类加载器:thread类中有一个get/setcontextclassloader(classloader cl);方法,可以获取/指定本线程中的类加载器)
(2). 如果类a中引用了类b,java虚拟机将使用加载类a的类加载器来加载类b
(3). 还可以直接调用classloader.loadclass(string classname)方法来指定某个类加载器去加载某个类
每个类加载器加载类时,又先委托给其上级类加载器当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则会抛出classnotfoundexception,不是再去找发起者类加载器的儿子,因为没有getchild()方法。例如:如上图所示: myclassloader->appclassloader->ext->classloader->bootstrap.自定定义的myclassloader1首先会先委托给appclassloader,appclassloader会委托给extclassloader,extclassloader会委托给bootstrap,这时候bootstrap就去加载,如果加载成功,就结束了。如果加载失败,就交给extclassloader去加载,如果extclassloader加载成功了,就结束了,如果加载失败就交给appclassloader加载,如果加载成功,就结束了,如果加载失败,就交给自定义的myclassloader1类加载器加载,如果加载失败,就报classnotfoundexception异常,结束。
这样的好处在哪里呢?可以集中管理,不会出现多份字节码重复的现象。有两个类要再在system,如果让底层的类加载器加载,可能会出现两份字节码。而都让爷爷加载,爷爷加载到已有,当再有请求过来的时候,爷爷说:哎,我加载过啊,直接把那份拿出来给你用啊。就不会出现多份字节码重复的现象。
现在有一道面试题:能不能自己写一套java.lang.system.?
分析:你写了也白写,因为类加载器加载,直接到爷爷那里去找,找成功了,分本就不回来理你的那个。
答案:通常不可以,因为委托机制委托给爷爷,爷爷在rt.jar包加载到这个类以后就不会加载你自己写了那个system类了。但是,我也有办法加载,我写一个自己的类加载器,不让他用委托机制,不委托给上级了,就可以了.
因为system类,list,map等这样的系统提供jar类都在rt.jar中,所以由bootstrap类加载器加载,因为bootstrap是祖先类,不是java编写的,所以打印出class为null
对于classloadertest类的加载过程,打印结果也是很清楚的。
三、自定义类加载器
下面来看一下怎么定义我们自己的一个类加载器myclassloader:
自定义的类加载器必须继承抽象类classloader然后重写findclass方法,其实他内部还有一个loadclass方法和defineclass方法,这两个方法的作用是:
loadclass方法的源代码:
1
2
3
|
public class <?> loadclass(string name) throws classnotfoundexception {
return loadclass(name, false );
}
|
再来看一下loadclass(name,false)方法的源代码:
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
|
protected class <?> loadclass(string name, boolean resolve) throws classnotfoundexception{
//加上锁,同步处理,因为可能是多线程在加载类
synchronized (getclassloadinglock(name)) {
//检查,是否该类已经加载过了,如果加载过了,就不加载了
class c = findloadedclass(name);
if (c == null ) {
long t0 = system.nanotime();
try {
//如果自定义的类加载器的parent不为null,就调用parent的loadclass进行加载类
if (parent != null ) {
c = parent.loadclass(name, false );
} else {
//如果自定义的类加载器的parent为null,就调用findbootstrapclass方法查找类,就是bootstrap类加载器
c = findbootstrapclassornull(name);
}
} catch (classnotfoundexception e) {
// classnotfoundexception thrown if class not found
// from the non-null parent class loader
}
if (c == null ) {
// if still not found, then invoke findclass in order
// to find the class.
long t1 = system.nanotime();
//如果parent加载类失败,就调用自己的findclass方法进行类加载
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;
}
}
|
在loadclass代码中也可以看到类加载机制的原理,这里还有这个方法findbootstrapclassornull,看一下源代码:
1
2
3
4
5
6
|
private class findbootstrapclassornull(string name)
{
if (!checkname(name)) return null ;
return findbootstrapclass(name);
}
|
就是检查一下name是否是否正确,然后调用findbootstrapclass方法,但是findbootstrapclass方法是个native本地方法,看不到源代码了,但是可以猜测是用bootstrap类加载器进行加载类的,这个方法我们也不能重写,因为如果重写了这个方法的话,就会破坏这种委托机制,我们还要自己写一个委托机制。
defineclass这个方法很简单就是将class文件的字节数组编程一个class对象,这个方法肯定不能重写,内部实现是在c/c++代码中实现的findclass这个方法就是根据name来查找到class文件,在loadclass方法中用到,所以我们只能重写这个方法了,只要在这个方法中找到class文件,再将它用defineclass方法返回一个class对象即可。
这三个方法的执行流程是:每个类加载器:loadclass->findclass->defineclass
前期的知识了解后现在就来实现了
首先来看一下需要加载的一个类:classloaderattachment.java:
1
2
3
4
5
6
7
8
9
10
|
package study.javaenhance;
public class classloaderattachment {
@override
public string tostring() {
return "hello classloader!" ;
}
}
|
这个类中输出一段话即可:编译成classloaderattachment.class
再来看一下自定义的myclassloader.java:
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
package study.javaenhance;
import java.io.bytearrayoutputstream;
import java.io.fileinputstream;
import java.io.fileoutputstream;
import java.io.inputstream;
import java.io.outputstream;
public class myclassloader extends classloader
{
//需要加载类.class文件的目录
private string classdir;
//无参的构造方法,用于class.newinstance()构造对象使用
public myclassloader(){
}
public myclassloader(string classdir){
this .classdir = classdir;
}
@override
protected class <?> findclass(string name) throws classnotfoundexception {
system.out.println(name);
string classpathfile = classdir + "\\" + name.substring(name.lastindexof( "." )+ 1 ) + ".class" ;
system.out.println(classpathfile);
try
{
system.out.println( "my" );
//将class文件进行解密
fileinputstream fis = new fileinputstream(classpathfile);
bytearrayoutputstream bos = new bytearrayoutputstream();
encodeanddecode(fis,bos);
byte [] classbyte = bos.tobytearray();
//将字节流变成一个class
return defineclass(classbyte, 0 ,classbyte.length);
} catch (exception e)
{
e.printstacktrace();
}
return super .findclass(name);
}
//测试,先将classloaderattachment.class文件加密写到工程的class_temp目录下
public static void main(string[] args) throws exception{
//配置运行参数
string srcpath = args[ 0 ]; //classloaderattachment.class原路径
string despath = args[ 1 ]; //classloaderattachment.class输出的路径
string desfilename = srcpath.substring(srcpath.lastindexof( "\\" )+ 1 );
string despathfile = despath + "/" + desfilename;
fileinputstream fis = new fileinputstream(srcpath);
fileoutputstream fos = new fileoutputstream(despathfile);
//将class进行加密
encodeanddecode(fis,fos);
fis.close();
fos.close();
}
/**
* 加密和解密算法
* @param is
* @param os
* @throws exception
*/
private static void encodeanddecode(inputstream is,outputstream os) throws exception{
int bytes = - 1 ;
while ((bytes = is.read())!= - 1 ){
bytes = bytes ^ 0xff ; //和0xff进行异或处理
os.write(bytes);
}
}
}
|
这个类中定义了一个加密和解密的算法,很简单的,就是将字节和oxff异或一下即可,而且这个算法是加密和解密的都可以用!
当然我们还要先做一个操作就是,将classloaderattachment.class加密后的文件存起来,也就是在main方法中执行的,这里我是在项目中新建一个
同时采用的是参数的形式来进行赋值的,所以在运行的myclassloader的时候要进行输入参数的配置:右击myclassloader->run as -> run configurations
第一个参数是classloaderattachment.class文件的源路径,第二个参数是加密后存放的目录,运行myclassloader之后,刷新class_temp文件夹,出现了classloaderattachment.class,这个是加密后的class文件。
下面来看一下测试类:
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
|
package study.javaenhance;
import java.util.arraylist;
public class classloadertest
{
public static void main(string[] args) throws exception
{
//获取类加载器,那么这个获取的是一个实例对象,我们知道类加载器也有很多种,那么因此也有其对应的类存在,因此可以获取到对应的字节码
system.out.println(classloadertest. class .getclassloader());
//获取类加载的字节码,然后获取到类加载字节码的名字
system.out.println(classloadertest. class .getclassloader().getclass().getname());
//下面我们看下获取非我们定义的类,比如system arraylist 等常用类
system.out.println(system. class .getclassloader());
system.out.println(arraylist. class .getclassloader());
//演示java 提供的类加载器关系
classloader classloader = classloadertest. class .getclassloader();
while (classloader != null )
{
system.out.print(classloader.getclass().getname()+ "-->" );
classloader = classloader.getparent();
}
system.out.println(classloader);
try {
//class classdate = new myclassloader("class_temp").loadclass("classloaderattachment");
class classdate = new myclassloader( "class_temp" ).loadclass( "study.javaenhance.classloaderattachment" );
object object = classdate.newinstance();
//输出classloaderattachment类的加载器名称
system.out.println( "classloader:" +object.getclass().getclassloader().getclass().getname());
system.out.println(object);
} catch (exception e1) {
e1.printstacktrace();
}
}
}
|
结果如下:
sun.misc.launcher$appclassloader@6b97fd
sun.misc.launcher$appclassloader
null
null
sun.misc.launcher$appclassloader-->sun.misc.launcher$extclassloader-->null
classloader:sun.misc.launcher$appclassloader
hello classloader!
这个时候我们会发现调用的app 的类加载器然后输出了结果,这个是正常的,因为这个时候会采用双亲委派机制。
那么这个时候,我们将自己生成的classloaderattachemet class文件,覆盖掉编译的时候生成的class 文件看下结果如何,如果正常应该会报错,因为这个时候走双亲委派机制在对应的classpath 是可以找到这个class 文件,因此app类加载器会处理,但是因为我们的class 是加密的因此会报错,运行结果如:
那么如何让其走到我们自定义的类加载器呢,只需要将编译时候生成的目录下的.class 文件删掉即可,那么这个是app加载不到,则会去调用findclass ,然后就会走到我们定义的类加载器中,运行结果如下:
参考资料:
张孝祥老师java增强视频
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:http://www.cnblogs.com/pony1223/p/7711092.html