Java 类加载机制详解
一、类加载器
类加载器(ClassLoader),顾名思义,即加载类的东西。在我们使用一个类之前,JVM需要先将该类的字节码文件(.class文件)从磁盘、网络或其他来源加载到内存中,并对字节码进行解析生成对应的Class对象,这就是类加载器的功能。我们可以利用类加载器,实现类的动态加载。
二、类的加载机制
在Java中,采用双亲委派机制来实现类的加载。那什么是双亲委派机制?在Java Doc中有这样一段描述:
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
从以上描述中,我们可以总结出如下四点:
1、类的加载过程采用委托模式实现
2、每个 ClassLoader 都有一个父加载器。
3、类加载器在加载类之前会先递归的去尝试使用父加载器加载。
4、虚拟机有一个内建的启动类加载器(Bootstrap ClassLoader),该加载器没有父加载器,但是可以作为其他加载器的父加载器。
Java 提供三种类型的系统类加载器。第一种是启动类加载器,由C++语言实现,属于JVM的一部分,其作用是加载 <Java_Runtime_Home>/lib 目录中的文件,并且该类加载器只加载特定名称的文件(如 rt.jar),而不是该目录下所有的文件。另外两种是 Java 语言自身实现的类加载器,包括扩展类加载器(ExtClassLoader)和应用类加载器(AppClassLoader),扩展类加载器负责加载<Java_Runtime_Home>\lib\ext目录中或系统变量 java.ext.dirs 所指定的目录中的文件。应用程序类加载器负责加载用户类路径中的文件。用户可以直接使用扩展类加载器或系统类加载器来加载自己的类,但是用户无法直接使用启动类加载器,除了这两种类加载器以外,用户也可以自定义类加载器,加载流程如下图所示:

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。
我们可以通过一段程序来验证这个过程:
public class Test {
}
public class TestMain {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader!=null){
System.out.println(loader);
loader = loader.getParent();
}
}
}
上面程序的运行结果如下所示:

从结果我们可以看出,默认情况下,用户自定义的类使用 AppClassLoader 加载,AppClassLoader 的父加载器为 ExtClassLoader,但是 ExtClassLoader 的父加载器却显示为空,这是什么原因呢?究其缘由,启动类加载器属于 JVM 的一部分,它不是由 Java 语言实现的,在 Java 中无法直接引用,所以才返回空。但如果是这样,该怎么实现 ExtClassLoader 与 启动类加载器之间双亲委派机制?我们可以参考一下源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
从源码可以看出,ExtClassLoader 和 AppClassLoader都继承自 ClassLoader 类,ClassLoader 类中通过 loadClass 方法来实现双亲委派机制。整个类的加载过程可分为如下三步:
1、查找对应的类是否已经加载。
2、若未加载,则判断当前类加载器的父加载器是否为空,不为空则委托给父类去加载,否则调用启动类加载器加载(findBootstrapClassOrNull 再往下会调用一个 native 方法)。
3、若第二步加载失败,则调用当前类加载器加载。
通过上面这段程序,可以很清楚的看出扩展类加载器与启动类加载器之间是如何实现委托模式的。
现在,我们再验证另一个问题。我们将刚才的Test类打成jar包,将其放置在 <Java_Runtime_Home>\lib\ext 目录下,然后再次运行上面的代码,结果如下:

