实战java虚拟机
深入理解java虚拟机
ClassLoader
类加载器(ClassLoader),它的主要工作在Class装载的加载阶段。其主要作用是从系统外部获得Class二进制数据流,然后交给java虚拟机进行连接、初始化等操作(ClassLoader无法改变类的连接和初始化工作)。
ClassLoader是一个抽象类,它提供了一些重要的接口,用于自定义Class的加载流程和加载方式.
public abstract class ClassLoader{
//给定一个类名.加载一个类,返回代表这个类的实例。如果找不到则抛出ClassNotFoundException;
public Class<?> loadClass(String name) throws ClassNotFoundException;
//根据给定的字节码流b定义一个类,off和len表示实际Class信息在byte数组中的位置和长度。这是procted方法,只允许ClassLoader以及其子类中使用;
protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain);
//查找一个类,这是一个受保护的方法,也是重载ClassLoader时,重要的系统扩展点。这个方法会在loadClass()时被调用,用于自定义查找类的逻辑。
protected Class<?> findClass(String name) throws ClassNotFoundException {};
//查找已经加载的类,它会去寻找已经加载的类。这个类是final方法,无法被修改。
protected final Class<?> findLoadedClass(String name) ;
}
ClassLoader的结构中,还有一个重要字段parent,它也是一个ClassLoader的实例,这个字段称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader会将某些请求交给自己的双亲处理。
ClassLoader的分类
在标准的java程序中,java虚拟机会创建3类ClassLoader为整个应用程序服务。它们分别是:
- BootStrapClassLoader(启动类加载器):负责加载JAVA_HOME/jre/lib目录中,或者-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,resources.jar…,名字不符合的即使放在该目录下也不识别)
-Xbootclasspath/a:path 被指定的文件追加到默认的bootstrap路径中。
-Xbootclasspath/p:path 让jvm优先于默认的bootstrap去加载path中指定的class
命令示例:
java -Xbootclasspath/p:D:\tmp\boottest.jar
java -Xbootclasspath/a:D:\tmp\clz
- ,ExtensionClassLoader(扩展类加载器):负责加载 JAVA_HOME/jre/lib/ext目录中,或者被java.ext.dirs系统变量指定路径中的所有类库
- AppClassLoader(应用类加载器,也称系统类加载器)。它负责加载用户路径(ClassPath)上所指定的类库。可以通过
ClassLoader.getSystemClassLoader()
开获取 - 此外,每个应用程序还可以自定义ClassLoader,扩展java虚拟机的获取Class数据的能力。
当系统需要使用一个类时,在判断类是否已经加载时,会先从当前底层的类加载器进行判断。
当系统需要加载一个类时,会从顶部类开始加载,一次向下尝试,直到成功。
这些ClassLoader中,启动类加载器最为特殊,它完全是由C代码实现的,并且在java中没有对象与之对应。
public class PrintClassLoaderTree {
public static void main(String[] args) {
ClassLoader cl = PrintClassLoaderTree.class.getClassLoader();
while (cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
}
}
打印结果:
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@1b6d3586
另外:String.class.getClassLoader(); 为null,并不是说没有类加载器,而是说它是的类加载器是 BootStrapClassLoader
ClassLoader的双亲委托机制
双亲委托机制,即在类加载的时候,系统会判断当前类是否已经被加载,如果被加载,就直接返回可用的类。如果没有被加载,则会请求双亲处理,若双亲处理失败,则自己处理。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, 检查类是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//委托双亲加载
c = parent.loadClass(name, false);
} else {
//bootstrap加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
long t1 = System.nanoTime();
// 若仍未找到,则执行自身的findClass寻找
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
return c;
}
}
双亲委托机制的弊端
双亲委托机制,检查类是否已经加载的委托是单向的。这种方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader加载的类。
通常情况下,启动类加载器中加载的类是系统核心类,包括一些重要的系统接口,而在应用类加载器,为应用类。这种模式下,应用类访问系统类自然是没有问题的,但是系统类访问应用类就会出现问题。
比较典型的例子便是JNDI服务,JNDI现在已经是java的标准服务,它的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署的在应用程序ClassPath下的JNDI接口提供者(SPI,Servie Provider Interface)的代码),但是启动类加载器不可能“认识”这些代码。
在java平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常称为(SPI,Service Provider Interface).java中常见的SPI接口有如JDBC,JNDI,JCE,JAXB和JBI等。
普通开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。
为了解决SPI接口调用的问题,java团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序全局范围内都没有设置过的话,那么这个类加载器默认是这个应用程序加载器。
SPI解决的方法—-JDBC为例
在rt.jar中仅定义了接口java.sql.Driver
/*
* When a Driver class is loaded, it should create an instance of
* itself and register it with the DriverManager. This means that a
* user can load and register a driver by calling
* <pre>
* <code>Class.forName("foo.bah.Driver")</code>
* </pre>
* 需要向DriverManager.register()自己;
**/
public interface Driver { }
引入mysql-connector-java-5.1.46.jar,后通常我们会这样使用,来创建数据库连接,
public Connection getConnection(String username,String password) throws ClassNotFoundException,
SQLException {
// 加载MySQL的JDBC的驱动 ---- 无需使用Class.forName,由于已经在mysql.Driver static{}域中注册了自己。
// Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/activitidb";
Connection conn = DriverManager.getConnection(url, username, password);
return conn;
}
很明显,此时的Driver是通过AppClassLoader加载
,查看DriverManager源码
public class DriverManager {
//1.类静态初始化
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
//2.loadInitialDrivers
private static void loadInitialDrivers() {
//......
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//3.使用serviceLoader寻找实现了java.sql.Driver的插件
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
try{
//4.迭代寻找Driver
while(driversIterator.hasNext()) {
println(" Loading done by the java.util.ServiceLoader : "+driversIterator.next());
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
//5.使用SystemClassLoader初始化driver
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
}
ServiceLoader源码:
public final class ServiceLoader<S> implements Iterable<S> {
private static final String PREFIX = "META-INF/services/";
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
public static <S> ServiceLoader<S> load(Class<S> service) {
//3.2返回线程上下文类加载器(Thread Context ClassLoader)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
private class LazyIterator implements Iterator<S> {
Enumeration<URL> configs = null;
//4.2迭代
public boolean hasNext() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//4.3 寻找jar插件中/META-INF/services/java.sql.Driver文件
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
}
}
针对各大厂商如Oracle,Mysql都必定会在各自的jar包中存在文件/META-INF/services/java.sql.Driver.
debug代码时,观察如下信息:
Thread.currentThread().getContextClassLoader(); //当前线程:sun.misc.Launcher$AppClassLoader@160bc7c0
com.mysql.jdbc.Driver.class.getClassLoader();//厂商驱动:sun.misc.Launcher$AppClassLoader@160bc7c0
DriverManager.class.getClassLoader(); // null -- BootstrapClassLoader
//DriverManager.getCallerClassLoader(); // sun.misc.Launcher$AppClassLoader@160bc7c0
java.sql.Driver.class.getClassLoader();//null -- BootstrapClassLoader
如果按照上述的代码展示的信息,按照双亲机制,DriverManager.connect加载到com.mysql.jdbc.Driver的,所以DriverManager才会做出如下的抉择:
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {
// Gets the classloader of the code that called this method, may
// be null.
ClassLoader callerCL = DriverManager.getCallerClassLoader();
return (getConnection(url, info, callerCL));
}
private static Connection getConnection(
String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if(callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
//............
}
自定义classloader
在jdk1.2之后,java.lang.ClassLoader添加了一个新的protected的方法findClass(),如果不需要修改类加载默认机制,重写findClass方法即可。
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(className);
if (null == clazz) {
File file = null;//获取class源文件
byte[] bytes = null;//获取资源文件
clazz = defineClass(className ,bytes , 0 ,bytes.length);
}
return clazz;
}
突破双亲机制
双亲模式的类加载方式是虚拟机的默认的行为,但并非必须这么做,通过重载ClassLoader可以修改这些行为。实际上,不少应用软件和框架都修改了这种行为,比如Tomcat和OSGi框架,都有各自独特的类加载顺序。突破双亲机制,需要通过重载loadClass()方法,改变类加载顺序。
public class OrderClassLoader extends ClassLoader {
private String clazzBaseForder;
public OrderClassLoader(String clazzBaseForder) {
this.clazzBaseForder = clazzBaseForder;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class re = findClass(name);
if(re == null) {
System.out.println("i can't load class:"+name+",need help from parent");
return super.loadClass(name, resolve);
}
return re;
}
@Override
protected Class<?> findClass(String clazzName) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(clazzName);
if (null == clazz) {
String fileName = getClazzFile(clazzName);
try {
RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
FileChannel channel = raf.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel writeChannel = Channels.newChannel(baos);
int len ;
while(true ) {
//Class一次性读取1024byte,会报错 java.lang.ClassFormatError: Extra bytes at the end of class file
int read = channel.read(buffer);
if(read == -1){
break;
}
buffer.flip();
writeChannel.write(buffer);
buffer.clear();
}
channel.close();
byte[] bytes = baos.toByteArray();
clazz = defineClass(clazzName ,bytes , 0 ,bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return clazz;
}
private String getClazzFile(String clazzName){
return clazzBaseForder + File.separator + clazzName.replaceAll("\\.", "/")+".class";
}
}
执行测试代码:
static void convert() throws Exception{
OrderClassLoader loader = new OrderClassLoader("D:\\tmp\\clz");
Class clazz = loader.loadClass("cn.jhs.chap10.HelloLoader");
HelloLoader helloLoader = (HelloLoader) clazz.newInstance();
}
会抛出异常:java.lang.ClassCastException: cn.jhs.chap10.HelloLoader cannot be cast to cn.jhs.chap10.HelloLoader
两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类完全不同的。
热替换(HotSwap)的实现
热替换是指在程序运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。类似计算机外设那样,接上鼠标,U盘不用重启机器就能立即使用。
两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类完全不同的。 可以利用这个特点,来模拟实现热替换。
static void hotswap()throws Exception{
while(true) {
//每一次新的OrderClassLoader,加载同一份字节码都是不同的对象。
OrderClassLoader loader = new OrderClassLoader("D:\\tmp\\clz");
Class clazz = loader.loadClass("cn.jhs.chap10.HelloLoader");
Object obj = clazz.newInstance();
Method m = clazz.getMethod("print", new Class[0]);
m.invoke(obj, new Object[0]);
Thread.sleep(1000);
}
}