一 类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载入JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
唯一标识符:
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;
但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),如下:
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
1 启动类加载器:
(BootstrapClassLoader),启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。总结一句话:启动类加载器加载java运行过程中的核心类库JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List…;
2 扩展类加载器:
ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器;
3 应用程序类加载器:
ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。总结一句话:应用程序类加载器加载CLASSPATH变量指定路径下的类 即指你自已在项目工程中编写的类。
4 自定义类加载器
Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,因为JVM自带的类加载器(ClassLoader)只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1、在执行非置信代码之前,自动验证数字签名;
2、动态地创建符合用户特定需要的定制化构建类;
3、从特定的场所取得java class,例如数据库中和网络中。
需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机默认采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面将会详细讲到!
1 ) 双亲委派原则
他的工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。这个理解起来就简单了,比如说,另外一个人给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就一分钱没有了。(例子不好,理解就好)
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:
可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
2)双亲委派机制为什么安全?
举个例子,ClassLoader加载的class文件来源很多,比如编译器编译生成的class、或者网络下载的字节码。而一些来源的class文件是不可靠的,比如我可以自定义一个java.lang.Integer类来覆盖jdk中默认的Integer类,例如下面这样:
package java.lang;
public class Integer {
public Integer(int value) {
System.exit(0);
}
}
View Code
初始化这个Integer的构造器是会退出JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该Integer类永远不会被调用,因为委托BootStrapClassLoader加载后会加载JDK中的Integer类而不会加载自定义的这个,可以看下下面这测试个用例:
public static void main(String...args){
Integer i=new Integer(1);
System.err.println(i);
}
View Code
执行时JVM并未在new Integer(1)时退出,说明未使用自定义的Integer,于是就保证了安全性。
3)自定义类加载器
在这一部分第一小节中,我们提到了java系统为我们提供的三种类加载器,还给出了他们的层次关系图,最下面就是自定义类加载器,那么我们如何自己定义类加载器呢?这主要有两种方式
(1)遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
我们看一下实现步骤
(1)创建一个类继承ClassLoader抽象类
(2)重写findClass()方法
(3)在findClass()方法中调用defineClass()
3.1 自定义文件类加载器
package classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
// 文件系统类加载器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
// 获取类的字节码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name); // 获取类的字节数组
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
// 得到类文件的完全路径
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
View Code
如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。
java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。
因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
测试类
package com.example;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
System.out.println(instance.toString());
this.instance = (Sample) instance;
}
}
View Code
package classloader;
import java.lang.reflect.Method;
public class ClassIdentity {
public static void main(String[] args) {
new ClassIdentity().testClassIdentity();
}
public void testClassIdentity() {
String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className); // 加载Sample类
Object obj1 = class1.newInstance(); // 创建对象
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
View Code
3.2 自定义网络类加载器
下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。
package classloader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
// 指定URL
this.rootUrl = rootUrl;
}
// 获取类的字节码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
// 从网络上读取的类的字节
String path = classNameToPath(className);
try {
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
// 得到类文件的URL
return rootUrl + "/"
+ className.replace('.', '/') + ".class";
}
}
View Code
在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:
客户端接口:
package classloader;
public interface Versioned {
String getVersion();
}
View Code
package classloader;
public interface ICalculator extends Versioned {
String calculate(String expression);
}
View Code
网络上不同版本的类
package com.example;
import classloader.ICalculator;
public class CalculatorBasic implements ICalculator {
@Override
public String calculate(String expression) {
return expression;
}
@Override
public String getVersion() {
return "1.0";
}
}
View Code
package com.example;
import classloader.ICalculator;
public class CalculatorAdvanced implements ICalculator {
@Override
public String calculate(String expression) {
return "Result is " + expression;
}
@Override
public String getVersion() {
return "2.0";
}
}
View Code
在客户端加载网络上的类的过程:
package classloader;
public class CalculatorTest {
public static void main(String[] args) {
String url = "http://localhost:8080/ClassloaderTest/classes";
NetworkClassLoader ncl = new NetworkClassLoader(url);
String basicClassName = "com.example.CalculatorBasic";
String advancedClassName = "com.example.CalculatorAdvanced";
try {
Class<?> clazz = ncl.loadClass(basicClassName); // 加载一个版本的类
ICalculator calculator = (ICalculator) clazz.newInstance(); // 创建对象
System.out.println(calculator.getVersion());
clazz = ncl.loadClass(advancedClassName); // 加载另一个版本的类
calculator = (ICalculator) clazz.newInstance();
System.out.println(calculator.getVersion());
} catch (Exception e) {
e.printStackTrace();
}
}
}
View Code
4 破坏双亲委派原则
4.1、为什么需要破坏双亲委派?
因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,
以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector
,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME\lib下文件,而其接口实现是由服务商提供的,由系统类加载器加载,根据类加载机制,若A类(如上文Driver接口)调用B类(如上文Driver实现类),则B类由A类的加载器加载。显然这里的A的类加载器(启动类加载器)无法加载B类(数据库服务商的具体实现类),这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。
那么启动类加载器,怎么委托子类来加载Drive实现呢?
通过Thread.currentThread().getContextClassLoader()
得到线程上下文加载器来加载Driver实现类。
4.2 线程上下文类加载器
名词解释
SPI:Service Provider Interface 服务提供者接口
当前类加载器(Current ClassLoader)
每个类都会使用自己的类加载器(加载自身的类加载器)来去加载所依赖的类,如果ClassA引用了ClassB,那么ClassA的类加载器就会加载ClassB(前提是ClassB没有被加载)
线程上下文类加载器(Context ClassLoader)
线程上下文类加载器是从jdk1.2开始引入的,类Thread中的getContextCLassLoader()与setContextClassLoader(ClassLoader classloader) 分别用来获取和设置上下文类加载器.如果没有通过用setContextClassLoader(ClassLoader classloader)进行设置的话,线程将继承其父线程的上下文类加载器。 Java应用运行时的初始线程的上下文加载器是系统类加载器(也就是应用类加载器),在线程中运行的代码可以通过该类加载器来加载类与资源
这句话的解释就在launcher的源代码中
看一下Launcher类的源码:
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 设置this.loader为系统类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 设置线程上下文类加载器是 this.loader(系统类加载器).
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
;
} catch (InstantiationException var6) {
;
} catch (ClassNotFoundException var7) {
;
} catch (ClassCastException var8) {
;
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
public ClassLoader getClassLoader() {
return this.loader;
}
View Code
JVM双亲委托加载模型的问题
Java提供了很多的服务提供者接口(SPI),提供这些接口让第三方去实现这些接口,常见的SPI有: jdbc、JNDI、JAXP(解析XML)等。这些SPI接口是由Java核心类库提供的,而这些SPI的实现代码则是作为Java应用依赖的jar包被引入类的classpath下的,SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。根据双亲委派类加载模型,父加载器(启动类加载器)加载的类无法访问子加载器(系统类加载器或者应用类加载器) 加载的类,所以就无法访问第三方的实现类,这是双亲委托模型的一个尴尬局面。
线程上下文类加载器的重要性
父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classloader加载的类。
这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的CLassLoader加载的类的情况,即改变了双亲委托模型。
部分理论参考链接:
二 解析和分配
1,解析阶段
我们知道,类的加载过程包含七个阶段:加载,验证,准备,解析,初始化,使用,卸载,七个阶段顺序开始,交叉进行。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,是这七个阶段之一。
调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。
在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。
2 静态分派
多见于方法的重载 --- 编译时多态
“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
3 动态分派
多见于方法的覆盖---运行时多态
静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同。
当子类未重写父类方法,如果用子类调用某个父类方法,按我们的理解是不是如果在子类找不到方法,就向上找父类的,依次类推
道理很简单,但是动态分派是个非常频繁的动作,如果每次都这么向上查找的话,会严重影响虚拟机的执行性能。所以虚拟机针对这种情况会做出一些优化手段,最常用的”稳定优化“手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable),使用虚方法表的索引来代替元数据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
下图中Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。