目的
Java 通过Javac编译之后是字节码文件。当我们启动虚拟的时候。虚拟机会将我们的字节码文件加载进内存,转化成JVM约定的格式,最终生成Class对象供我们使用。
步骤
加载
- 根据全限定名获取类的二进制流。
- 将类的二进制的代表的静态数据结构转化成方法区的动态数据结构。
- 在内存中生个一个指向这个类的java.lang.Class对象,作为方法区这个类的访问入口。
验证
- 文件格式校验。保证能被正确解析并存储于方法区。例如魔数、主次版本号等。
- 元数据验证。保证符合Java语言规范。是否继承了final类、抽象类未实现等。
- 字节码验证。通过数据流和控制流的分析,保证语义合法符合逻辑。比如类型赋值是否正确、类型转换是否合理
- 符号引用验证。确保类的解析正常。比如根据字符传全限定名能否找到对应类、可访问性校验等。
准备
为类中定义的变量分配内存并设置类变量初始值。
解析
将常量池中的符号引用替换成直接引用的过程。例如比如代码中引用一个类或者接口,将类名替换成对类的直接引用。
初始化
执行静态代码块,静态变量赋值。初始化是执行类构造器<client>()的过程。<client>()是javac编译的产物,它收集了所有的类变量的赋值动作和静态代码块。
类加载器
类加载阶段是通过一个全限定名获取描述该类的二进制字节流,这部分可以由应用程序自定义加载方式,来获取所需要的类。
在Java中,类在Java的唯一性是由类加载器与类全限定名共同确立。
默认类加载器(jdk8及之前版本)
- 启动类加载器(Bootstrap Class Loader):一般由C++实现,是虚拟机的一部分。加载<JAVA_HOME>\lib目录或者-Xbootclasspath指定目录下的且是能被jvm识别的类库。
- 扩展类加载器(Extension Class Loader):在Launcher$ExtClassLoader中以Java代码实现,主要加载<JAVA_HOME>\lib\exe目录或被java.ext.dirs指定的目录中的所有类库。
- 应用类加载器(Application Class Loader):在Launcher$AppClassLoader中以Java代码实现。主要加载classpath路径上的所有类库。
双亲委派模型
除了Bootstrap Class Loader没有父加载器外,其余类都有自己父加载器。如果一个类收到了加载请求,它会把这个请求委托给父类去加载,每一层都会向上委托,最终传递到最顶层加载器中,只有当父类加载器无法完成加载请求,子加载器才会尝试自己处理。
为什么这么做
- 保证优先由JVM去加载类,可以保证支撑Java基础体系的类都是由JVM加载,保证安全性
- 避免类的重复加载。
破坏双亲委派模型
双亲委派机制并不是一个强制性约束,是Java设计者推荐给开发者的使用类加载器的方式。破环双亲机制,就是通过自定义类加载器,重写loadClass模板方法,不用优先去父类加载(看实现)。
实现
基于jdk1.8
Java提供了一个ClassLoader抽象模板类,虚拟机通过对ClassLoader的扩展,组成Jvm的类加载机制。在Jvm中ExClassLoader,AppClassLoader都最终继承于ClassLoader.
ClassLoader中有三个重点方法:
- public Class<?> loadClass(String name) throws ClassNotFoundException
- protected Class<?> findClass(String name) throws ClassNotFoundException
- protected final Class<?> defineClass(String name, ByteBuffer b,
CodeSource cs)
执行过程
loadClass : 是Jvm发出加载后的处理过程。是双亲委托机制主要实现地方。
findClass: 是类加载器加载类的主要执行过程。
definedClass:使用来解析findClass类加载的二进制代码,生成Class对象
源码摘要
源码为了简洁,简化了逻辑,保证了大致流程。
loadClass
protected Class<?> loadClass(String name) {
synchronized (getClassLoadingLock(name)) {
//判断是否已经加载
Class<?> c = findLoadedClass(name);
if(c==null){
//如果有父类加载器,通过父类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 通过BootStrap进行加载
c = findBootstrapClassOrNull(name);
}
if (c == null) {
//调用本类加载器进行加载
c = findClass(name);
}
}
return c;
}
findClass
在ClassLoader中,没有实现findClass方法,是在URLClassLoader实现的:
protected Class<?> findClass(final String name) {
//类的路径进行替换
String path = name.replace('.', '/').concat(".class");
//ucp URLClassPath 从url中加载资源
Resource res = ucp.getResource(path, false);
return defineClass(name, res);
}
defineClass
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
{
ProtectionDomain 是对 CodeSource的封装。CodeSource是url和signers
//省略了不是直接内存读取的部门代码 !b.isDirect()
int len = b.remaining();
//1. class文件路径不能以java.开头 2. 判断签名证书(没有接触过,不清楚作用,可能和字节码加密有关)
protectionDomain = preDefineClass(name, protectionDomain);
//获取字节码文件路径
String source = defineClassSourceLocation(protectionDomain);
//通过navtive方法读取 生成Class
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
// 也是和签名,证书有关
postDefineClass(c, protectionDomain);
return c;
}
native方法defineClass2
Classloader.c
JNIEXPORT jclass JNICALL Java_java_lang_ClassLoader_defineClass2
jvm.cpp
static jclass jvm_define_class_common
systemDictionary.cpp
Klass* SystemDictionary::resolve_from_stream
classFileParser.cpp
instanceKlassHandle ClassFileParser::parseClassFile
实际案例
Tomcat部署多个应用如何隔离
在一个web容器中,可以部署多个web应用,但是每个应用中jar的依赖版本可能都不同。因此通过扩展类加载器来保证类库的隔离。
在web容器中,需要隔离的类库:
- 容器+所有应用都可以使用的类库。
- 只能容器使用的类库。
- 所有应用使用的类库。
- 只能单个应用使用的类库。
- Jsp文件
在Tomcat中,类加载如下:
- CommonClassLoader:加载/common目录下的类库,Tomcat和所有应用共享
- CatalinaClassLoader:加载/server目录下类库,只能tomcat使用,其它所有应用不可见
- SharedClassLoader:加载shared目录下类库,只能被所有应用使用,tomcat不可见。
- WebappClassLoader:加载器/WebApp/WEB-INF目录下类库,仅被应用使用。每个应用由一个WebappClassLoader实例
- JasperLoader:每一个jsp文件对应一个JasperLoader类加载器实例。当服务器检测到JSP文件修改,会替换掉JasperLoader的实例,重现新建一个类加载器来实现jsp热加载功能。
在Tomcat 6及之后,在catalina.properties中的server.loader和share.loader中主动开启,才会建立Catalina类加载器和Shared类加载器。
如果我们将Spring放到共享目录,它如何加载我们用户程序的类:使用线程上下文加载器。
OSGi 热部署关键实现
OSGi(Open service Gateway Initiative) 是一个基于Java语言的动态模块化规范。OSGI中的每个模块(Bundle)是一个jar,和我们平时使用jar的区别在于,它声明了自己依赖的Package和Class,同时也声明了它导出的Package。
OSGi可以基于Bundle维度的热插拔,主要归功于灵活的类加载器机制。它的类加载器查找规则如下:
- 以Java.*开头的类委托给父类加载器
- 否则,委派列表名单内的类,委派给父类加载器
- 否则,Import列表中的列,委托给Export这个类的Bundle的类加载器
- 否则,查找是否在自己的Fragment Bundle中,是委派给Fragment Bundle的类加载器
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器
- 否则,类查找失败
参考文献
- 深入理解JVM虚拟机 第三版
- JAVA源码
1484





