一个java类的main方法是如何执行的?第一步就是通过类加载器将其加载到JVM中。
类加载过程:加载 > 验证 > 准备 > 解析 > 初始化 > 使用 > 卸载
加载:通过IO读入字节码文件,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:验证字节码文件的正确性,比如验证这是不是一个class文件,格式正不正确。
准备:给静态变量分配内存和赋默认值。
解析:把一些静态方法替换为指针或者句柄等,是把符号引用替换为直接引用,称之为静态链接。
初始化:静态变量赋指定值,执行静态代码块。
public class DynamicLoad { static { System.out.println("测试使用时才会加载类"); } public static void main(String[] args) { new A(); B b = null; } } class A { static { System.out.println("加载A"); } public A() { System.out.println("初始化A"); } } class B { static { System.out.println("加载B"); } public B() { System.out.println("初始化B"); } }
输出结果:
测试使用时才会加载类
加载A
初始化A
类加载器:
- 引导类加载器,加载JRE的lib目录下的核心类库
- 扩展类加载器,加载JRE的lib目录下的ext中的拓展类库
- 应用程序类加载器,负责加载ClassPath路径下的类
- 自定义加载器:负责加载用户自定义路径下的类
打印一下某个加载器能加载到哪些类:
# 引导类加载器加载文件 URL[] urls = Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i]); } System.out.println("********************"); # 扩展类加载器加载文件 System.out.println(System.getProperty("java.ext.dirs")); System.out.println("********************"); # 应用程序类加载器加载文件 System.out.println(System.getProperty("java.class.path"));
类加载器都是由com.misc.Launcher这个类负责创建的,可以看下这个类的构造方法:
// 这个类是单例的 private static Launcher launcher = new Launcher(); public Launcher() { Launcher.ExtClassLoader var1; try { // 引导类加载器,其父加载器设置为null,因为父加载器是引导类加载器,这是虚拟机处理的,不属于java的范畴 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { // 应用程序类加载器,父加载器设置为扩展类加载器 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } }
}
从上面构造方法,了解到了应用程序类加载器的父加载器是扩展类加载器,这其实是为了双亲委派的加载模式,加载某个类时,会先委托父加载器加载,如果父加载器在类加载路径下都加载不到,会再返回给自己的类加载器加载。
AppClassLoader的顶级父类是ClassLoader,在它的loadClass方法中实现了双亲委派模式:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // 检查当前类加载器是否已经加载了当前类 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 判断父加载器是否为空,不为空的话向上委派 if (parent != null) { c = parent.loadClass(name, false); } else { // 父加载器为空,交给引导类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // class还未被加载,调用findClass加载类 long t1 = System.nanoTime(); 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(); } } if (resolve) { resolveClass(c); } return c; } }
设计双亲委派的意义在于:避免类库被随意加载,比如java.lang.Integer类只能由引导类加载器加载,如果任何一个加载器都能加载的话,不就可以篡改了么。第二点是避免重复加载。
一个类被加载之后,它所依赖的类也会被当前类的类加载器所加载,除非显示的指定使用其他的类加载器。
下面自定义一个类加载器:
// 定义一个User类,之后通过自定义类加载器进行加载 public class User { private String id; private String name; public void print() { System.out.println("CustomClassLoader user"); } }
要自定义类加载器,需要继承ClassLoader,重写findClass即可
public class CustomClassLoader extends ClassLoader { String classPath; public CustomClassLoader(String classPath) { this.classPath = classPath; } /** * 读取class文件,将文件读取为字节数组 * * @param name * @return * @throws Exception */ private byte[] loadByte(String name) throws Exception { // 包名替换成路径 name = name.replaceAll("\.", "/"); // 获取class文件路径 FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } /** * 重写findClass,将字节数组转为Class对象,只要调用ClassLoader的defineClass方法即可 * * @param name * @return * @throws ClassNotFoundException */ protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } public static void main(String[] args) throws Exception { CustomClassLoader customClassLoader = new CustomClassLoader("/Users/project/JVM/src/main/java"); Class<?> clazz = customClassLoader.findClass("com.dluo.loader.User"); Object instance = clazz.newInstance(); Method print = clazz.getDeclaredMethod("print"); print.invoke(instance, null); System.out.println(instance.getClass().getClassLoader().getClass()); } }
看输出结果,成功执行了User的print方法,打印类加载器的类名,是我们自定义的类加载器
CustomClassLoader user class com.dluo.loader.CustomClassLoader
常规场景下类加载机制都会使用双亲委派模式,但有些情况下双亲委派会产生限制。比如我们使用tomcat时,它可能会部署多个程序,不同的程序可能使用不同的类库版本,比如一个项目是spring4,而另一个项目是spring5,这样就不能要求一个类只能有一份了,需要使其隔离。所以,从这个角度出发,tomcat类加载器,至少要实现多个版本的类库实现。再拓展来说,同一个容器中的类库应实现共享,要不就会重复,容器自己依赖的类库和应用程序的类库也应该隔离。还有一点值得注意,jsp文件需要编译成class文件运行,而且jsp经常要修改,应该保证能动态更新,所以每个jsp应该都是独一无二的类加载器,加载完即卸载,更新时再重新创建类加载器加载jsp文件。综上,tomcat这种类加载的方式一定违背了双亲委派模式。
重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { long t1 = System.nanoTime(); // 非自定义的类还是走双亲委派加载 if (!name.startsWith("com.dluo.loader")) { c = this.getParent().loadClass(name); } else { c = findClass(name); } sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } }
现在测试一下,看一下能不能加载两个User类。
创建两个目录:
mkdir -p temp1/com/dluo/loader
mkdir -p temp2/com/dluo/loader
将User的print方法输出不同的内容,分别编译为.class文件,copy到对应的temp文件中测试使用
public static void main(String[] args) throws Exception { CustomClassLoader customClassLoader1 = new CustomClassLoader("/Users/dluo/temp1"); Class<?> clazz1 = customClassLoader1.loadClass("com.dluo.loader.User"); Object instance1 = clazz1.newInstance(); Method method1 = clazz1.getDeclaredMethod("print"); method1.invoke(instance1, null); System.out.println(clazz1.getClassLoader()); CustomClassLoader customClassLoader2 = new CustomClassLoader("/Users/dluo/temp2"); Class<?> clazz2 = customClassLoader2.loadClass("com.dluo.loader.User"); Object instance2 = clazz2.newInstance(); Method method2 = clazz2.getDeclaredMethod("print"); method2.invoke(instance2, null); System.out.println(clazz2.getClassLoader()); }
输出结果:
版本一类加载器
com.dluo.loader.CustomClassLoader@58372a00
版本二类加载器
com.dluo.loader.CustomClassLoader@568db2f2
从这里可以看到,虽然都是使用CustomClassLoader,但是已经是不同的类加载器实例初始化的了,一个JVM程序中同时存在两个User类,打破了双亲委派加载模式。