您的位置:首页 > 编程语言 > Java开发

Java的类加载

2016-04-30 11:45 423 查看
  类加载器ClassLoader,用于将类加载到JVM,java提供了三个自带的类加载器,按启动顺序分别为,Bootstrap,Extension,Application,如下

  Bootstrap,启动类加载器,内嵌于JVM,本身是JVM的一部分,先天就加载在JVM中,用C++编写,Bootstrap的启动随着JVM的启动而启动。

  Extension,扩展类加载器,抽象类ClassLoader的子类。

  Application,系统(应用)类加载器,抽象类ClassLoader的子类。

  运行java程序时,首先加载JVM到内存,JVM启动Bootstrap类加载器,由其加载java的核心类库,jre/lib/下的所有jar包,之后Bootstrap加载Extension和Application类加载器,并创建它们的实例,除了Bootstrap,其余的类加载器都是类,java中的所有类均是由类加载器来加载到JVM才能使用的,然后启动Extension类加载器,由其加载jre/lib/ext/下的所有jar包,最后启动Application类加载器,由其加载classpath指定的目录下的类,classpath指定了工程当前目录下的类,由此这一步实际是加载应用程序到JVM。

  任何一个类都有一个静态的Class类型的实例class,类通过类加载器加载到内存后,类加载器会返回该类的Class类型的实例给JVM,同时用此实例初始化该类的静态Class类型实例class,JVM实际是通过此类的Class类型的实例,来操作此类的实例的,由此可以看出,java的底层实际是通过反射来使用类及其实例的。

  java程序中,类的唯一标识是类的完全限定名。

  JVM中,类的唯一标识是类的完全限定名 + 加载此类的类加载器的实例的名字,在JVM中,同一个类仅能被加载一次,这里的同一个类用的就是JVM中类的唯一标识来区分的。

  java的委托类加载机制,java中的类加载器使用委托加载模式,任何一个类加载器,都会被指定一个委托类加载器的实例,当类加载器加载某个类时,首先将其交给它的委托类加载器来加载。

  Bootstrap,特殊,无委托类加载器。

  Extension,委托Bootstrap。

  Application,委托Extention。

  用户自定义类加载器,由构造方法传入指定,传null为Bootstrap,不传默认为Application,传一个类加载器的实例。

  类加载器A委托B,B默认,委托Application,此时若A加载类,首先委托B加载,B先委托Application加载,Appliaction先委托Extension加载,Extension委托Bootstrap加载,此时Bootstrao调用loadClass加载,若未找到类,返回给Extension加载,依次类推,直到加载此类,若最后回到A后,A也未能加载类,抛出ClassNotFoundException异常。

  综上来看,使用委托机制,无论哪个类加载器,最终都会委托到Bootstrap来加载,因为除了Bootstrap,都要指定委托类加载器,只有委托到一个没有委托的类加载器才能停止,而只有Bootstrap没有委托类加载器,如此可以优先考虑用最底层的类加载器来加载,这是java的一种安全机制,保证java的核心类库始终由Bootstrap来加载,可以防止加载恶意的类。

  类E的加载A->B->App->Ext->Boot的过程中,若类E最终被App加载,我们称App为类E的定义类加载器,即真正加载E的,A,B,App都称为类E的初始类加载器,即导致类E被加载的,每个类加载器都维持一张表,记录自己作为初始类加载器所加载的类,由此A,B,App均记录了E,下次再用A,B,或App加载E时,会直接返回E的类类型实例,而不再加载,但是不管谁返回的,类E最初是由App加载的这点不变,通过E.class.getClassLoader()或e.getClass().getClassLoader()可返回加载E的定义类加载器,类E在JVM中的唯一标识为E的完全限定名 + E的定义类加载器。

A a = new A();
a.getParent();


  可以返回类加载器A的委托类加载器,仅类加载器有该方法。

  抽象类ClassLoader,所有的类加载器均是继承自此类加载器,ClassLoader加载类的过程是,使用loadClass()方法加载类,该方法先调用findLoaderClass()方法检查此类是否已经被加载过,应该就是检查自己作为初始类加载器维护的记录表,若加载过,直接返回此类的Class类型实例,否则返回null,若返回null,交由委托类加载器加载,委托类加载器的加载过程同样,也是loadClass()…,若均加载失败,即返回null,最后回到ClassLoader,其会调用findCLass()方法加载,这个就是抽象类ClassLoader中的抽象方法,实际没有实现,需要我们自己实现,App与Ext均是继承自ClassLoader,已经实现了findClass()方法。

  实现自定义类加载器通常是用于加载网络上的类,或者是本地不属于本工程的类,基本过程是,首先确定该类的URL,即资源定位,之后打开定位到此资源的输入流,而后以字节的形式将该类的内容读到本地的字节数组中,最后调用java提供的defineClass方法加载类的字节内容到JVM。

