1. 类加载开篇
1.在java代码中,类的加载、连接与初始化过程都是在程序运行期间完成的;
2.提供了更大的灵活性,增加了更多的可能性;
3.类加载器是沙箱的第一道防线,保护代码不被恶意干扰,保护已验证的类库,代码放入有不同行为的保护域。
2. 类加载过程
1.加载:查找并加载类的二进制文件;
2.连接
验证 : 确保被加载类的准确性(运行时环境与编译时环境是否一致;是否符合规范的class文件)
准备:为类的静态变量分配内存,并将其初始化为默认值;
解析:把类中的符号引用转为直接引用;
3.初始化:为类的静态变量赋予正确的初始值;
3. 类的使用方式
我们对类的使用方式可以分为两种:主动使用 和 被动使用
所有的java虚拟机实现必须在每个类或接口被java程序 首次主动使用 时才初始化他们。
4. 类的加载
类的加载是指将 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class 对象 (虚拟机规范并未指出Class对象要放在哪里)用来封装类在方法区内的数据结构;
加载class文件的方式:
---从本地系统中直接加载
---通过网络下载.calss文件
---从zip,jar等归档文件中加载.class文件
---将java源文件动态编译为.class文件
类加载的最终产品是位于内存中的Class对象;
Class对象封装了类在方法区内的数据结构,并且向我们提供了访问方法区内数据结构的接口;
5. 类的初始化
主动使用(7种)
---创建类的实例
---访问某个类或接口的静态变量,或者对该静态变量赋值
---调用类的静态方法
---反射(如 Class.forName("com.test.ClassName"))
---初始化一个类的子类
---java虚拟机启动时被表明为启动类的类
---jdk1.7以后提供的动态语言支持,MethodHandle实例解析结果 REF_getstatic, REF_putstatic,REF_inoveStatic句柄对应的类没有初始化,则初始化。
6. 类加载器
虚拟机自带了加载器
---根类加载器(BootStrapClassLoader)c++实现,负责加载/lib下的类,我们常用的rt.jar就是根加载器所加载的。可以通过系统属性查看所加载的jar。
System.getProperty("sun.boot.class.path")
-----------------------------------------
C:Program FilesJavajdk1.8.0_162jrelib
esources.jar;
C:Program FilesJavajdk1.8.0_162jrelib
t.jar;
C:Program FilesJavajdk1.8.0_162jrelibsunrsasign.jar;
C:Program FilesJavajdk1.8.0_162jrelibjsse.jar;
C:Program FilesJavajdk1.8.0_162jrelibjce.jar;
C:Program FilesJavajdk1.8.0_162jrelibcharsets.jar;
C:Program FilesJavajdk1.8.0_162jrelibjfr.jar;
C:Program FilesJavajdk1.8.0_162jreclasses
---扩展类加载器(ExtensionClassLoader) Java实现,可以在java里获取,负责加载/lib/ext下的类。可以通过系统属性获取信息。
System.getProperty("java.ext.dirs")
-----------------------------------------
C:Program FilesJavajdk1.8.0_162jrelibext;
C:windowsSunJavalibext
---系统(应用)类加载器(AppClassLoader)java实现,可以加载classpath下的java类。可以通过系统属性获得。
System.getProperty("java.class.path")
用户自定义的类加载器
---java.lang.ClassLoader的子类
---用户可以指定类的加载方式
Spring中的类加载器
tomcat中的类加载器
获取ClassLoader的途径
---clazz.getClassLoader();
---Thread.currentThread.getContxtClassLoader();//线程上下文类加载器
---ClassLoader.getSystemClassLoader();
---DriverManager.getCallerClassLoader();
类加载器的命名空间
---每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。
---在同一个命名空间中,不会出现类的完整名字相同的两个类
---在不同的命名空间中,有可能出现类的完整名字相同的两个类。
7. 双亲委派以及破坏双亲委派
用户自定义
用户自定义
启动类加载器 bootstrap classloader
扩展类加载器 extension classloader
应用类加载器 APP classloader
自定义classloader
loader1
loader2
---根加载器/启动类加载器在 java程序中用 null表示。
双亲委派机制
在绝大多数场景下,类的加载都遵循双亲委派机制,当一个类将要被加载的时候,当前类的加载器总要委派父加载器去加载,父加载器加载不到了,才会由自己去加载。在ClassLoader中的loadClass方法中可以看到。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
在一般的自定义类加载器中,jvm建议大家,去重写findClass方法,而非直接重写loadClass方法,等于我们的自定义类加载器在一般情况下也要遵守双亲委派机制。
破坏双亲委派
我们在自定义ClassLoader时,如果重写他的loadClass方法,就会对他原先的双亲委派机制进行破坏,我们可以通过自己扩展的方式去加载这个二进制流产生Class对象。
对于rt.jar已经加载过的类,他的实现又是第三方厂商实现的类,要怎么加载呢?这就是我们经常接触到的javaSPI机制(Service Provider Interface),Thread.currentThread().currentClassLoader()就是jdk给自己开的后门,对于这些个场景,会破坏自己的双亲委派机制,进行用线程上下文加载器进行加载。
例如java.sql.Driver,位于rt.jar下,由bootStrap加载器加载,就是一个典型的spi,会有不同的厂商去实现这个接口,而我们所引用的第三方实现包是位于classpath下的,当我们要加载时,使用双亲委派就不会起到作用了。所以他是通过线程上下文加载器去加载的。
ServiceClassLoader是现在对spi机制的一个通用的处理。他会找到所有classpath下"META-INF/services/"对将要加载的接口的全类路径名文件的查找,找到后将文件中的实现类通过线程上下文加载器全部加载。
// 规定前缀 service为要加载的class名字 loader为上下文加载器
private static final String PREFIX = "META-INF/services/"; String fullName = PREFIX + service.getName(); configs = loader.getResources(fullName); c = Class.forName(cn, false, loader);
spi机制的扩展,springboot中的自动装配机制,我们会自定义starter也会通过扫描的方式,被springFactoryLoader加载到bean容器中。这种思想与jdk有异曲同工之妙。
8. 类加载器命名空间的应用
---类之间的可见性问题
由不同类加载器加载的同一个类所产生的对象,相互是不可见的,我们用下面的例子来印证一下:
ClassLoader loader1 = new MyClassLoader();
ClassLoader loader2 = new MyClassLoader();
Class clazz1 = loader1.loadClass("com.test.User");
Class clazz2 = loader2.loadClass("com.test.User");
User user1 = clazz1.newInstance();
Object user2 = clazz2.newInstance();
user1.setUser(user2);
由于user对象被不同的类加载器所加载,loader1和loader不在同一个命名空间,所以,两次加载出来的user对象之间是不可见的。所以这两个对象在发生强制类型转换的时候,会抛出异常。
---类与类之间的隔离性
我们所熟知的tomcat中,我们可以再tomcat中部署多个web应用,那么如果部署的多个应用中有类冲突,jar版本不一致等问题,tomcat又是怎么做到类与类的隔离的,这就是上述的类加载器的命名空间起到的作用。具体的tomcat类加载器实现在上下文的帮助下,也比较好理解,有兴趣的可以自行深入。
9. 先有类加载器还是先有加载类加载器的类
说到这里我们可能会有些疑惑了,既然我们的类是由类加载器所加载得来的,那么我们的类加载器又是被谁加载的呢。说到这里,上文中的boostrapClassLoader在我们java程序中为什么是null的原因就可以得到解释了,boostrapClassLoader是由c++编写,所以在程序启动中,会自动加载,而bootstrapClassloader又可以加载rt.jar。在rt.jar中会有一个比较关键的类会被他加载,如下是com.misc.Launcher类反编译的源码
public class Launcher { private static URLStreamHandlerFactory factory = new Factory((1)null); private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); private ClassLoader loader; private static URLStreamHandler fileHandler; public Launcher() { ExtClassLoader var1; try { var1 = ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if (var2 != null) { SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager) this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { ; } catch (InstantiationException var6) { ; } catch (ClassNotFoundException var7) { ; } catch (ClassCastException var8) { ; } } else { var3 = new SecurityManager(); } if (var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } } }
launcher在初始化的时候,会加载ExtClassLoader和AppClassLoader,而且在加载AppClassLoader,会把上下文类加载器设置为AppClassLoader。所有先有类加载器和先有加载类加载器的问题在这里就得到了解释。
看到这里,一个类的前世今生想必大家心里都有一些深刻的过程了。
10. 一些关于类加载的参数
---想看到程序启动都加载了哪些类:-XX:+TraceClassLoading
---想看到程序执行会卸载哪些类 :-XX:+TraceClassUnloading