现在,该类就不再通过 AppClassLoader 来加载,而是通过 ExtClassLoader 来加载了。如果我们试图把jar包拷贝到<Java_Runtime_Home>\lib,尝试通过启动类加载器加载该类时,我们会发现编译器无法识别该类,因为启动类加载器除了指定目录外,还必须是特定名称的文件才能加载。
三、自定义类加载器
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:
package com.paddx.test.classloading;
import java.io.*;
/**
* Created by liuxp on 16/3/12.
*/
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("/Users/liuxp/tmp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.paddx.test.classloading.Test");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
运行上面的程序,输出结果如下:

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:
1、这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
3、这类 Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
四、总结
双亲委派机制能很好地解决类加载的统一性问题。对一个 Class 对象来说,如果类加载器不同,即便是同一个字节码文件,生成的 Class 对象也是不等的。也就是说,类加载器相当于 Class 对象的一个命名空间。双亲委派机制则保证了基类都由相同的类加载器加载,这样就避免了同一个字节码文件被多次加载生成不同的 Class 对象的问题。但双亲委派机制仅仅是Java 规范所推荐的一种实现方式,它并不是强制性的要求。近年来,很多热部署的技术都已不遵循这一规则,如 OSGi 技术就采用了一种网状的结构,而非双亲委派机制。
Java 类加载机制详解的更多相关文章
- JAVA类加载机制详解
“代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是变成语言发展的一大步”,这句话出自<深入理解JAVA虚拟机>一书,后面关于jvm的系列文章主要都是参考这本书. JAV ...
- Java类加载器详解
title: Java类加载器详解date: 2015-10-20 18:16:52tags: JVM--- ## JVM三种类型的类加载器- 我们首先看一下JVM预定义的三种类型类加载器,当一个 J ...
- JVM类加载机制详解(二)类加载器与双亲委派模型
在上一篇JVM类加载机制详解(一)JVM类加载过程中说到,类加载机制的第一个阶段加载做的工作有: 1.通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件).而获取的方式,可 ...
- JVM类加载机制详解
引言 如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 加载 在加载阶段,虚拟机需要完成以下三件事情: 1)通过一个类的全限定名来获取定义此 ...
- Java 反射机制详解(下)
续:Java 反射机制详解(上) 三.怎么使用反射 想要使用反射机制,就必须要先获取到该类的字节码文件对象(.class),通过字节码文件对象,就能够通过该类中的方法获取到我们想要的所有信息(方法,属 ...
- Java 反射机制详解(上)
一.什么是反射 JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意方法和属性:这种动态获取信息以及动态调用对象方法的功能称为java ...
- java异常处理机制详解
java异常处理机制详解 程序很难做到完美,不免有各种各样的异常.比如程序本身有bug,比如程序打印时打印机没有纸了,比如内存不足.为了解决这些异常,我们需要知道异常发生的原因.对于一些常见的异常,我 ...
- Java SPI机制详解
Java SPI机制详解 1.什么是SPI? SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制.SPI是一种动态替换发现的机制, 比如有个 ...
- Java程序员的必备知识-类加载机制详解
类加载器的概念 类加载器是一个用来加载类文件的类. Java源代码通过javac编译器编译成类文件.然后JVM来执行类文件中的字节码来执行程序.类加载器负责加载文件系统.网络或其他来源的类文件. JV ...
随机推荐
- 拦截器getmodel方法什么时候被调用(没搞懂有什么鸟用,自己搭的项目中用到了这个)
拦截器是Struts2最强大的特性之一,它是一种可以让用户在Action执行之前和Result执行之后进行一些功能处理的机制.Struts2 的预定义拦截器 modelDriven 如果action实 ...
- 分享整理的sql脚本
1. 表空间使用率 SQL> select a.tablespace_name, 2 round(a.total_size) "total_size M" ...
- IE6解决固定定位代码
有些朋友在进行网页布局时,会遇到IE6浏览器不支持固定定位的兼容性问题,本博将详细介绍此问题的解决方法,需要了解的朋友可以参考下. ie6 垂直居中固定定位,代码如下: #center {_posit ...
- 115个Java面试题和答案——终极列表
from http://www.importnew.com/10980.html#collection http://www.importnew.com/11028.html 下面的章节分为上下两篇, ...
- golang bufio writer,reader 缓存规则
读,写,缓冲区可以杜绝频繁的读,写动作1.写缓存,如果一次write的长度大于buffer长度那么久发送当前缓冲区的内容并且发送要写入的内容,就是不在缓存了.如果发送的内容小于buffer长度,就按缓 ...
- SQL UNION 和 UNION ALL 操作符
SQL UNION 和 UNION ALL 操作符 SQL Full Join SQL Select Into SQL UNION 操作符 UNION 操作符用于合并两个或多个 SELECT 语句的结 ...
- Html 中select标签的边框与右侧倒三角的去除
首先是边框的去除:可以设置属性border:none;或border:0px; 不过这还是有一个bug,不同浏览器会在选中select标签时,加上一个边框: 之后是右侧倒三角的去除:设置属性 appe ...
- php之图片处理类缩略图加水印
用到两个image系统函数 imagecopymerge — 拷贝并合并图像的一部分 imagecopyresampled — 重采样拷贝部分图像并调整大小 /* 如何知道图片的大小和类型 无法确认调 ...
- shell脚本获取mysql插入数据自增长id的值
shell脚本获取mysql插入数据自增长id的值 在shell脚本中我们可以通过last_insert_id()获取id值,但是,需要注意的是,该函数必须在执行插入操作的sql语句之后,立即调用,否 ...
- 隐藏和显示 ng-show ng-hide
<div ng-controller='DeathraymenueController'> <button ng-click="toggleMenue()" ...