//自定义类加载器,必须是ClassLoader的子类,重写基类的findClass方法,用于加载我们自己指定的网络上的某个类资源,建议重写findClass方法,但是不建议重写loadClass方法,loadClass方法已经给我们写好了搜索类的顺序,但是findClass方法实际并没有完全实现,需要我们自己完善此方法,loadClass搜索类的顺序是,先调用findLoadedClass方法检测是否已经加载过此类,若加载过,则直接返回此类的Class实例,否则返回null,此时会调用委托类加载器的loadClass方法,搜索顺序同上,找到类或加载类成功,返回其Class实例,否则返回null,此时调用findClass方法,这个方法建议由继承ClassLoader的子类来实现
public class MyClassLoader extends ClassLoader {
//我们要加载的类资源的路径,仅给出路径,不指定资源文件的名字,即类名,在我们调用
//loadClass方法时以参数的形式传入
private String resource_path;
//指定自定义类加载器的委托类加载器
private ClassLoader parent;
//用户自定义类加载器使用默认的委托类加载器,App
public MyClassLoader(String resource_path) {
this.resource_path = resource_path;
}
//用户自定义类加载器使用指定的委托类加载器
public MyClassLoader(String resource_path, ClassLoader parent) {
//使用基类的构造方法来初始化委托类加载器
super(parent);
this.resource_path = resource_path;
}
//重写基类的findClass方法,此方法根据我们给出的类名,找到此类并加载到JVM,类名是
//指程序中类的完全限定名
protected Class<?> findClass(String class_name)
throws ClassNotFoundException {
//若找到此类并加载成功,应该返回此类的Class类型的实例
Class<?> c = null;
//将此类保存到程序中的字节数组中
byte[] class_data = this.getClassData(class_name);
if(class_data == null) {
//字节数组空,即没有找到类,抛出类未找到异常
throw new ClassNotFoundException();
} else {
//字节数组不空,即找到类并成功的读取了类的数据,使用java提供的方法
//defineClass将此类的数据加载到JVM,并返回Class类型实例
c = this.defineClass(class_name, class_data, 0, class_data.length);
}
return c;
}
//流,任何一个流,都有一个源,一个目的地,流就是将源的内容送往目的地, 无论输入流还是输出流,都是将源的内容送到目的地,输入输出只是相对程序来说的,程序是当前使用该流的程序,输入流,程序是目的地,将源的内容送往程序。输出流,程序是源,将程序的内容送往目的地
private byte[] getClassData(String class_name) {
//输入流,用于将类数据读入到程序
InputStream is = null;
//资源URL,协议,IP,路径,文件名
String resource_url = null;
URL url = null;
//字节数组作为缓冲,一次可以缓冲4KB个字节
byte[] buffer = new byte[1024*4];
//表示当前读取到的字节个数
int len = -1;
//字节数组输出流,可以将程序中字节数组的数据写出到输出流中,也可以将输出流中的内容直接转成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
//要加载的资源的完整URL
resource_url = resource_path + "/" +
class_name.replace(".", "/") + ".class";
//初始化URL对象,任一个URL对象都对应一个具体的资源
url = new URL(resource_url);
//通过一个具体的URL对象打开一个输入流,此输入流的源就是URL对象所指向的资源,即将URL对象所指向的资源的内容读入到程序
is = url.openStream();
//将源的内容读入到程序的缓冲字节数组中,一次读取的字节个数最大为缓冲字节数组的大小,返回值为读取的字节数,<= 字节数组的长度,若到文件的末尾或出错则返回-1,之后程序再从字节数组中取出数据做相应的处理
while((len = is.read(buffer)) != -1) {
//字节数组输出流,源是程序的缓冲字节数组,将源的内容写到输出流,此时数据是缓冲在输出流的,再向输出流写是向输出流后追加,循环写到字节数组输出流,将程序的内容写到字节数组输出流,还没有执行输出,最后将输出流转成字节数组算是输出了。
baos.write(buffer, 0, len);
}
//toByteArray创建一个字节数组,并将输出流的内容输出到此字节数组中去并返回此字节数组,这一步算是真正的输出
return baos.toByteArray();
} catch (Exception e) {
//输出异常仅仅是在调试程序的时候使用,实际交付客户的项目是不能将异常输出的,但是必须做出客户能够知道的相应的处理
System.out.println("getClassData " + e.toString());
//由系统输出方法的调用栈信息及异常信息,异常信息就是e.toString()返回的内容,e.printStackTrace()这个方法并非主线程执行而是由系统启动一个线程来做相应的处理,在此实际是个多线程程序,这个方法在项目交付客户的时候也应该注释掉,不能给客户,对于这些以后要注释的信息可以做个TODO标记
e.printStackTrace();
} finally {
//流使用完之后应该关闭,释放其所占用的资源
if(is != null) {
try {
is.close();
} catch (IOException e) {
System.out.println("getClassData " + e.toString());
e.printStackTrace();
}
}
if(baos != null) {
try {
baos.close();
} catch (IOException e) {
System.out.println("getClassData " + e.toString());
e.printStackTrace();
}
}
}
return null;
}
}
public class ClassLoaderTest {
public static void main(String[] args) {
//要访问的资源在web应用上,所以使用之前应该先启动Tomcat服务器
String resource_path = "http://localhost:8080/ServletTest/resource/classes";
MyClassLoader myl = new MyClassLoader(resource_path);
MyClassLoader myl1 = new MyClassLoader(resource_path);
String class_name = "com.lnu.test.Person";
try {
//可以使用Class<?> c = Class.forName(class_name, true, myl) 来加载类,这里是网络上的类,由此不可能有APP来加载,最终是由my1与my11来加载的
Class<?> c = myl.loadClass(class_name);
Class<?> c1 = myl1.loadClass(class_name);
System.out.println("类名:" + c.getName());
System.out.println("类的定义类加载器: " + c.getClassLoader());
System.out.println("自定义类加载器的委托类加载器 :" + myl.getParent());
System.out.println("===测试被不同的类加载器的实例加载的同一个类是
否是相同的类型===");
Object objc = c.newInstance();
Object objc1 = c1.newInstance();
JVM认为,被不同的类加载器实例加载的同一个类属于不同的类类型
objc与objc1分别被不同的类加载器加载,由此为不同的实例,返回false
System.out.println(objc.getClass() == objc1.getClass());
//两个类加载器的类类型,都是MyClassLoader类型,返回true,但是是两个不同的
实例
System.out.println(myl.getClass() == myl1.getClass());
System.out.println("======执行类的某个方法=========");
Object obj = c.newInstance();
Class<?>[] param_type = {};
Method method = c.getDeclaredMethod("say", param_type);
Object[] param_values = {};
method.invoke(obj, param_values);
} catch (Exception e) {
System.out.println("ClassLoaderTest " + e.toString());
e.printStackTrace();
}
}
}
//这个类主要是利用反射将URLClassLoader中的addURL方法暴露出来,此方法可以加载本地的JAR文件
public class URLClassLoaderReflectTest {
private Method method;
//系统类加载器App实际是URLClassLoader的子类,在此强制类型转换,对象上转用系统类加载器的实例初始化URLClassLoader
private static URLClassLoader ucl = (URLClassLoader) ClassLoader.getSystemClassLoader();
找到addURL方法,并将其返回
private Method getMethod() {
try {
//根据方法名addURL,方法的参数类型,返回此方法,这里可以直接使用URLClassLoader中的静态class变量,因为URLClassLoader是被已经加载的App的实例初始化的
method = URLClassLoader.class.getDeclaredMethod("addURL",
new Class[] { URL.class });
method.setAccessible(true);
return method;
} catch (Exception e) {
System.out.println("initAddMethod " + e.toString());
e.printStackTrace();
}
return null;
}
//对要加载的目录下的文件进行处理
//file,要加载的一个JAR文件,或是一系列JAR文件的上一级目录
//files,将要加载的所有的JAR文件都add到List列表
private void loopFiles(File file, List<File> files) {
//如果file是目录,则此目录下有超过一个的JAR文件要被加载
if (file.isDirectory()) {
//返回file目录下的所有文件及目录
File[] tmps = file.listFiles();
//遍历返回的文件数组
for (File tmp : tmps) {
//对其中的每个文件再次进行处理,若file还是目录则还要处理,只有是文件时才会进入下一步
loopFiles(tmp, files);
}
} else {
//若file是文件,则将文件名的后缀为jar或者zip的文件添加到list
if (file.getAbsolutePath().endsWith(".jar")
|| file.getAbsolutePath().endsWith(".zip")) {
files.add(file);
}
}
}
//通过反射调用addURL方法将jar文件加载到JVM
//file,要加载到JVM的JAR文件
private void loadJarFile(File file) {
try {
this.getMethod().invoke(ucl, new Object[] { file.toURI().toURL() });
System.out.println("加载JAR包:" + file.getAbsolutePath());
} catch (Exception e) {
System.out.println("loadJarFile " + e.toString());
e.printStackTrace();
}
}
//根据给定的文件的路径来加载路径下的JAR文件
//path,文件的路径
public void loadJarPath(String path) {
List<File> files = new ArrayList<File>();
File lib = new File(path);
loopFiles(lib, files);
for (File file : files) {
loadJarFile(file);
}
}
public static void main(String[] args) {
//这个方法执行完后就已经将E:/jar/路径下的JAR文件加载到JVM了
new URLClassLoaderReflectTest().loadJarPath("E:/jar/");
try {
//JAR文件已经被加载到JVM,loadClass直接返回Class实例
Class<?> c = ucl.loadClass("com.lnu.test.TestJar");
Object obj = c.newInstance();
Class<?>[] param_type = {};
Object[] param_values = {};
Method mhd = c.getDeclaredMethod("say", param_type);
mhd.invoke(obj, param_values);
} catch (Exception e) {
System.out.println("main " + e.toString());
e.printStackTrace();
}
}
}
public class URLClassLoaderTest {
//用URLClassLoader加载JAR包下的类,需要指定JAR包文件的位置,要给出文件名
private String resource_url_1 = "http://localhost:8080/ServletTest/resource/classes/TestJar.jar";
private String resource_url_2 = "http://localhost:8080/ServletTest/resource/classes/TestJars.jar";
//用URLClassLoader加载指定路径下的class文件,在此仅仅是指定路径,不指定类文件,实际要加载的类文件在调用loadClass方法时传入,URLClassLoader认为URL资源最后一个"/"之前出现的内容都是路径,所以在此classes后必须有"/",否则URLClassLoader会认为classes是一个文件,而试图去解析加载它,而不是将其当做一个目录
private String resource_path = "http://localhost:8080/ServletTest/resource/classes/";
//创建两个URLClassLoader对象,一个用于测试加载JAR包下的类,一个用于测试加载指定路径下的类
private URLClassLoader ucl_jar;
private URLClassLoader ucl_class;
//调用反射时所用到的,参数类型数组与参数值数组
private Class<?>[] param_type = {};
private Object[] param_values = {};
private Class<?> c;
private Object obj;
private Method mhd;
public void TestURLClassLoader() {
try {
//加载JAR包下的类,指定URL资源,
URL url_1 = new URL(resource_url_1);
URL url_2 = new URL(resource_url_2);
//直接加载类文件,仅指定URL资源的路径
URL url_path = new URL(resource_path);
//URLClassLoader的构造方法需要传递一个URL数组,意思是指,可以指定可以搜索加载多个指定路径下的JAR文件或class文件资源
ucl_jar = new URLClassLoader(new URL[]{url_1, url_2});
ucl_class = new URLClassLoader(new URL[]{url_path});
//之后的内容就跟自定义类加载器的使用同样了,即,调用loadClass方法,指定类名,来加载指定的类
System.out.println("====测试使用URLClassLoader加载jar包下的类====");
c = ucl_jar.loadClass("com.lnu.test.TestJar");
obj = c.newInstance();
mhd = c.getDeclaredMethod("say", param_type);
mhd.invoke(obj, param_values);
System.out.println("====测试使用URLClassLoader加载jar包下的类====");
c = ucl_jar.loadClass("com.lnu.test.TestJars");
obj =c.newInstance();
mhd = c.getDeclaredMethod("say", param_type);
mhd.invoke(obj, param_values);
System.out.println("===测试使用URLClassLoader加载指定路径下的类==");
c = ucl_class.loadClass("com.lnu.test.Person");
obj = c.newInstance();
mhd = c.getDeclaredMethod("say", param_type);
mhd.invoke(obj, param_values);
System.out.println("==测试使用URLClassLoader加载指定路径下的类===");
c = ucl_class.loadClass("com.lnu.test.Persons");
obj = c.newInstance();
mhd = c.getDeclaredMethod("say", param_type);
mhd.invoke(obj, param_values);
} catch (Exception e) {
System.out.println("URLClassLoaderTest say " + e.toString());
e.printStackTrace();
} finally {
try {
if(ucl_jar != null) {
ucl_jar.close();
}
if(ucl_class != null) {
ucl_class.close();
}
} catch (IOException e) {
System.out.println("URLClassLoaderTest say " + e.toString());
e.printStackTrace();
}
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: