----
每个ClassLoader实例之间是隔离的
自定义的ClassLoader可以使用父ClassLoader加载的类
如果由父ClassLoader加载的类,需要访问子ClassLoader加载的类,可以将子ClassLoader传递到父ClassLoader中。
这里的父子类,并不是java中的继承关系。只是有一个变量指向其他对象。就像链表中每个节点可以指向上个节点
{{:pasted:20180331-155218.png}}
===== 类加载机制 =====
- 寻找jre目录,寻找jvm.dll,并初始化JVM;
- 产生一个Bootstrap Loader(启动类加载器);
- Bootstrap Loader的搜索路径为:(System.out.println(System.getProperty("sun.boot.class.path"));)
- Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。
- Bootstrap Loader的搜索路径为:System.out.println(System.getProperty("java.ext.dirs"));
- Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
- AppClass Loader的搜索路径为:System.out.println(System.getProperty("java.class.path"));
- 最后由AppClass Loader加载我们自定义的类
===== 类生命周期 =====
{{:pasted:20160320-123612.png}}
- 装载:通过一个类的全限定名来获取此类的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口
- 连接
- 验证:检查类格式等,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:给类变量分配内存,并根据类变量类型设置默认值(即内存中置0),准备阶段是正式为类变量分配内存并设置类变量初始值(各数据类型的零值)的阶段,这些内存将在方法区中进行分配。但是如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会初始化为ConstantValue属性指定的值。
- 解析:解析阶段是在虚拟机将常量池内的符号引用替换为直接引用的过程。(class字节码文件中存的是可读字符,使用前必须转换成地址引用)
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
- 初始化
- 这里所谓的“初始化”是指类的初始化,即执行了className字节码的方法()
- ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
===== 类加载方法 =====
==== 关于forName()方法 ====
这个方法总是返回要加载的类的Class类的实例
- forName(String className)单参数时, initialize=true
- 总是使用当前类装载器(也就是装载执行forName()请求的类 的类装载器)
- 总是初始化这个被装载的类(装载、连接、初始化)
- forName(String className, boolean initialize, ClassLoader loader)
- loader指定装载参数类所用的类装载器,如果null则用bootstrp装载器。
- initialize=true时,肯定连接,而且初始化了;(装载、连接、初始化)
- initialize=false时,(装载、[连接])绝对不会初始化,但是可能被连接了,但是这里有个例外,如果在调用这个forName()前,已经被初始化了(当然,这里也暗含着:className是被同一个loader所装载的,即被参数中的loader所装载的,而且这个类被初始化了),那么返回的类型也肯定是被初始化的
==== 关于用户自定义的类装载器的loadClass()方法 ====
- loadClass(String name)单参数时, resolve=false (装载)
- 如果这个类已经被这个类装载器所装载,那么,返回这个已经被装载的类型的Class的实例,否则,就用这个自定义的类装载器来装载这个class,这时不知道是否被连接。绝对不会被初始化
- 这时唯一可以保证的是,这个类被装载了。但是不知道这个类是不是被连接和初始化了
- loadClass(String name, boolean resolve)(装载、连接)
- resolve=true时,则保证已经装载,而且已经连接了。resolve=falses时,则仅仅是去装载这个类,不关心是否连接了,所以此时可能被连接了,也可能没有被连接
===== 自定义装载器 =====
* loadClass:加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重新实现,我们可以完全控制和管理类的加载过程。
* defineClass:该方法是 ClassLoader 中非常重要的一个方法,它接收以字节数组表示的类字节码,并把它转换成 Class 实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。
* getSystemClassLoader:Java2 中新增的方法。该方法返回系统使用的 ClassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理
* findSystemClass:调用系统装载器装载类。底层是调用getSystemClassLoader
* findLoadedClass:缓存已经装载过的类。每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,该方法就是在该名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回 null。这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。
===== Demo =====
=== 主程序 ====
import java.io.*;
import java.lang.reflect.*;
class AClassLoader extends ClassLoader {
public String loadPath = "/Users/fang/untitled/"; // 定义类存放的路径
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class klass = null;
try {
klass = findLoadedClass(name); //充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader
// 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法
if (klass != null) {
System.out.println("already load: "+name);
return klass;
}
if (klass == null) { //如果读取字节失败,则试图从JDK的系统API中寻找该类。
klass = findSystemClass(name);// 调用系统默认的ClassLoader(AppClassLoader)装载类,
System.out.println("load with SystemClassLoader: " + name);
// 如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。
// 当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。
}
if (resolve && klass != null) {
resolveClass(klass); // 连接class
}
} catch (ClassNotFoundException e){// 如果系统无法找到该类,则用自己的方式装载
byte[] bs = getClassBytes(name);//从一个特定的信息源寻找并读取该类的字节。
if (bs != null && bs.length > 0) {
klass = defineClass(name, bs, 0, bs.length); // 该方法接受由原始字节组成的数组并把它转换成 Class 对象
System.out.println("load with CustomLoader: " + name);
}
} catch (Exception e) {
throw new ClassNotFoundException(e.toString());
}
return klass;
}
/**
* 定义获取类的方法,可以从网络,文件系统等地方获取
*
* @param className
* @return
*/
private byte[] getClassBytes(String className) {
FileInputStream fis = null;
try {
fis = new FileInputStream(loadPath+className+".class");
} catch (FileNotFoundException e) {
// System.out.println(e);
System.out.println("name:"+className);
return null; //如果查找失败,则放弃查找。捕捉这个异常主要是为了过滤JDK的系统API。
}
try {
byte[] bs = new byte[fis.available()];
fis.read(bs);
return bs;
}catch (Exception e){
throw new RuntimeException("");
}
}
}
public class b {
public static void main(String[] args) throws Exception{
AClassLoader loader = new AClassLoader();
Class c = loader.loadClass("A", false); // 类加载
Object o = c.newInstance();
Method m = c.getMethod("print", java.lang.String.class);
m.invoke(o, "fang");
}
}
=== 要加载的类(A.java) ====
public class A{
public void print(String str) {
System.out.println("嗨," + str + ", 你终于找到我了!");
}
public String toString() {
return "我是一个A对象!";
}
}
===== 热加载 =====
程序说明
* 有接口I
* 自定义类加载器AClassLoader
* 主程序B
* 现在新建一个类A继承接口I,将A编译成一个class文件,并放在AClassLoader的搜索目录下
* 启动程序B,验证是否能正确加载类A,并转换成I类型
AClassLoader加载类A后,需要递归加载接口I。要使程序能正常工作必须保证,AClassLoader加载接口I是必须使用系统加载器,如果用AClassLoader加载接口I,那在程序中将无法转换成接口I类型。因为程序中的接口I是由系统加载器加载的,而类A的接口I是由AClassLoader加载的
=== 主程序 ===
import java.io.*;
class AClassLoader extends ClassLoader {
public String loadPath = "/Users/fang/untitled/"; // 定义类存放的路径
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class klass;
klass = findLoadedClass(name); //充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader
// 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法
if (klass != null) {
System.out.println("already load: "+name);
return klass;
}
// 根据需要确定是先调用系统类装载器,还是自定义装载器
try{
klass = getClassWithSystemClassLoader(name);
}catch (Exception e){
klass = getClassSelf(name);
}
if (resolve && klass != null)
resolveClass(klass); // 连接class
return klass;
}
/**
* 调用系统装载器
*/
private Class getClassWithSystemClassLoader(String name){
try {
//如果读取字节失败,则试图从JDK的系统API中寻找该类。
Class c = findSystemClass(name);// 调用系统默认的ClassLoader(AppClassLoader)装载类,
System.out.println("load with SystemClassLoader: " + name);
// 如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。
// 当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。
return c;
} catch (ClassNotFoundException e){ // 如果系统无法找到该类,则用自己的方式装载
throw new RuntimeException(e);
}
}
/**
* 使用自己的装载器
*/
private Class getClassSelf(String name){
//从一个特定的信息源寻找并读取该类的字节。
try {
byte[] bs = getClassBytes(name);
System.out.println("load with CustomLoader: " + name);
return defineClass(name, bs, 0, bs.length); // 该方法接受由原始字节组成的数组并把它转换成 Class 对象
}catch (ClassFormatError e){
throw new RuntimeException(e);
}
}
/**
* 定义获取类的方法,可以从网络,文件系统等地方获取
*/
private byte[] getClassBytes(String className) {
FileInputStream fis = null;
try {
fis = new FileInputStream(loadPath+className+".class");
byte[] bs = new byte[fis.available()];
fis.read(bs);
return bs;
}catch (Exception e){
try {
if(fis != null)
fis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
throw new RuntimeException(e);
}
}
}
public class B {
public static void main(String[] args) throws Exception{
AClassLoader loader = new AClassLoader();
Class c = loader.loadClass("A", false); // 类加载
I o = (I)c.newInstance();
o.print("123");
}
}
=== 接口I ===
public interface I {
public void print(String s);
}
=== 被加载类A ===
public class A implements I{
public void print(String str) {
System.out.println("hello " + str);
}
}
==== 热替换 ====
热替换很难做,因为同一个classloader不能加载2个同名的类。也就是说热替换的前提是旧的class对象被回收,才能加载新的类对象
类回收(unload)条件 (总之卸载类完全无法控制)
* 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
* 普通列表项目加载该类的ClassLoader已经被GC。
* 该类的Java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
当然可以退而求其次,替换新new的对象。旧对象无法改变
* 参考:http://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/
* 参考:https://www.ibm.com/developerworks/cn/java/j-lo-hotdeploy/
所以如果要热替换,最好是替换一些计算逻辑,不要替换数据结构
下面代码实现了热替换静态方法
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class App {
// 定义一个可能被热替换的函数
static Method method = null;
public static void main(String[] args) throws Exception{
// 初始化函数
method = App.class.getMethod("print", String.class);
new Thread(){
public void run(){
for(;;){
try {
Thread.sleep(1000);
method.invoke(null, "fang");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
for(;;) {
Thread.sleep(1000);
// 热加载的jar包位置
File file = new File("/Users/fang/Documents/test1/tmppp/A.jar");
URL url = file.toURL();
URL[] urls = new URL[]{url};
ClassLoader cl = new URLClassLoader(urls);
// 加载类aa.bb.A
Class c = cl.loadClass("aa.bb.A");
// 更新目标方法
method = c.getMethod("print", String.class);
}
}
public static void print(String str) {
System.out.println("A: " + str);
}
}