最近看《深入理解JAVA虚拟机》一书,关于类加载,做下记录。
java类型的加载和链接过程都是在运行过程中完成的,类的生命周期包括 加载到虚拟机中,然后经过验证,准备,解析,初始化,使用,最后卸载。
虚拟机规范中严格规定了有且只有四种情况必须立即进行初始化(前面过程要先进行),包括遇到new, 获取/修改非final static变量或调用静态方法; 使用java.lang.reflect进行反射调用; 初始化类时如果父类没初始化则先初始化父类; main方法所在主类。
加载主要是获取类的二进制字节流,并将字节流代表的静态存储结构转化为方法区的运行时数据结构,然后再java堆中生成一个代表该类的java.lang.Class对象。
加载后验证,准备阶段为类变量(static修饰)分配内存并赋初始值,非static修饰的在对象初始化时随对象一起分配在java堆中。但final static会被初始化为ConstantValue属性值,如 final static int a=123 会赋值123,获取该字段不会触发初始化; 但 static int a=123, 准备阶段只会赋int默认值0,获取该字段时会触发初始化。
准备阶段完成后,类初始化时,先对static变量赋值。
借用网上的一个例子:
class Test1 {
public static Test1 t= new Test1();
public static int a;
public static int b = 0;
private Test1() {
a++; b++;
}
public static Test1 GetInstence() {
return t;
}
}
在其它类中调用getInstance方法后,a=1,b=0. 按照上述流程分析一下,首先static变量分配类型赋初值,t=null,a=0,b=0;然后静态变量t赋值, a=1,b=1,a无赋值动作,b赋值为0。
类通过classloader加载,jvm默认有3个classloader,通过下面代码可以打印classloader加载的类信息:
URL[]urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i< urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
Bootstrap ClassLoader:
加载java基础类,由C++编写,getClassLoader()返回NULL,负责加载{JRE_HOME}/lib目录下的rt.jar(特定文件名的jar包,自定义的jar不加载)及{JRE_HOME}/classes下的类
ExtClassLoader:加载java扩展类,加载{JRE_HOME}/lib/ext下的jar及{JRE_HOME}/lib/ext/classes下的类
AppClassLoader:加载应用classpath下的类
JVM中类加载器实现的是双亲委派模型,实现代码集中在java.lang.ClassLoader的loadClass方法中,加载类时,先检查是否被加载,否则调用父加载器的loadClass方法,如果父类加载器为空,则调用Bootstrap ClassLoader。每个层次的加载器都如此,最终传递到顶层类加载器,都无法加载,才会使用子加载器加载,都无法加载,抛class not found exception
下面是个例子, com.jar中有个UserDefineClass,分别打印其classloader:
//自定义classloader调用类
public class UserDefineClass {
public void method1() {
System.out.println("userdefine loader:" + UserDefineClass1.class.getClassLoader());
if(UserDefineClass1.class.getClassLoader() != null) {
System.out.println("userdefine loader parent:" + UserDefineClass1.class.getClassLoader().getParent());
}
new AppClass().method1();
}
}
public class AppClass {
public void method1() {
System.out.println("appclass loader:" + AppClass.class.getClassLoader());
}
}
// 测试类main方法
public static void main(String []args) throws Exception {
ClassLoader loader = new URLClassLoader(new URL[] { new URL("file:/E:/testClass/com.jar") } );
//Thread.currentThread().setContextClassLoader(loader);
Class c = loader.loadClass("com.test.UserDefineClass");
Method m = c.getMethod("method1");
m.invoke(c.newInstance());
}
将UserDefineClass从工程中移除,打成jar包放在E:/testClass下,执行main方法,会显示:
userdefine loader:java.net.URLClassLoader@19ee1ac
userdefine loader parent:sun.misc.Launcher$AppClassLoader@45a877
appclass loader:sun.misc.Launcher$AppClassLoader@45a877
如果将com.jar拷贝到{JRE_HOME/classes}下解压(build
path指定的jre),且将AppClass也放在{JRE_HOME/classes}下,则都会由bootstrap
classloader加载。但如果AppClass没在{JRE_HOME/classes},UserDefineClass会由bootstrap
classloader加载,但后面会报AppClass: class not found,因为由bootstrap
classloader加载的类其依赖类也必须由它或是其parent加载。
将AppClass放在{JRE_HOME/classes}下,UserDefineClass放在{JRE_HOME}/lib/ext下,由
extclassloader加载,
不管AppClass有没有放在其它地方,UserDefineClass由ext加载,AppClass由bootstrap加载。
总之,优先由bootstrap classloader加载,然后ext classloader,然后app
classloader,最后自定义的classloader,如果当前classloader加载的类中依赖其它类,且依赖的类未由当前
classloader或其parent加载,则报class not found。
如果不同的地方有不同版本的相同类,则可能会因为其它类依赖的程序版本不同,导致问题,比如WEB工程中WEB-INF下的jsp编译相关的jar与容器自带的冲突,容器也有自己的类加载器及加载顺序、范围。
双亲委派模型中如果被父加载器加载的类要调用子加载器的具体实现类,如JNDI,JDBC实现,java引入线程上下文加载器(thread
Context ClassLoader)。 现在也有灵活的类加载架构,如OSGi,实现模块化热部署等。
可以自定义classloader,如果类被自定义的classloader和JVM的classloader同时加载,则是不同的CLASS,会出现 class cast exception,可以定义接口或基类让JVM加载,自定义classloader加载实现类,可以转为父类,实际调用的是子类的实现方法:
public class ClassLoadTest {
public static void main(String []args) throws Exception {
Object obj = new MyClassLoader().loadClass("com.java.load.Test").newInstance();
System.out.println(obj.getClass().getClassLoader()); // parent是AppClassLoader
// Test类implements LoadInterface,不能与LoadInterface在同一个package,放在{JRE_HOME}/classes下
LoadInterface c = (LoadInterface)obj;
c.method1();
}
}
class MyClassLoader extends ClassLoader { // 自定义加载器,不完全遵循双亲委派模型
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
将上面AppClass.java改一下,继承java.lang.Comparable接口,重写接口方法,假如UserDefineClass作为JVM基类或ext类加载,那么是没法访问AppClass的,可以在UserDefineClass中保存一个ClassLoader对象,将AppClass的classloader即AppClassLoader传入, 或UserDefineClass通过Thread.currentThread().getContextClassLoader()拿到默认AppClassLoader的或设置的classloader来加载AppClass类,由于Comparable也是在JVM的bootstrap classloader加载,可以转换成Compare类型,调用接口方法时,实际执行的是子类的方法。这样实现了类似jdbc中被父加载器加载的类要调用子加载器的具体实现类。
loader = Thread.currentThread().getContextClassLoader();
Object obj = loader.loadClass("AppClass").newInstance();
Comparable<String> t2 = (Comparable)obj;
t2.compareTo(null);
浙公网安备 33010602011771号