在初学java的时候,很多书和资料都会说java是一个跨平台的语言,是一个动态语言,可以在运行期间加载类。首先说,java是一个跨平台的语言,是因为它的两个关键点:
任何一个java文件都是先编译成 .class文件,然后再经过jvm解释成机器码。只要拥有.class文件和jvm,那么任何一个平台都可以运行。然后再看,java是一个动态语言,这是这篇的重点。
当java文件被编译成 .class文件之后,它是怎么被使用的呢?
在java.lang包中有一个抽象类,名为 ClassLoader ,这个类是所有类加载器的超类。这个类中有这几个重要方法:
从上面的说明可以看出来ClassLoader通过loadClass()方法来加载字节码文件,然后返回一个字节码对象。来看一下源码:
代码1:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
/*首先通过findLoadedClass方法检查这个类是否被加载,过程是去元空间中查看是否有当前类的信息,是否被加载需要检查 包名+类名+类加载器的id*/
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) {
// If still not found, then invoke findClass in order
/*查找这个类是否能被这个类加载器加载,不能的话就返回null,回到它的子加载器,递归回来,查看子加载器是否能被加载*/
long t1 = System.nanoTime();
c = findClass(name);
。。。。
}
}
if (resolve) {
/*如果这个类加载了,那么就链接它*/
resolveClass(c);
}
return c;
}
}
一些重要的点,都在代码中注释出来了。看完这段,是不是有一点还不清楚呢?这里好像只是讲怎么发现字节码文件,并没有加载类啊。这个后面讲,先来讲一下java最基础的类加载机制模型。
Java语言系统自带有三个类加载器:
怎么创建AppclassLoader
/*静态内部类继承了URLClassLoader*/
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
/*得到classpath下的所有类的路径*/
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
/*这里创建AppClassLoader,注意这里并没有去加载路径下的类*/
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
ExtentionClassLoader 类似于这种方式去创建,那BootStrapClassLoader是怎么创建的呢?
其实java中并没有这个类,只是一个概念,它嵌入到jvm中了,由c++编写的。也就是jvm一启动就会有这个类,但是在Launcher类中规定了它加载类所在的路径
private static String bootClassPath = System.getProperty("sun.boot.class.path");
从上面的代码也看到了 AppclassLoader 继承的是URLClassLoader,但它的父加载器却是ExtentionClassLoader ,采用这种方式是有两个方面:
protected Class> findClass(final String name)
throws ClassNotFoundException
{
。。。。
public Class> run() throws ClassNotFoundException {
/*解析类的路径*/
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
/*通过字节码文件的路径获得相应的Class类*/
return defineClass(name, res);
}
。。。。。
return result;
}
前面有个问题是只是找到字节码文件,并没有加载它,答案就在这里,通过defineClass(name, res)来加载它。这里又引出一个问题,为什么不在loadClass()中加载它,而是通过findClass()来加载?继续留问题,后面讲。
private final ClassLoader parent;
是通过类与类组合的方式来定义一个父加载器的,为了构成双亲委派模型。
那什么是双亲委派模型呢?先来看张图:
在对这个图回头看代码1,结合下面的例子来理解.
这里有一个Hello.java
public Class Hello {
public void test() {
String str="Hello";
}
}
当编译完成后,有了一个Hello.class的文件。现在要加载这个字节码文件:
这里回答一下,关于String类是否加载引起的思考,到底类加载是在什么时候呢? 资料上的回答是:什么情况下需要开始类加载过程的第一个阶段:”加载”。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握。 没有答案就自己找答案吧
public class StringDemo {
//空的
}
public class IntDemo {
private Integer a=3;
private Integer a1=3;
public static void main(String[] args){
IntDemo intDemo=new IntDemo();
// StringDemo stringDemo=new StringDemo();
}
}
在idea中加入jvm参数 -XX:+TraceClassLoading 追踪类加载的顺序。
第一次运行,StringDemo相关被注释掉:
[Loaded sun.launcher.LauncherHelper from D:\jdk\jre\lib\rt.jar]
[Loaded sun.misc.URLClassPath FileLoader 1 from D:\jdk\jre\lib\rt.jar]
[Loaded java.net.Inet6Address from D:\jdk\jre\lib\rt.jar]
[Loaded IntDemo from file:/F:/code/JVM/out/production/JavaString/]
[Loaded java.net.URI from D:\jdk\jre\lib\rt.jar]
[Loaded sun.launcher.LauncherHelper$FXHelper from D:\jdk\jre\lib\rt.jar]
[Loaded java.lang.Void from D:\jdk\jre\lib\rt.jar]
第二次运行,去掉注释:
[Loaded sun.misc.URLClassPath FileLoader 1 from D:\jdk\jre\lib\rt.jar]
[Loaded IntDemo from file:/F:/code/JVM/out/production/JavaString/]
[Loaded sun.launcher.LauncherHelper$FXHelper from D:\jdk\jre\lib\rt.jar]
[Loaded java.lang.Void from D:\jdk\jre\lib\rt.jar]
[Loaded StringDemo from file:/F:/code/JVM/out/production/JavaString/]
两段信息对比可以看出两点:
那么为什么要设计双亲委派模型呢?
假设,整个系统中没有这个设计,还是上面的程序,没有双亲委派模型,那么String这个类就由AppclassLoader加载了,然后可能在有另一个自定义的加载器加载String这个类了,那么程序就乱套了!!到处都是真假难辨的String。而有了双亲委派模型,不管是哪个加载器加载的String,这个类都是由BootStrapClassLoader加载。双亲委派模型最主要的作用是保证java核心类的安全性。
注意:一个类可以被多次加载,但是一个加载器只能加载一次,而且判断一个类是不是相同的,是比较 包名+类名+类加载器id
当某个classloader加载的所有类实例化的对象都被GC回收了,那么这个加载器就会被回收掉。
首先,明确双亲委派模型是一种规范,在自定义类加载器的时候完全可以重写loadClass()方法中的逻辑。这里回答一下前面的问题,为什么不用loadClass()实现类加载的功能,而是用findClass()。这是为了把委派模型的逻辑和类加载器要实现的逻辑分离开了。所以一般自定义类加载器loadClass()一般不动,而是重写findClass()。
什么时候要破坏委派模型呢?
来看一个例子java需要连接数据库,但是数据库的品种这么多,每家厂商写的程序都不同,为了让每个厂商统一,java制定了一系列规范的接口,放在BootStrapClassLoader可以加载的路径中。所有厂商只要按照这些接口规范写就好了,并且统一有一个管理的类在DriverManager。这样java制定者和厂商都开开心心,这时候出现了一个问题。
根据委派模型,A类中引用了B类
public class A{
private B = new B();
}
那么默认的B的加载器也是由A的加载器加载的!!
所以通过DriverManager统一来得到Driver的话,那么BootStrapClassLoader默认是加载 java.sql 包下的Driver接口。但实际上必须要加载它的实现类。
可是,根据委派模型,父类加载器去调用子类加载器是不可能完成的
必须由BootStrapClassLoader来加载,这就难办了,BootStrapClassLoader说我的工作就是加载 %JRE_HOME%\lib 下的,要我加工作量,不干!!况且我也找不到在哪啊,我只知道它的接口啊,并不知道它的实现类
于是,出现了一个设计,在线程Tread类中内置一个
/* The context ClassLoader for this thread 默认是appclassloader ,可以自己设置*/
private ClassLoader contextClassLoader;
因此在父类加载器需要调用子类加载器的时候,就可以通过
Thread.currentThread().getContextClassLoader();
来获取想要的类加载器。
下面来看JDBC的源码:
在java.sql.DriverManager中
//从Thread.currentThread中取出appclassloader
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
是怎么使用这个classLoader的呢?
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class> aClass = null;
try {
//是通过Class.forName()并指定类加载器。
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
}
。。。。
return result;
}
java web中的热部署,指的是热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。在编写java程序的时候,每一次改动源代码,就必须重启一次应用程序,那java的这个缺陷是由什么导致的呢?
然而热部署对于开发来说实在太重要了,想想当你调试js代码的时候,一个屏幕源代码,一个屏幕浏览器,随时随地的观察代码带来的变化。而java对于这一点,由于它语言的特性导致很难做到这一点。
如果想要实现热部署,需要做哪些工作呢?
第一步销毁ClassLoader,如果要做到这一步,那么这个类文件一定不能是appclassloader加载的,因此要自定义ClassLoader。
第二步,要做到的话,首先必须有一个监听器去监听类发生变化,然后才能相应的创建ClassLoader去加载。
这也是Tomcat服务器在处理类加载的时候进行的做法,每一个web应用都有一个相应的类加载器。原理图如下:
并且Tomcat自定义的类加载器并不遵循双亲委派模型,而是先检查自身可不可以加载这个类,而不是先委派。前面也提到了,BootStrap,System和Common 这三个类加载器遵守委派模型同时加载java以及tomcat本身的核心类库。这样做的好处是更好的实现了应用的隔离,但是坏处就是加大了内存浪费,同样的类库要在不同的app中都要加载一份。(当然可以配置使得不是所有的app都要被加载,默认是全部加载)
Tomcat中当类发生改变时,监听器监听到触发StandardContext.reload(),然后销毁以前的类加载器,重新创造一个类加载器。
在使用idea开发的时候,可以在debug模式下,配置下图两个地方:
就可以实现方法层面的修改,即你修改了方法中的代码,不需要tomcat重新发布,也不需要重启服务器。就可以实现热部署了。
这是因为jdk1.6增加了agentmain方式,实现了运行时动态性(通过The Attach API 绑定到具体VM)。其基本实现是通过JVMTI的retransformClass/redefineClass进行method body级的字节码更新,ASM、CGLib之类基本都是围绕这些在做动态性。