类生命周期:
其中1到5这几个步骤属于类加载,而其中能够干预的只有第一步,其他四步都是jvm自动的,程序员无法干预。
类加载器
类加载器负责装入类,就像上面说的,实际上类加载器只能干预到第一步,他可以搜索网络、jar包、zip包、文件夹、二进制数据、内存指定位置,凡是可以存储的数据的地方,他都可以去加载。一个java运行是,至少有三个类加载实例,负责不同类型类的加载。
-
Bootstrap loader核心类库加载器:他是c或者c++实现的,没有对应的java类,他负责加载JRE_HOME/jre/lib目录或者用户配置的目录。由于没有对应的java类,所以打印这个加载器的类名是,输出为null
-
Extension Class Loader拓展类库加载器:ExtClassLoader的实例,他负责加载JRE_HOME/jre/lib/ext目录、jdk扩展包、用户配置的目录
-
application class loader用户应用程序加载器:AppClassLoader的实例,他负责加载java.class.path指定的目录,用户应用程序class-path 或者java命令运行时使用参数-cp。
public class ClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(String.class.getClassLoader());
System.out.println(ZipPath.class.getClassLoader());
System.out.println(ClassLoader.class.getClassLoader());
}
}
null
sun.misc.Launcher$ExtClassLoader@694f9431
sun.misc.Launcher$AppClassLoader@18b4aac2
动态加载
jvm是如何定一个唯一类的呢?需要同一个加载器+同一个类名。当一个加载器去加载一个类的时候,会判断这个类自己是否已经加载过,如果已经加载过,就不会重新加载了。
测试类:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException, InterruptedException, NoSuchMethodException, InvocationTargetException {
ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:C:\\Users\\xiaosong.yang\\Desktop\\")});
while (true) {
Class clazz = classLoader.loadClass("Test1");
Object classLoaderTest = clazz.newInstance();
clazz.getMethod("sayHello").invoke(classLoaderTest);
Thread.sleep(1000);
}
}
被加载的类
public class Test1 {
public void sayHello() {
System.out.println("xxxx");
}
}
这上面的代码,我在我的桌面上建了一个Test.java的类,然后通过javac编译成Test.class。然后在IDEA中通过这段代码进行加载,创建了一个加载器,然后不断地重新加载这个类,如果我们在测试程序运行过程中,修改被加载的类的输出,从"xxxx"改成"haha",然后编译,会发现这个类测试类中的输出还是没有变。如果我们把测试类中的加载器每次都创建一个新的加载器,如下:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException, InterruptedException, NoSuchMethodException, InvocationTargetException {
while (true) {
ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:C:\\Users\\xiaosong.yang\\Desktop\\")});
Class clazz = classLoader.loadClass("Test1");
Object classLoaderTest = clazz.newInstance();
clazz.getMethod("sayHello").invoke(classLoaderTest);
Thread.sleep(1000);
}
}
如图,把classLoader的创建,放到while循环内部,这样每次加载Test1的时候,都是一个新的加载器,然后我们再重复上面的操作,把"xxxx"改成"haha",然后编译,这样我们会发现,输出会自动变成新的。正如我们最上面说的一个加载器只会加载一个类一次。
双亲委派模型
双亲委派模型是java推荐,也是默认的一种类加载机制,如图所示,当你自己的加载器想加载一个类的时候,他会向上委托,让上层的加载器来加载,如果上次加载器找不到,再让下寻找,直到有找得到这个类的加载器来加载。比如我们上面所演示的动态加载的代码,如果我们将被加载类和测试类放在同一个项目中,那当UrlClassLoader加载器进行加载时,就会向上委托,到Bootstrap来加载,然后Bootstrap应该找不到这个类,再往下寻找,Extension加载器发现也找不到,然后再往下寻找,然后Application加载器发现可以加载,就会把Test类给加载了,然后我们循环去动态加载,其实是无效的。所以上面那个动态加载类,如果想要在一个应用中实现,仅仅像上面这么写是不行的,因为受到了双亲委派模型的限制,表面上看上去在不断地new新的加载器,实际上都不是你new的这个加载器加载的Test。
双亲委派模型能起到公用和隔离这两个作用。比如一个tomcat容器中,放了两个应用,A应用和B应用有各自属于自己独有的类,和加载器,这个时候可以通过这个模型,在最下层进行隔离,让他们互相不受影响。同样对于一些公共的类模块,这个模型又可以保证他们互相公用,节省内存空间。
双亲委派模型的打破
世事都不是绝对的,双亲委派模型也有他的缺点,所以在java历史上双亲委派模型被多次打破过。
远古时代的兼容
双亲委派模型是java1.2才有的,而类加载器和抽象类java.lang.ClassLoader则在java1.0时代就已经存在了,所以当java1.2出现双亲委派模型时,为了向前兼容,该模型不得不做出妥协。所以第一种打破模型的方式就是自定义一个ClassLoader,重写其中的findClass方法即可,这样就会走自己的类加载代码,不会向上委派。
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String path;
private String className;
public MyClassLoader(String path, String className) {
this.path = path;
this.className = className;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String filePath = path + name + ".class";
byte[] b = new byte[0];
try {
InputStream is = new FileInputStream(new File(filePath));
OutputStream os = new ByteArrayOutputStream();
b = new byte[1024];
int length = 0;
while ((length = is.read(b)) != -1) {
os.write(b, 0, length);
}
return defineClass(name, ((ByteArrayOutputStream) os).toByteArray(), 0, ((ByteArrayOutputStream) os).toByteArray().length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
由上向下的调用
双亲委派模型是自下而上的调用,但是也是存在自上而下的调用,比如JNDI、JDBC、JCE、JAXB等。为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置或者直接通过Thread.currentThread.getContextClassLoader来获取当前线程的加载器进行加载,而不是传递到底层去加载。如果创建线程时未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(也就是Application加载器)。
以JDBC为例,传统模式下我们利用jdbc链接数据库,都需要指明数据库的驱动包:
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");
或者这样:
System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");
但是在SPI机制下,我们不用再手动设置驱动包,
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");
DriverManager中会有一个静态代码块执行ServiceLoader.load(Class<S> service, ClassLoader loader)这个方法。但是ServiceLoader是Bootstrap loader加载器加载的,根据双亲委派模型,他是无法进行加载由服务商提供的驱动包,所以这个时候就通过setContextClassLoader()的方法,偷偷把服务商提供的类加载进来。
热部署需求
第三次双亲委派模型的打破是因为热部署(动态部署)的需求导致的。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。