类加载机制

目的

Java 通过Javac编译之后是字节码文件。当我们启动虚拟的时候。虚拟机会将我们的字节码文件加载进内存,转化成JVM约定的格式,最终生成Class对象供我们使用。

步骤

加载
验证
准备
解析
初始化
使用
卸载
加载
  1. 根据全限定名获取类的二进制流。
  2. 将类的二进制的代表的静态数据结构转化成方法区的动态数据结构。
  3. 在内存中生个一个指向这个类的java.lang.Class对象,作为方法区这个类的访问入口。
验证
  1. 文件格式校验。保证能被正确解析并存储于方法区。例如魔数、主次版本号等。
  2. 元数据验证。保证符合Java语言规范。是否继承了final类、抽象类未实现等。
  3. 字节码验证。通过数据流和控制流的分析,保证语义合法符合逻辑。比如类型赋值是否正确、类型转换是否合理
  4. 符号引用验证。确保类的解析正常。比如根据字符传全限定名能否找到对应类、可访问性校验等。
准备

为类中定义的变量分配内存并设置类变量初始值。

解析

将常量池中的符号引用替换成直接引用的过程。例如比如代码中引用一个类或者接口,将类名替换成对类的直接引用。

初始化

执行静态代码块,静态变量赋值。初始化是执行类构造器<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没有父加载器外,其余类都有自己父加载器。如果一个类收到了加载请求,它会把这个请求委托给父类去加载,每一层都会向上委托,最终传递到最顶层加载器中,只有当父类加载器无法完成加载请求,子加载器才会尝试自己处理。

启动类加载器
扩展类加载器
应用类加载器
自定义类加载器
自定义类加载器
为什么这么做
  1. 保证优先由JVM去加载类,可以保证支撑Java基础体系的类都是由JVM加载,保证安全性
  2. 避免类的重复加载。
破坏双亲委派模型

双亲委派机制并不是一个强制性约束,是Java设计者推荐给开发者的使用类加载器的方式。破环双亲机制,就是通过自定义类加载器,重写loadClass模板方法,不用优先去父类加载(看实现)。

实现

基于jdk1.8
Java提供了一个ClassLoader抽象模板类,虚拟机通过对ClassLoader的扩展,组成Jvm的类加载机制。在Jvm中ExClassLoader,AppClassLoader都最终继承于ClassLoader.

ExtClassLoader
AppClassLoader
URLClassLoader
SecureClassLoader
ClassLoader

ClassLoader中有三个重点方法:

  1. public Class<?> loadClass(String name) throws ClassNotFoundException
  2. protected Class<?> findClass(String name) throws ClassNotFoundException
  3. protected final Class<?> defineClass(String name, ByteBuffer b,
    CodeSource cs)
执行过程
loadClass
findClass
defineClass

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中,类加载如下:
启动类加载器
扩展类加载器
应用程序类加载器
Common类加载器
Catalina类加载器
Shared类加载器
WebApp类加载器
Jsp类加载器
  • 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的类加载器
  • 否则,类查找失败

参考文献

  1. 深入理解JVM虚拟机 第三版
  2. JAVA源码
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值