首先,本文并不是一篇入门的有关类加载的科普文章,相关类加载入门文章:
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
上文较为深入的解释了类加载的相关概念和API,但没有解决的问题如下:
1.整个java程序的类加载是如何启动的,哪些地方触发了类加载
2.对于加载进来的类,如何判断谁是该类的真正类加载器(就像双亲委派模型一样的那种机制)
3.某个方法运行时的类加载过程是怎样的
4.tomcat中类加载的使用(较为正统),他到底实现了怎样的类加载机制,如何兑现他所说的“类库隔离”的效果
解释:
1.先附上一段代码:
1>.Main
package loader;/*
*@author:wukang
*/
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
MyLoader loader=new MyLoader();
Class<Student> studentClass= (Class<Student>) loader.loadClass("loader.Student");
/*
Student student=studentClass.newInstance();
student.setName("wukang");
*/
Object student=studentClass.newInstance();
Method setMethod=studentClass.getMethod("setName", String.class);
String myname="wukang";
setMethod.invoke(student,myname);
Student student1=new Student();
student1.check(loader);
Method checkMethod=studentClass.getMethod("check", MyLoader.class);
checkMethod.invoke(student,loader);
System.out.println("-------------------------");
System.out.println(Main.class.getClassLoader());
System.out.println(myname.getClass().getClassLoader());
System.out.println(setMethod.getClass().getClassLoader());
System.out.println(student.getClass().getClassLoader());
}
}
2>.MyLoader:
package loader;/*
*@author:wukang
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class MyLoader extends ClassLoader{
static String BASE="F:\\study\\algrithem\\target\\classes\\";
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.startsWith("java.")){
System.out.println(name);
return super.loadClass(name);
}
Class<?> clazz=findLoadedClass(name);
if(clazz!=null){
return clazz;
}
String[] className=name.split("\\.");
String fileLocation=BASE+className[0]+"\\"+className[1]+".class";
FileInputStream inputStream;
try {
inputStream=new FileInputStream(new File(fileLocation));
int length=inputStream.available();
byte[] bytes=new byte[length];
inputStream.read(bytes);
inputStream.close();
return defineClass(name,bytes,0,length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
}
3>.简单的一个类:
package loader;/*
*@author:wukang
*/
public class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student check(MyLoader loader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return (Student) loader.loadClass("loader.Student").newInstance();
}
}
不管你是纯手工调用java命令,还是使用IDE中一键启动按钮,最终调用的都是jdk提供的java命令,命令参数包括你的Main类(或者符合格式的jar包),命令行参数,用到的其他的类的路径;然后此时就会启动jvm,他会预先将启动类加载器(c++)实现创建好,并设置一些访问规则(比如阻止自定义的类加载器去主动加载核心的类,如java.* sun.*等等,有鉴于此,对于jdk公开的类库,往往都会委托到上层加载,而不是自己去加载,否则会有SecurityException之类的异常),加载rt.jar中的所有的核心类,然后创建扩展类加载器,去加载扩展库目录的里面的类目(有关细节暂且不提)。接着会创建系统类加载器,去加载你类路径(通过java命令传入的)下的类。在真正进行加载你的Main方法所在的类之前,jvm已经创建好了这些,并且已经加载了很多核心类目。所有准备工作完成之后,会执行Main.main(args); jvm中有几条字节码指令会触发或者检查类加载,如new、getstatic、putstatic、invokestatic checkcast。注意是先执行方法,发现类还没有被加载,去加载类。由于Main类是第一层被调用的用户代码,将由系统类加载器去加载,这个类加载器在ClassLoader中有体现,遵循双亲委派机制,最终还是由他去加载Main类。反映在类库中,就是调用自己的findClass,defineClass。z最终在方法区中创建一个Main的class对象并返回。创建过程也很有意思。第一,首先检查Main是否有父类未加载,递归加载父类,此时会创建一条继承链,有继承链存在的Class对象的对应对象实例可以互相转化。 此处并不要求子父类的类加载器是相同的。 这也是一个常见误区。之前我认为子 父 类的类加载器也必须是一个,否则会发生ClassCastException。构造继承链也很简单,每个对象在内存中都有其对应的内存表示,其中有一个指针记录着当前类的直接父类的符号引用,类加载的连接阶段会将其转化为实际引用。一直加载到Object类,当前由于是系统类加载器,会先检查父类加载器是否加载过Object,并返回其Class引用。至此Main类加载完毕。Object类是Main类的父类,但是其类加载器却并不是一个,但是两个类之间存在继承关系链,因此能够相互转化(转化时依据当前上下文环境会产生checkcast指令)。现在聪明的读者可能会问到,如何判断某个类是被那个类加载器加载的,尤其是在双亲委派模型下,LoaderA.loadClass(clazz),其内部又委托其他Loader去加载,就好像是说 我给你钱让你帮我买个苹果,这个苹果到底是谁买的一样。问题的答案是,哪个类加载器最终显式调用了defineClass方法(该方法会调用一个defineClass0的native方法),该类就是被其所加载的,而不是loadClass,findClass等等。关于这一点,读者可以查看jdk源码和其注释,并使用简单的代码测试一下。
首先描述下方法的运行时数据结构-栈帧。某个方法的选择(暂不涉及执行),会考虑其方法名和方法参数类型,此时的类型会触发检查,但只是简单的定义检查(静态的,编译期完成,仅检查方法的类型名是否相同,不检查实际类加载器,这种的方法选择过程称之为静态分派调用),方法的参数和方法的局部变量都会被存取到栈帧的局部变量表中,与方法内部的局部变量的类加载规则相同。对于方法中的代码,要么是1.单纯的进行方法调用,要么是2.基础变量int,bool等的基础操作,要么是3.进行变量赋值(纯粹两个变量的赋值和将某个方法返回值直接赋值给某个变量),对于1.将会递归的适用此规则,对于2.不做讨论,基础变量的操作不涉及类加载,装箱拆箱已经特殊处理过,在此不进行讨论。3.是我们需要分析的。对于一个变量的赋值时原理上都需要作一次类型检查,但是有部分编译期能通过类型推断得出的,运行时不会做进一步检查,比如Student a =new Student(); Student s=a; 对于尤其涉及到类加载的,或者多方法调用将返回值赋值给某个变量的,将会插入一条checkcast字节码指令(该指令仅进行类型检查,不涉及类加载;当前仅当有前述四条指令的时候才会触发相应的类加载)。该指令首先检查当前方法的调用者所在类的类加载器是否加载过该类,如果已经加载过,返回该Class引用与=右侧的Class引用进行比较,看其是否存在一条继承链关系,若成功,则能进行赋值,否则将抛出ClassCastException.若该类未被加载过,直接抛出ClassCastException,此处的加载并非是指最终确实被当前方法调用者类的类加载加载(通过defineClass),而是以它为查润入口,最终返回一个Class引用(不管是否被他真正加载还是委托其它类加载器加载,查询阶段只是返回Class引用而已);
对于
Class<?> studentClass= loader.loadClass("loader.Student");
这句话解释起来可能稍微有点绕,jvm内的内存布局大致如此:
在方法区中存在一个总的Class对象,其他特定的clazz,比方说Student类的Class对象是studentClass,studentClass这个类对象的类是Class,是Class类的一个实例,而最高层的Class对象一样是Class的一个实例,自己指向自己。在将某个class的字节码导入jvm实例化成对应的class对象时,会将其与Class对象构造一条继承链。最高层的Class类也是Object类的子类。但是实例化Obeject之前你却需要先有一个类型,这造成了是先有鸡还是先有蛋的问题。在此我们不讨论之。关于这段说法,可以简单的如下检验:
System.out.println(studentClass); System.out.println(studentClass.getClass()); System.out.println(studentClass.getClass().getClass());
最终输出是 Student ,Class ,Class
第四个问题:读者应当了解过tomcat的类加载机制,并对servlet有一定的了解。
其实例化过程依次是
1.StandardWrapperValve:
2.使用InstanceManager进行实例化:
3.DefaultInstanceManager实现:
此处的classLoader即是心心念的WebAppClassLoader。它的类继承关系如下:
在WebappClassLoaderBase中对loadClass进行了重写,其关键loadClass逻辑(每一步都很关键!代码过长,我就不贴图了,只是摘抄其注释):
(0) Check our previously loaded local class cache
※(0.2) Try loading the class with the system class loader, to prevent the webapp from overriding J2SE classes
(0.5) Permission to access this class when using a SecurityManager
(1) Delegate to our parent if requested
(2) Search local repositories
(3) Delegate to parent unconditionally
注意 此处的加载逻辑最简化之后是这样:
Servlet s =clazz.newInstance();
其中Servlet类是javax.开头的,具体servletClass是我们自定义的,读者可以按照之前的分析,分析一下这句话怎么执行的。
对于一些公用的类,比如承接所有文件请求的DefaultServlet,是由上层类加载器完成的,每个jsp文件单独有一个特定的loader去加载(此处加载最终指的是defineClass由特定loader执行。从而实现热替换,不影响其他的jsp和容器),每个应用(Context)级别都有一个特定的webapploader去加载我们的特定的servlet类和类库,而不是简单的调用下loadeClass就可以的。这样是真正的类库隔离,不同的context下的类库真的没办法一起共用(1.有加载路径检查2.就算加载出来也没法赋值)。
抛开tomcat,设想一下怎么实现单个servlet级别(注意,servlet最初设计时是要求他是无状态的)的热替换(不单是加载新的,而且还得把旧的最终卸载掉,否则就会内存泄露了)。由此可以引申到微服务单个service怎么实现热替换(这个经常用到)
考虑的问题主要有:
1.session的影响(1>.信息同步,2>.session中的对象可能会持有当前servlet的引用,下次再调的时候还会出现问题,这不是实现问题,而是一个代码设计问题)
2..何时能够替换掉原先的servlet
3.旧的servlet怎么被替换掉
4.servlet内部联调其他servlet
未完待叙