面向对象的语言如cpp、Java、C#等,都很好的支持了类 - class,在class的基础之上也实现了各个语言的面向对象的很多如多态、泛型等功能。cpp属于编译语言,类是”静态”的概念,会被编译器翻译成二进制的机器码,变成静态的符号。而像Java这种编译器编译+虚拟机解释的语言,虚拟机在编译后字节码中,在语义上更加灵活的支持class,比如我们可以通过代码对class进行操作,使程序能实现更丰富的功能(比如HotCode,字节码修改等)。
后续的内容将围绕Java语言的Class进行一些介绍,欢迎拍砖和讨论:
(本文涉及的环境为HotSpot JDK v1.7.0_67 64-Bit, Mac OSX)
初识 Java Class
最简单的在Java中使用一个Class的方式就是new
,可以把定义好的Java类实例化出来使用。当解释器看到这个类的时候,如果是第一次看到这个类的符号,不知道它有哪些成员,哪些函数,就要去加载并解析这个类,才能在代码里被引用。分为如下步骤:
加载
获得包名和类名之后,JVM要通过文件、URL的方式读入class的完整字节序列(如字节数组),放入内存交给ClassLoader(类加载器),当遇到如下主动访问时,会触发该class的装载
验证
根据读入的字节序列,判断该序列的合法性和规范性:
解析class
把字节序列转换成可识别的Class
初始化
调用函数(classinit)、初始化块,初始化class
Class文件结构
经过上述初始化后,该class就可以被使用和访问了。那么以我们最经常使用的方式,class以文件形式在磁盘被读入内存解析。下面我们来看一下具体的class文件结构是什么样子的
public class Test {
private int myValue = 4096;
public void myFunc(long a) {
long c = a;
}
}
- cafebabe
固定的文件头幻数(MagicHeader)
- 00 00 00 33
此版本号及主版本号
J2SE 8 = 52 (0x34 hex)
J2SE 7 = 51 (0x33 hex)
J2SE 6.0 = 50 (0x32 hex)
J2SE 5.0 = 49 (0x31 hex)
JDK 1.4 = 48 (0x30 hex)
(引自wiki百科)
- 00 1b
常量池元素个数(代表后面包含27-1=26个元素符号)
- 0a 00 08 00 15
对应一个常量符号,0a代表常量类型(MethodRef),08和15分别为指向的常量索引
此处不同版本的jdk存储常量的顺序可能不一致,各种常量类型对应的长度也不一致,有兴趣的读者可参阅官方文档
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
我们也可以通过jdk的工具 javap -v -constants -c -private Test
方便的查看常量池
- 00 21 常量池最后一个符号式 `java/lang/Object` 后面是访问控制字节(.!这两个字符开始)
代表public class
- 00 07 00 08
this_class和super_class,数字对应常量池中得索引即7和8
- 字段及方法的列表
(此处不再赘述)
Class loader是加载并解析class的关键模块,在JVM中,有多种Class loader:
BootStrap Classloader
启动类加载器(如rt.jar),cpp实现,Java中不可见
Extension Classloader
扩展类加载器,/lib/ext和-Djava.ext.dirs的加载等
App Classloader
也称为System ClassLoader,负责classpath下类加载,可通过ClassLoader.getSystemClassLoader()获得
自定义Classloader
应用自定义类加载器
除了Bootstrap Class Loader外,其它的类加载器都是java.lang.ClassLoader类的子类
各个加载器通过双亲委托
的机制来相互配合:
1. 当系统需要一个类时,加载器自底向上进行寻找
2. 当系统加载一个类时,加载器自顶向下加载(可以防止开发者覆盖Java核心类库)
见java.lang.ClassLoader类loadClass方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查该class是否已经加载过了,有则直接返回Class引用
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 使用双亲委托机制,让父加载器优先加载,避免JVM核心类库被下层加载器覆盖!
if (parent != null) {
// 如果存在父加载器,则让父加载器进行加载,委托给双亲
c = parent.loadClass(name, false);
} else {
// 如果不存在父加载器,则是Bootstrap加载器,直接加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 未找到则继续
}
if (c == null) {
long t1 = System.nanoTime();
// 父加载器未找到的话,则由当前加载器进行加载,findClass为虚方法,需要ClassLoader对应子类进行实现
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 解析class
resolveClass(c);
}
return c;
}
}
JVM默认的ClassLoader规范,严格的定义了ClassLoader的加载顺序。使得类加载是清晰、明确的单向加载、隔离的行为。一般获取ClassLoader的方式有以下几种:
ClassLoader.getSystemClassLoader().getParent()
获得Extension ClassLoader
Thread.getContextClassLoader 包含线程上下文的classloader
classloader是线程相关的,具有父子关系的线程间,默认共享一个classloader,也可通过Thread.setContextClassLoader打破
ClassLoader.getSystemClassLoader()
获取App Classloader
this.class.getClassLoader()
获取加载当前类的Classloader
在双亲委托模式下,下层加载器可以使用父加载器加载的类,即应用使用JDK核心类库。但反过来是不允许的,核心类库不能直接访问应用类。这种方式在一些情况下会造成一些麻烦。例如在使用JDBC时,应用要主动额外的加载数据库Driver类,虽然在应用看来这显得没有必要:
try {
Class.forName("com.mysql.jdbc.Driver"); //加载mysql driver驱动
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 获取驱动对应的Connection
Connection conn = DriverManager.getConnection(url,user,password);
分析一下getConnection()方法
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller)
throws SQLException {
// 首先获取caller的ClassLoader,caller类指向调用getConnection的Class
// 这里通过调用类的ClassLoader或Thread.getContextClassLoader()进行加载
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized (DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
...
for(DriverInfo aDriver : registeredDrivers) {
// 当前的ClassLoader是否可以加载driver
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
...
Connection con = aDriver.driver.connect(url, info);
...
} catch (SQLException ex) {
}
}
}
...
}
那么,我们如何在必要时打破双亲委托的模型?最简单的我们可以实现一个自定义的ClassLoader
public class YuShanFangMain {
public static class YuShanFang {
public String bonjour = "say hello";
}
/** * 自定义的ClassLoader的子类,忽略双亲委托机制,简单的直接返回Class */
public static class YuShanFangClassLoader extends ClassLoader {
private Map<String, Class<?>> supportedClass = new HashMap<>();
public YuShanFangClassLoader() {
super();
supportedClass.put("com.dev.YuShanFangMain.YuShanFang", YuShanFang.class);
}
/* 重写loadClass,根据名字寻找class */
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
/* 重写findClass,根据名字找到class */
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println(String.format("YuShanFang ClassLoader look up the class %s", name));
return supportedClass.get(name);
}
}
public static void main(String[] args) {
// 自定义的ClassLoader简单的实现loadClass、findClass即可
ClassLoader ysfClassLoader = new YuShanFangClassLoader();
try {
Class<?> ysf = ysfClassLoader.loadClass("com.dev.YuShanFangMain.YuShanFang");
System.out.println(ysf.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
通过上述我们可以看到实现一个自定义的ClassLoader足够简单,也带来很多灵活性。在这种模式下,我们可以自己开发高级功能,比如局部的代码热替换(运行中更改一个类的定义和行为),类似HotCode的功能。下面我们实现一个简陋版本的HotCode,模拟代码热替换
public static class YuShanFangClassLoader extends ClassLoader {
/** * 简单起见我们检测一个class文件的变化,我们只访问这一个类 */
private String hotCodeClass = "HotCodeClass";
private String fileName = String.format("/tmp/%s.class", hotCodeClass);
private static ConcurrentMap<String, Class<?>> codeCache = new ConcurrentHashMap<>();
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 这里我们要去掉findLoaderClass, 不使用代码类的缓存
// Class<?> c = findLoadedClass(name);
Class<?> c = findClass(name);
if (c == null)
c = super.loadClass(name, resolve);
if (resolve) {
resolveClass(c);
}
return c;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
/** * 1. 判断是否是此类加载器关心的类,如果不是则跳过加载 加载类时, * 会遇到父类、父接口等,比如任何类的默认父类Object, * 不能加载这些类。交由上层类加载器加载 */
if (!name.equals(hotCodeClass))
return clazz;
/** * 2. 检测代码文件时间戳(也可以是MD5)是否发生变化 */
File codeFile = new File(fileName);
String identifier = String.format("%s.%d", fileName, codeFile.lastModified());
clazz = codeCache.get(identifier);
if (clazz != null)
return clazz;
/** * 3. 发生变化后重读class文件字节码 */
try (FileChannel channel = new FileInputStream(codeFile).getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate((int) (codeFile.length() * 2));
while (true) {
int n = channel.read(buffer);
if (n == 0 || n == -1)
break;
}
byte[] bitsOfClass = new byte[buffer.position()];
System.arraycopy(buffer.array(), 0, bitsOfClass, 0, buffer.position());
buffer.array();
/** * 4. 解析字节码, defineClass是ClassLoader的默认解析器实现 * 这里我们复用即可 */
clazz = defineClass(name, bitsOfClass, 0, bitsOfClass.length);
/** * 5. 缓存 */
if (clazz != null) {
codeCache.put(identifier, clazz);
}
} catch (IOException | SecurityException e) {
e.printStackTrace();
}
return clazz;
}
}
while (true) {
try {
// 注意,每次要使用心得ClassLoader实例,否则ClassLoader会认为
// 加载的类为重复定义的!最简单的删除ClassLoader里已注册的Class
// 的方法是让GC回收这个ClassLoader !!
ClassLoader ysfClassLoader = new YuShanFangClassLoader();
Class<?> ysf = ysfClassLoader.loadClass("HotCodeClass");
Object object = ysf.newInstance();
Method method = ysf.getDeclaredMethod("func", int.class);
System.out.println(method.invoke(object, 5));
} catch (Exception e) {
e.printStackTrace();
}
Thread.sleep(1000L);
}
HotCodeClass实现如下
public class HotCodeClass {
public int func(int a) {
return a + a;
//return a * a;
}
}
程序输出如下:
25
25
25
25
10
10
10
10
很多web容器就是为了实现隔离,防止命名污染等,都是通过自定义不同的ClassLoader来实现,比如Tomcat。所以,灵活的运用ClassLoader可以带来很多高级功能,实现动态的加载管理。对高级功能有兴趣的读者可留言讨论!