一直想学习java虚拟机类加载机制,废话不多说,正片走起。
类加载机制:JVM将class文件加载到内存,并对数据进行校验,解析和初始化最终形成JVM可直接使用的java类型的过程。
1.类的生命周期
类从被加载到虚拟机内存到卸载出内存共经历以下生命周期。
加载,链接(包括:验证,准备,解析),初始化,使用,卸载。
加载,验证,准备,初始化,卸载这五个运行顺序是固定的,而解析则不一定,某些情况下解析可以在初始化之后,这是为
了支持java运行时绑定。
2.生命周期简介
2.1 加载:(1)通过类的全限定名获取定义类的二进制字节流。
(2)将二进制字节流所代表的静态存储结构转化为JVM方法区运行时数据结构
(3)在堆中生成一个代表此类的java.lang.Class对象,作为这个类的访问入口
2.2验证:
保证Class文件字节流的信息符合JVM规范,并且不会危害虚拟机自身安全。(包括文件格式验证,元数据验证,
字节码验证,符号引用验证)
文件格式验证:验证字节流是否符合Class文件格式的规范并且能被当前版本的JVM处理。文件格式验证后,字
节流才会进入方法区进行存储。所以以下三个验证都是基于方法区存储结构进行的。
元数据验证:验证字节码描述的信息进行语义分析,保证他符合java语言的规范。(是否有父类,是否重写了父
类的final方法,是否实现了抽象父类的所有方法,是否定义了与父类冲突的方法)
字节码验证:进行数据流和控制流的分析,对类的方法体进行校验分析,保证程序运行时不会出现危害JVM安全
的行为
符号引用验证:此校验发生在JVM将符号引用转化为直接引用的时候(解析阶段),可以看作对类自身以外的信
息进行匹配性校验。
2.3准备
准备阶段是正式为类变量(static变量)分配内存(方法区内存)并设置初始值。
例如:public static int i=1;
那么准备阶段过后i的值为0而不是1。因为此时并未执行任何java方法,而赋值动作的putstatic指令发生在程序编译过
后存放在<clinit>方法之中,所以赋值动作是在初始化时发生。
但如果是:public static final int i=1;
编译时javac会为value生成ConstantValue属性,准备阶段JVM就会根据ConstantValue的设置为i赋值为1。
2.4解析
解析阶段将常量池里的符号引用(对目标的描述,例如我的老板)替换为直接引用(定位到内存的指针,比如我左
边10米的地方)。
2.5初始化:
初始化是执行类构造器 <clinit>方法的过程。
<clinit>是编译器自动收集类变量的赋值动作和static代码块中的语句合并而成的。虚拟机能够保证 <clinit>在多线程
环境下被正确的加锁和同步。
虚拟机严格规定了有且只有四种情况必须对类进行初始化。
(1)当遇到new (创建对象),getstatic(获取静态变量) ,pustatic(设置静态变量),invokestatic
(调用静态方法)这四条字节码指令时,如果类没有进行初始化,则触发类的初始化。
(2)当java.lang.reflect包的方法反射调用一个类的时候,如果类没有被初始化,则触发其初始化。
(3)初始化一个类的时候如果他的父类没有被初始化,则先初始化他的父类。
(4)虚拟机启动时用户需要指定一个执行的主类(含有main方法),虚拟机会先初始化这个主类。
以下过程不会触发类的初始化:
(1)当访问一个静态域(静态变量)时,只有定义这个静态域的类会被初始化。(子类访问父类的静态变量不会初始化子类)
(2)通过数组定义类的引用,不会触发此类的初始化。
(3)引用常量不会触发类的初始化(static final定义的常量)。
3.类加载器
实现“通过一个类的全限定名获取到描述此类的二进制字节流”的动作。
类加载器的层次结构:
启动类加载器(bootstrap class loader):用来加载java核心库,比如java.lang.String,java.lang.Class;他不是用
java语言编写的也不继承自java.lang.ClassLoader。
扩展类加载器(extension class loader):用来加载java的扩展哭库(JAVA_HOME/jre/lib/rt.jar或者jav.ext.dirs路径下的内容)
应用类加载器(application class loader):一般来说java应用的类都是由他来加载的。
自定义类加载器(bootstrap class loader):通过继承java.lang.ClassLoader实现自己的类加载器。
自定义类加载器
package com.lwx.study;
import java.io.*;
public class FileClassLoader extends ClassLoader{
//class文件的根目录
private String rootDir;
public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String s) throws ClassNotFoundException {
Class<?> x = findLoadedClass(s);
//如果类已经被加载过直接返回
if (x!=null){
return x;
}
ClassLoader parent = this.getParent();
//如果类没有加载先交给父加载器加载,依次往上提交
AppClassLoader ExtClassLoader BootStrapClassLoader
try {
x= parent.loadClass(s);
} catch (ClassNotFoundException e) {
//e.printStackTrace();
}
if (x!=null){
return x;
}
//父加载器不能加载,则自己加载
byte[]by=getClassData(s);
x = this.defineClass(s, by, 0, by.length);
//自己不能加载则抛出异常
if (x==null){
try {
throw new FileNotFoundException();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
return x;
}
private byte[] getClassData(String s){
String path=rootDir+"/"+s.replace(".","/")+".class";
InputStream is=null;
ByteArrayOutputStream baos=null;
try {
is=new FileInputStream(path);
byte[]bytes=new byte[1024];
baos=new ByteArrayOutputStream();
int temp;
while((temp=is.read(bytes))!=-1){
baos.write(bytes,0,temp);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if (baos!=null)
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (is!=null)
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
双亲委托机制
当一个类加载器收到类加载请求时,它不会去尝试加载,而是委托给父类加载器,只有当父类加载器不能完成加载动作时,它才回去尝试自己加载。
好处是,如果启动类加载器可以加载这个类,那么子类就没有机会加载这个类。例如用户定义了一个java.lang.String
类,他会一层层往父类加载器委托,最终到启动类加载器(加载核心库)。启动类加载器发现自己能加载java.lang.String,那么
他就会去加载java.lang.String。这样用户永远无法定义与核心库冲突的全限定类名。
破坏双亲委托机制
(1)有些场景下双亲委托机制无法满足我们的需求,如果基础类要调用用户的代码,首先基础类要通过ExtClassLoader加
载,但是ExtClassLoader却无法加载用户代码,这个时候我们可以选择不使用双亲委托机制。
例如线程上下文类加载器(默认为AppClassLoader):
Thread.currentThread().getContextClassLoader()
//通过设置改变类加载器
Thread.currentThread().setContextClassLoader(ClassLoader classLoader);
(2)Tomcat服务器的类加载机制也不能使用系统默认的类加载器,它为每个应用提供了一个独立的类加载器,而且不是
采用双亲委派机制,而是子类加载器优先加载。
(3)OSGI,面向java的动态模块系统,每个模块有自己对应的类加载器,他负责加载模块包含的java包和类。比如A模
块引用B模块的Test类时,加载Test类的类加载器是B模块的加载器。(谁定义谁加载)