概述
Java
文件最终会被编译成Class
文件,Class
文件最终需要加载到JVM
中才能运行和使用,虚拟机把描述类的Class
文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被直接使用的Java
类型,这就是虚拟机的类加载机制
在java的语言里,类的加载,链接,初始化
,都是在运行期间进行的,这样虽然会增加一些性能开销,但是会让Java
程序更加的灵活,Java
天生可以动态扩展语言的特性就是依赖运行期动态加载和动态链接来实现的
类的加载时机
类被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期包括,加载,验证,准备,解析,初始化,使用,和卸载
七个阶段,其中验证,准备,解析
3个部分统称为连接
其中加载,验证,准备,初始化,卸载这5个阶段的开始顺序是确定的,类加载过程必须按照这种顺序按部就班的开始
,而解析则不确定,他某些情况下可以在初始化之后开始,注意这里我们说的是开始
,而不是进行或完成,强调这一点是因为这些阶段是交叉混合的完成,通常在一个阶段进行过程中激活另一个阶段
类的加载过程
下面我们全面了解一下类加载的全过程
加载
加载
是类加载
的一个阶段,不要混淆这俩个概念
加载阶段主要完成了下面三个事情
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
- 在内存中生成一个java.lang.class对象,作为方法区这个类的各个数据的访问入口
这三个定义不算具体,第一个通过一个类的全限定名获取定义此类的二进制字节流
,没有指定从哪里获取,怎样获取,这样就给了开发人员很大的灵活性,比如
- 我们可以从ZIP中获取,比如jar
- 我们可以从网络中获取
- 运行时计算生成,这种场景运用最多的是动态代理技术
- ...
加载阶段完成后,虚拟机外部的二进制字节流,就按照虚拟机所需的格式存入到了方法区之中,然后实例化一个java.lang.class对象,虽然他是对象,但是他存在方法区里面
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机的安全
准备
准备阶段是正式为类变量(static变量)分配内存并设置初始值的阶段,这些变量所使用的内存,将在方法区分配,这里我们重点说一下初始值,比如下面这个变量
public static int value=123;
这个阶段为他赋值为0,而不是123,因为这时尚未执行任何java方法,把变量赋值为123,是在初始化阶段才会执行,下面列出,java基本数据类型的零值
解析
解析阶段是虚拟机把常量池内的符号替换为直接引用的过程
初始化
初始化是类加载的最后一步,前面的类加载过程中,除了加载阶段用户可以控制以外,其余动作都由虚拟机主导,到了初始化阶段,才是真正执行类中定义的java程序代码
关于类加载的初始化阶段,在虚拟机规范严格规定了有且只有5种场景必须对类进行初始化:
- 使用new关键字实例化对象时,读取或者设置一个静态字段(不包括编译期常量),以及调用静态方法的时候,必须触发类加载的初始化过程(类加载的最终阶段)
- 使用反射对类进行调用的时候,如果类还没初始化,那么就要初始化
- 当初始化一个类的时候,如果其父类还没初始化,那么则先触发父类的初始化
- 当java虚拟机启动时,用户需要制定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类
- 当使用JDK 1.7 的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic
的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化(这点看不懂就算了,这是1.7的新增的动态语言支持,其关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,这是一个比较大点的话题,这里暂且打住)
类与类加载器
虚拟机把类加载阶段的通过一个类的全限定名获取定义此类的二进制字节流
,这个动作放到java虚拟机外部去实现,以便让用户来决定如何去获取需要的类。实现这个动作的代码块叫做类加载器
类加载器虽然最用于类的加载阶段,但是他在java程序起到的作用不限类的加载阶段,比如,如何判断俩个类相等
,只有俩个类是被同一个加载器加载的前提下才有意义,否则即使俩个类源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,俩个类必定不相等
这里说的相等,包括类的Class
对象equals方法
,也包括Instance of
关键字判断
双亲委派模型
从java虚拟机的角度来讲,只存在俩种不同的类加载器
- 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现,是虚拟机自身的一本分,
- 另一种是所有其他的类加载器,这些类加载器由java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader
从java开发人员来看,类加载器还可以划分更为细致一些,绝大部分都会用到以下3种
Bootstrap ClassLoader(启动列加载器)
这个上面已经介绍过了,这个类加载器负责将放在<JAVA_HOME>\lib
目录中的或者被-Xbootclasspath
参数所指定路径中的,并且被虚拟机识别的类库
加载到虚拟机内存中
Java虚拟机启动就是通过启动类加载器创建一个初始类完成的,由于这个加载器是C++实现的,所以该加载器不能被java代码访问
Extension ClassLoader(扩展类加载器)
他负责加载<JAVA_HOME>\lib\ext
目录中的文件,或者被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器,java
中的实现类ExtClassLoader
Application ClassLoader(应用程序类加载器)
由于这个类类加载器是,ClassLoad中的getSystemClassLoader方法的返回值,所以又称为系统类加载器,他负责加载用户路径(classpath)所指定的类库,开发者可以直接使用,如果开发者没有自定义ClassLoader,这个就是程序的默认类加载器,java
中的实现类AppClassLoader
下图中展示的类加载器的层次关系,被称为双亲委派模型,双亲委派模型要求除了顶层的启动类加载器外,其他应当有自己的父加载器,这里的加载器如父子关系一般,不是以继承实现,而是以组合实现
双亲委派模型的工作过程
如果一个加载器收到了类加载的请求,他首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器完成,每一个层次的加载器都是如此,因此所有的加载请求,都应该传入到顶层的启动加载器中,只有父类加载器反馈,无法完成这个加载请求,自加载器才会尝试自己去加载
双亲委托模型的好处
- 避免重复加载,如果已经加载,就不需要再次加载
- 安全,如果你定义
String
类来代替系统的String
类,这样会导致风险,但是在双亲委托模型中,String
类在java
虚拟机启动时就被加载了,你自定义的String
类是不会被加载的
双亲委托模型的实现源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
// 首先检查类是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类抛出ClassNotFoundException异常
//则说明父类不能加载该类
}
if (c == null) {
//如果父类无法加载,则调用自身的findClass进行加
c = findClass(name);
}
}
return c;
}
上方逻辑很清楚,首先检查类是否被加载过,如果没有被加载过,就调用父类的加载器加载,如果父类加载器为空就调用启动加载器加载,如果父类加载失败,就调用自己的findClass
加载
Java中的ClassLoader
上面说了ClassLoader有三种类型,但是系统提供的ClassLoader不止3个,上方已经说了,这里的加载器如父子关系一般,不是以继承实现,而是以组合实现
,所以不要混淆,ClassLoader的继承关系如下:
下面分别对这5种ClassLoader介绍:
- ClassLoader:是一个抽象类,定义了
ClassLoader
的主要功能 - SecureClassLoader:继承了抽象类
ClassLoader
,但是SecureClassLoader
不是ClassLoader
的实现类,而是扩展ClassLoader
类加入了权限方面的功能,加强了ClassLoader
的安全性 - UrlClassLoader:继承自
SecureClassLoader
,可以通过Url路径从jar文件和文件夹中加载类和资源 - ExtClassLoader和AppClassLoader:都继承自
URLClassLoader
,他们都是Launcher
的内部类,Launcher
是java虚拟机的入口应用,ExtClassLoader
和AppClassLoader
是在Launcher
中初始化的
自定义ClassLoader
系统提供的类加载器只能加载指定目录下的类,如果想要加载网络上或者磁盘上某一文件夹中的Class文件,需要自定义ClassLoader,实现ClassLoader需要两步
- 定义一个类并继承
ClassLoader
- 重写
findClass
方法,并且在findClass
方法中调用defineClass
方法
public class CustomClassLoader extends ClassLoader {
private String path;
public CustomClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz;
//注释1
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
} else {
//注释2
clazz = defineClass(name, data, 0, data.length);
}
return clazz;
}
private byte[] loadClassData(String name) {
String fileName = getFileName(name);
File file = new File(path,fileName);
InputStream in=null;
ByteArrayOutputStream out=null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length=0;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
return out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(in!=null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try{
if(out!=null) {
out.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
return null;
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){//如果没有找到'.'则直接在末尾添加.class
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
上方就是自定义的ClassLoader,我们分析一下:
- 注释1:loadClassData方法会获得class文件的字节码数组
- 注释2:defineClass方法把class字节码数组转化为Class类的实例
下面我们验证一下是否可用
在你的磁盘上定义一个Hello.java
文件
package com.baidu.bpit.aibaidu.lib;
public class Hello {
public void say() {
System.out.println("helloword");
}
}
然后用命令行进入你的磁盘,执行javac Hello.java,就会生成Hello.class文件,接下来在AS中创建一个Java Library,进行测试
public class MyClass {
public static void main(String[] args) {
//注释1
CustomClassLoader customClassLoader = new CustomClassLoader("/Users/v_renxiaohui01/Desktop");
try {
//注释2
Class<?> aClass = customClassLoader.findClass("com.baidu.bpit.aibaidu.lib.Hello");
if (aClass != null) {
try {
Object o = aClass.newInstance();
System.out.println(o.getClass().getClassLoader());
Method say = aClass.getDeclaredMethod("say", null);
//注释3
say.invoke(o, null);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
- 注释1:传入要加载类的路径
- 注释2:加载
class
文件 - 注释3:通过反射调用
Hello的say
方法
看下日志
com.baidu.bpit.aibaidu.lib.CustomClassLoader@677327b6
helloword
参考:《深入理解Java虚拟机》