注:本文是个人对java虚拟机规范提到的知识的一点总结。
在Java中,类必须经过jvm使用类装载器(class loader)装载(load)之后才能使用。以主程序(Class
A)为例,当jvm调用程序的main方法时,在没有装载A.class文件时,这是不可能的事情。
装载class文件是程序执行的第一步,这跟linux下的程序执行器(execve)装载目标文件的原理是一样的,jvm充当execve的角色,读取
class文件的二进制数据,转换成jvm内部能够识别的数据结构。在这个过程中生成了一个A类关联的Class对象,该Class对象记录了A类相关的
数据。
一个类真正能使用要经过装载、连接、初始化三个步骤。连接又可以分为“验证”、”准备“、”解析
“三个步骤。总体说来,由于class文件本身记录了很多数据结构(常量池、字段信息、方法信息、引用信息等),必须要转换成内部形式,这个过程就通过装
载来实现,但是,class文件自身并没有完成链接,这跟C的目标文件有很大差别——其实也就是解析执行和编译执行的区别了,装载之后形成的内部结构还存
在很多符号引用,需要resolve引用,这就是连接过程,原理跟C的链接是一样——解决内部和外部符号引用。
在连接过程,jvm试图解析类A中出现的符号引用,比如A中定义了:
private B b=new B();
符号引用b是一个字段引用,B()是一个方法引用,并且B是定义在别的class文件的(一个类只能对应一个class文件),所以jvm试图解析
b,这个过程同时要对B进行装载(如果B并没有被当前装载器装载),装载过程一般是递归进行的,一直到达最高类层次(Object)。
关于JVM是如何装载、连接、初始化的,内容太多,详细的信息要参考Java虚拟机规范。
Java中(jdk 1.6版)有三种加载器,启动加载器→扩展加载器(ExtClassLoader)→用户程序加载器(AppClassLoader)。箭头左边的类是右边类的父类。其中启动类加载器对于我们是不可见的,用于加载java源码包的类以及jdk安装路径下的类(rt.jar
etc),而扩展类加载器用于加载ext目录下的类,用户类加载器则是加载普通的class文件。
Java给我们提供了使用自定义类装载器来装载程序的可能,这有利于程序的扩展。想想看applet
的运行,JDBC驱动的装载(典型的JDBC访问语句Class,forName()),我们在编译的时候能知道它们要装载什么类型的类吗?
以下仅通过一个示例来说明自定义类装载器的原理以及使用自定义装载实现动态类型转换:
package greeter;
public interface Greeter {
public
void sayHello();
}
我在包greeter下定义了一个接口Greeter。
然后我实现一个自定义类装载器GreeterClassLoader(如果是没有特殊目的的加载,直接继承ClassLoader就可以了,根本不用覆盖任何方法):
//注:该实现是稍微有点复杂的,JDK文档鼓励使用另一种方法,稍后提供这种方法并说明两者之间的差异。
public class GreeterClassLoader extends ClassLoader{
private String basePath;
//自定义装载作用的根目录,装载器将在这个目录下查找class文件
public
GreeterClassLoader(String path){
this.basePath=path;
}
//覆盖loadClass方法
@Override
protected
synchronized Class loadClass(String name, boolean resolve) throws
ClassNotFoundException {
Class result=null;
//看看这个类是不是已经加载过了,如果是直接返回缓存中的Class对象(放在JVM的堆中)
result=super.findLoadedClass(name);
if(result!=null){
System.out.println("class "+name+" has been loaded before.");
return result;
}
//上一步没找到,看看是不是系统类,委托给启动类装载器去装载
result=super.findSystemClass(name);
if(result!=null){
System.out.println("found by system classloader.");
return result;
}else{
System.out.println("system loader can not find it.");
}
//都找不到,直接读取源文件
byte classdata[]=null;
//do not try to load system class
if(name.startsWith("java")){
System.out.println("i encountered a system class:"+name);
throw new ClassNotFoundException();
}
classdata=this.getClassData(name);
if(classdata==null){
System.err.println("can't load "+name);
}
System.out.println(name);
//从字节码中解析出一个Class对象
result=defineClass(name, classdata, 0, classdata.length);
if(result==null){
System.out.println("Class format error.");
throw new ClassFormatError();
}
//是否需要解析
if(resolve){
this.resolveClass(result);
}
return result;
// return super.loadClass(name, resolve);
}
//从文件中读取class文件的二进制数据
private byte[] getClassData(String name){
byte[] retArr=null;
//read the byte data of the class file
name=name.replace('.', '/');
String path=this.basePath+"/"+name+".class";
System.out.println(path);
try {
FileInputStream fin = new FileInputStream(path);
BufferedInputStream bis=new BufferedInputStream(fin);
ByteArrayOutputStream baos=new ByteArrayOutputStream();
int c=bis.read();
while(c!=-1){
baos.write(c);
c=bis.read();
}
bis.close();
System.out.println("read finished.");
retArr=baos.toByteArray();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
return null;
}catch(IOException ex){
ex.printStackTrace();
return null;
}