[置顶] 《深入理解java虚拟机》读书笔记——类加载机制

时间:2022-12-29 13:06:22

一、类的生命周期:

[置顶]        《深入理解java虚拟机》读书笔记——类加载机制

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,但解析阶段不一定,在某些情况下可以在初始化之后在开始,这是为了支持java语言的运行时绑定(也称动态绑定和晚期绑定)。关于运行时绑定可以查看这篇文章:http://zhangjunhd.blog.51cto.com/113473/49452/。 



二、类加载的时机

什么情况下开始类加载阶段的第一个阶段:加载?java虚拟机并没有进行强制约束,这点交给虚拟机的具体实现来*把握。

但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行初始化(主动使用)。

1、使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。

2、使用java.lang.reflect包的方法对类进行反射调用的时候(如Class.forName("con.xx.Test"))。

3、初始化一个类的子类的时候。

4、虚拟机执行启动时,被标明为启动类的类(java Test)。

5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(关于jdk1.7的动态语言——http://www.infoq.com/cn/articles/jdk-dynamically-typed-language/)

除主动引用(上述5中场景)之外,其他所有引用类的方式都不会触发初始化,这些其他类的引用方式称为被动使用

比如:

1、通过子类引用父类的静态字段,不会导致子类初始化。

2、通过数组定义来引用类,不会触发此类的初始化。

3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发。

4、通过类名获取Class对象,不会触发类的初始化。

5、通过Class.forName加载指定类时,如果指定参数initialize(这个参数告诉虚拟机,要不要对类进行初始化)为false时,也不会触发类的初始化。

6、通过ClassLoader默认的loadClass方法,也不会触发初始化。




三、类的生命周期的各个阶段各发生了什么

加载
加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:
1. 通过一个类的全限定名获取该类的二进制流。
2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。 
3. 在内存中生成该类的Class对象,作为方法区该类的数据访问入口。

[置顶]        《深入理解java虚拟机》读书笔记——类加载机制




验证
验证的目的是为了确保Class文件的字节流中的信息不会危害到虚拟机.在该阶段主要完成以下四钟验证:
1. 文件格式验证:验证字节流是否符合Class文件的规范,如是否以魔数0xCAFEBABE开头,主、次版本号是否在当前虚拟机处理范围内,常量池中的常量是否有不被支持的类型等等。
2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否继承了不允许被继承的类(被final修饰的类)等。
3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常验证:符号引用中通过字符串描述的全限定名能否找到对应的类、在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段等。


准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
    public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。

但是注意如果声明为:

    public static final int value = 123 //在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将value赋值为123


解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。


初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

初始化阶段是执行类构造器<clinit>()方法的过程。关于<clinit>()方法:

1、<clinit>()方法是由编译器自动收集类中所有的类变量的赋值动作静态代码块(static{}块)中的语句合并产生的。收集的顺序由语句在源文件中出现的顺序决定,静态代码块只能f访问定义在它之前的变量,定义在它之后的变量可以赋值却不能访问(编译报错:Illegal forward reference)。

2、父类的<clinit>()方法先执行,也就意味着父类中的静态代码块优先于子类的变量赋值。

3、<clinit>()方法对于类或接口来说不是必须的。即一个类要是没有变量赋值,也没有静态代码块,编译器就可以不为其生成<clinit>()方法。

4、接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口也会生成<clinit>()方法,不同的是执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义变量被使用时,它才会被初始化,另外接口的实现类在初始化时也不会初始化接口。

5、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。


四、类加载器及双亲委派模型

jvm提供了3种类加载器:

1、启动类加载器(Bootstrap ClassLoader):负责加载< JAVA_HOME>\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机识别(按文件名识别,如rt.jar)的类。
2、扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载 <JAVA_HOME>\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
3、应用程序类加载器(Application ClassLoader):在个类加载器由sun.misc.Launcher$AppClassLoader实现,由于他是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器,负责加载用户路径(classpath)上的类库。


双亲委派模型如下:

[置顶]        《深入理解java虚拟机》读书笔记——类加载机制

双亲委派模型的工作过程:一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个加载请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

需要指出的是,加载器之间的父子关系实际上去组合关系而不是继承关系

[置顶]        《深入理解java虚拟机》读书笔记——类加载机制

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器 ,都拥有一个独立的命名空间(子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看到父加载器看到的类,而父加载器加载的类看不到子加载器加载的类)。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的
类加载器不同,那这两个类就必定不相等(equals,isAssignableFrom,isInstance等方法)。


五、如何实现自定义类加载器

先看看ClassLoader类的loadClass()方法

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {//类没有被加载
long t0 = System.nanoTime();
try {
if (parent != null) {//找加载器的父加载器。如果父加载器不是null(不是Bootstrap ClassLoader),那么就执行父加载器的loadClass方法
c = parent.loadClass(name, false);
} else {//把类加载请求一直向上抛,直到父加载器为null(是Bootstrap ClassLoader)为止
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) { //判断若父加载器没有加载到这个类,就由自己加载
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

再看一下findClass()这个方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

因此
1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可

2、如果想打破双亲委派模型,那么就重写整个loadClass方法


当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader{

private String name; //类加载器的名字

private String path = "d:\\"; //加载类的路径

private final String fileType = ".class"; //class文件的扩展名

public MyClassLoader(String name){
super(); //让系统类加载器成为该类加载器的父加载器

this.name = name;
}

public MyClassLoader(ClassLoader parent ,String name){
super(parent); //显示指定该类加载器的父加载器
this.name = name;
}

@Override
public String toString() {
return this.name;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassData(name);
return this.defineClass(name,data,0,data.length);
}

private byte[] loadClassData(String name){
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try{
this.name = this.name.replace(".","\\"); //com.xx.xx 换成com\xx\xx
is = new FileInputStream(new File(path + name +fileType));
baos = new ByteArrayOutputStream();
int ch = 0;
while(-1 != (ch = is.read())){
baos.write(ch);
}
data = baos.toByteArray();
}catch (Exception e){
e.printStackTrace();
}finally {
try{
is.close();
baos.close();
}catch (Exception e){
e.printStackTrace();
}
}
return data;
}

public static void main(String[] args) throws Exception{
MyClassLoader loader1 = new MyClassLoader("loader1"); //父加载器为系统类加载器
loader1.setPath("d:\\myapp\\serverlib\\");

MyClassLoader loader2 = new MyClassLoader(loader1,"loader2");
loader2.setPath("d:\\myapp\\clientlib\\");

MyClassLoader loader3 = new MyClassLoader(null,"loader3");//null表示父加载器是根类加载器
loader3.setPath("d:\\myapp\\otherlib\\");
test(loader2);
test(loader3);
}
public static void test(ClassLoader loader) throws Exception{
Class clazz = loader.loadClass("Sample");
Object object = clazz.newInstance();
}
}
自定义类加载器可以拿来做什么呢:

1、加密:java代码很容易被反编译,如果你需要把自己的代码进行加密,可以先将编译后的代码用某种加密算法加密,然后实现自己的类加载器,负责将这段加密后的代码还原。

2、从非标准的来源加载代码:例如你的部分字节码是放在数据库中甚至是网络上的,就可以自己写个类加载器,从指定的来源加载类。
3、动态创建:为了性能等等可能的理由,根据实际情况动态创建代码并执行。

4、javaWeb服务器,如tomcat都实现了自己的不同的类加载器,使得类库能被不同的应用程序隔离或共享等等。