java反射机制与类加载机制
Class (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/lang/Class.html
Field (Java Platform SE 7 ) - https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Field.html
ByteArrayOutputStream (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/io/ByteArrayOutputStream.html
URL (Java SE 9 & JDK 9 ) - https://docs.oracle.com/javase/9/docs/api/java/net/URL.html
Class热替换与卸载 - http://www.importnew.com/22462.html
深入探讨 Java 类加载器 - https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
反射机制(Reflection) – http://blog.qiji.tech/archives/4374
深入理解Java类型信息(Class对象)与反射机制 - https://blog.csdn.net/javazejian/article/details/70768369
深入理解Java类加载器(一):Java类加载原理解析 - https://blog.csdn.net/justloveyou_/article/details/72217806
认识Class对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于Java中出现RRTI的说法则是源于《Thinking in Java》一书,其作用是在运行时识别一个对象的类型和类的信息,这里分两种:传统的 RTTI,它假定我们在编译期已知道了所有类型,在没有反射机制创建和使用类对象时,一般都是编译期已确定其类型,如new对象时该类必须已定义好。另外一种是反射机制,它允许我们在运行时发现和使用类型的信息。在Java中用来表示运行时类型信息的对应类就是Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。
Class类也是类的一种,但是特别地 Class 类用来描述使用 class 关键字定义的类,这些描述信息用于在运行时获取类定义内容,Class类的对象作用是运行时提供或获得某个对象的类型信息。编写好的类在编译后都会产生一个Class对象,表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的字节码文件中。比如创建一个Shapes类,编译Shapes类后就会创建其包含Shapes类相关类型信息的Class对象,并保存在Shapes.class字节码文件中。每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载。
在运行期间,如果我们要产生某个类的对象,Java虚拟机(JVM)会检查该类型的Class对象是否已被加载。如果没有被加载,JVM会根据类的名称找到.class文件并加载它。一旦某个类型的Class对象已被加载到内存,就可以用它来产生该类型的所有对象。
通过反射方法实例化对象及修改私有成员为公有成员
import java.lang.reflect.*;
class Gum {
private int weight = 0;
static { System.out.println("Loading Gum"); }
public Gum(){ }
public Gum(int i){ weight = i; }
public String greeting(){ return "Hi!"; }
}
public class coding {
public static void print(Object obj) {
System.out.println(obj);
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
Class clz = Class.forName("Gum");
// Class clazz = Gum.class;
// Constructor[] cts = clz.getConstructors();
// for ( Constructor c:cts ) {
// print("Contructor: "+c.getName());
// }
// Constructor constructor = clz.getConstructor();
// Object object = clz.newInstance();
Constructor constructor = clz.getConstructor(int.class);
Object object = constructor.newInstance(0);
Method method = clz.getMethod("greeting");
String msg = (String)method.invoke(object);
print(msg);
Field field = clz.getDeclaredField("weight");
field.setAccessible(true);
print("get private member:"+field.get(object));
Class<?> class1 = Class.forName("Gum");
String name = class1.getClassLoader().getClass().getName();
print("class loader is " + name);
} catch(Exception e) {
e.printStackTrace();
}
}
}
使用类反射方法 Class.forName() 载入类,返回值是对应类的Class对象,也可以通过对象实例的 getClass() 方法获取,这是基础对象类Object定义的方法,字面常量的方式获取Class对象如 Object.class。如果没有定义相应的类构造函数,getConstructor() 会引发 NoSuchMethod 异常。
Field.setAccessible(true) 并不是将方法的访问权限改成了public,而是取消java的权限控制检查。即使是public成员,其accessible 属性默认也是false,即需要进行权限检查。
与Java反射相关的类如下:
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法
类加载器
JVM预定义几种类加载器,当JVM启动的时候,Java缺省开始使用如下三种类型的类加载器:
启动(Bootstrap)类加载器:引导类加载器是用 本地代码实现的类加载器,它负责将 <JAVA_HOME>/lib下面的核心类库 或 -Xbootclasspath选项指定的jar包等 虚拟机识别的类库 加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以 不允许直接通过引用进行操作。
扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System)类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库 加载到内存中。开发者可以直接使用系统类加载器,是最常用的加载器,同时也是java中默认的加载器。通过Java反射机制 Class.getClassLoader() 可以得到类加载器信息。
除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器(context class loader),从 JDK 1.2 开始引入的。Java.lang.Thread 类中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。典型的例子有:通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。大部分Java application服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。还有一些采用hot swap特性的框架,也使用了线程上下文类加载器,比如 seasar (full stack framework in japenese)。
JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用)。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形)。 系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器。
虚拟机出于安全等因素考虑,不会加载<JAVA_HOME>/lib目录下存在的陌生类,换句话说,虚拟机只加载<JAVA_HOME>/lib目录下它可以识别的类。因此,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。
在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。通过用户自定义的类装载器,你的程序可以加载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。
除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般 用户自定义类加载器的工作流程(可以结合后面问题解答一起看):
1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
3、调用本类加载器的 findClass() 方法,试图获取对应的字节码。如果获取的到,则调用 defineClass() 导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,向上抛出异常。
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中,一个类用其全名和一个ClassLoader的实例 作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。
在java.net包下有一个 URLClassLoader 类提供了运程加载类的功能,它让我们可以通过以下几种方式进行加载:
* 文件: (从文件系统目录加载)
* jar包: (从Jar包进行加载)
* Http: (从远程的Http服务进行加载)
一个class被一个ClassLoader实例加载过的话,就不能再被这个ClassLoader实例再次加载,即加载类时调用了defileClass()方法,重新加载字节码、解析、验证后就不能重复执行。系统默认的AppClassLoader加载器内部会缓存加载过的class,重新加载的话,就直接取缓存。所以需要热加载只能重新创建一个ClassLoader,然后再去加载已经被加载过的class文件。实现热加载需要重载 ClassLoader 的 loadClass() 方法,跳过内部的双亲委托机制。即使不采用双亲委托机制,比如java.lang包中的相关类还是不能自定义一个同名的类来代替,主要因为JVM解析、验证class的 时候,会进行相关判断。强制重复加载引发 java.lang.LinkageError 异常:
Exception in thread "main" java.lang.LinkageError: loader (instance of NetworkClassLoader): attempted duplicate class definition
JVM中class和Meta信息存放在PermGen space区域。如果加载的class文件很多,那么可能导致PermGen space区域空间溢出。引起:java.lang.OutOfMemoryErrorPermGen space. 对于有些Class我们可能只需要使用一次,就不再需要了,JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
该类所有的实例都已经被GC。
加载该类的ClassLoader实例已经被GC。
该类的java.lang.Class对象没有在任何地方被引用。
System.gc()执行后的Class的卸载是不可控的。
自定义类加载器--远程类加载器
反射机制的应用必须要求该类是public访问权限的类,这样才能由加载器加载。要实现HotSwap热加载,只需要创建新实例去加载目标类就可以,热加载逻辑可以根据文件更新时间来做判断。
import java.lang.reflect.*;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.net.URL;
import java.net.HttpURLConnection;
/**
* NetworkClassLoader ncl = new NetworkClassLoader("http://xxx.com");
* String basicClassName = "Gum";
* Class<?> clazz = ncl.loadClass(basicClassName);
* Gum oo = (Gum)clazz.newInstance();
*/
class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
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 = rootUrl + "/" + className.replace('.', '/') + ".class";
try {
URL url = new URL(path);
// HttpURLConnection con = (HttpURLConnection)url.openConnection();
// System.out.println(con.getResponseCode() +" "+ con.getContentType());
// BufferedReader buffer = new BufferedReader(new InputStreamReader(ins));
// StringBuffer bs = new StringBuffer();
// String line = null;
// while((line=buffer.readLine())!=null){
// bs.append(line);
// }
// System.out.println("getClassData:"+bs.length()+":"+bs.substring(0));
// return bs.toString().getBytes();
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int len = 0;
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("getClassData:"+len+":");
return baos.toByteArray();
} catch (Exception e) {
System.out.println("getClassData exception:");
e.printStackTrace();
}
System.out.println("getClassData null:");
return null;
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class c = findLoadedClass(name);
if( name.startsWith("java.") ){
ClassLoader system = ClassLoader.getSystemClassLoader();
c = system.loadClass(name);
}
if (null==c) c = findClass(name);
if( resolve ) resolveClass(c);
return c;
}
}