今天我们学习类加载器,关于类加载器其实和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这个类的类加载器的名字,代码如下:

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.
代码演示如下:
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方法的源代码:

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

再来看一下loadClass(name,false)方法的源代码:

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,看一下源代码:

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:

package study.javaenhance;

public class ClassLoaderAttachment {
@Override
public String toString() {
return "Hello ClassLoader!"; } }

这个类中输出一段话即可:编译成ClassLoaderAttachment.class

再来看一下自定义的MyClassLoader.java:

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文件。

下面来看一下测试类:

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增强视频

JAVA提高七:类加载器的更多相关文章

  1. 黑马程序员——【Java高新技术】——类加载器

    ---------- android培训.java培训.期待与您交流! ---------- 一.概述 (一)类加载器(class loader) 用来动态加载Java类的工具,它本身也是Java类. ...

  2. Java中的类加载器

    转载:http://blog.csdn.net/zhangjg_blog/article/details/16102131 从java的动态性到类加载机制   我们知道,Java是一种动态语言.那么怎 ...

  3. Java中的类加载器以及Tomcat的类加载机制

    在加载阶段,虚拟机需要完成以下三件事情: 1.通过一个类的全限定名来获取其定义的二进制字节流. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构. 3.在Java堆中生成一个代表这个类 ...

  4. 黑马程序员:Java基础总结----类加载器

    黑马程序员:Java基础总结 类加载器   ASP.Net+Android+IO开发 . .Net培训 .期待与您交流! 类加载器 Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个 ...

  5. Java中的类加载器--Class loader

    学习一下Java中的类加载器,这个是比较底层的东西,好好学习.理解一下.  一.类加载器的介绍 1.类加载器:就是加载类的工具,在java程序中用到一个类,java虚拟机首先要把这个类的字节码加载到内 ...

  6. Java基础加强-类加载器

    /*类加载器*/ 把.class文件从硬盘上加载出来,将类的字节码(二进制)加载到内存中 /*类加载器及其委托机制*/ Java虚拟机中可以安装多个类加载器(可以自己编写),系统默认三个主要类加载器, ...

  7. JAVA基础_类加载器

    什么是类加载器 类加载器是Java语言在1.0版本就引入的.最初是为了满足JavaApplet需要.现在类加载器在Web容器和OSGI中得到了广泛的应用,一般来说,Java应用的开发人员不需要直接同类 ...

  8. Java基础之类加载器

    Java类加载器是用户程序和JVM虚拟机之间的桥梁,在Java程序中起了至关重要的作用,理解它有利于我们写出更优雅的程序.本文首先介绍了Java虚拟机加载程序的过程,简述了Java类加载器的加载方式( ...

  9. Java JVM——2.类加载器子系统

    概述 类加载器子系统在Java JVM中的位置 类加载器子系统的具体实现 类加载器子系统的作用 ① 负责从文件系统或者网络中加载.class文件,Class 文件在文件开头有特定的文件标识. ② Cl ...

随机推荐

  1. 201521123010 《Java程序设计》第4周学习总结

    1.本周学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. 这周上课主要学了继承,在打代码的时候对各个关键字(除了super关键字)的用法有一点混乱.对多 ...

  2. 201521145048《Java程序设计》第4周学习总结

    1. 本章学习总结 学会了如何去设计一个类,尽量用private修饰属性,public修饰方法. 了解继承的目的. 了解继承和多态的关系. 了解关键字extends super final overr ...

  3. JAVA课设 学生基本信息管理 团队博客

    1.成员 邹其元 网络1512 201521123060 杨钧宇 网络1512 201521123062 2.项目Git地址 团队项目码云地址 //添加截图 3. 项目git提交记录截图(要体现出每个 ...

  4. 201521123116 《java程序设计》第十二周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 2. 书面作业 Q1.字符流与文本文件:使用 PrintWriter(写),BufferedReader(读) ...

  5. java课程设计——算术运算测试个人博客

    1.团队课程设计链接 团队博客:http://www.cnblogs.com/yytx/p/7064790.html 2.个人负责模块 有关排行榜的界面设计和代码编写 3.自己的代码提交记录截图 4. ...

  6. 201521123009 《Java程序设计》第1周学习总结

    1. 本周学习总结 对Java进行了了解与简单的学习.第一次接触Java觉得比较难理解. 希望之后的深入学习可以解决目前的一些问题. 2. 书面作业 为什么java程序可以跨平台运行?执行java程序 ...

  7. logback:用slf4j+logback实现多功能日志解决方案

    slf4j是原来log4j的作者写的一个新的日志组件,意思是简单日志门面接口,可以跟其他日志组件配合使用,常用的配合是slf4j+logback,无论从功能上还是从性能上都较之log4j有了很大的提升 ...

  8. eclipse: eclipse导入工程出现大红叹号

    总结: 问题原因:工程中classpath中指向的包路径错误 解决办法:到BUILDPATH CONFIG````中,liberaies中 出现红色叉号的包为路径错误的包.到classpath中修改相 ...

  9. Lucene第二篇【抽取工具类、索引库优化、分词器、高亮、摘要、排序、多条件搜索】

    对Lucene代码优化 我们再次看回我们上一篇快速入门写过的代码,我来截取一些有代表性的: 以下代码在把数据填充到索引库,和从索引库查询数据的时候,都出现了.是重复代码! Directory dire ...

  10. Oracle_Sequence如何初始化开始值

    Sequence的start with 值如何确定才能保证生成的主键不会冲突??? 我的项目中最开始数据库表主键的生成策略是 increment,但由于后来采用了集群部署的方式,出现了主键冲突的问题. ...