文档章节

Java类装载过程与类装载器

Hosee
 Hosee
发布于 2016/03/16 22:22
字数 2565
阅读 2K
收藏 17

3 月,跳不动了?>>>

1. class装载验证流程

1.1 加载

  • 装载类的第一个阶段
  • 通过类的全限定名取得类的二进制流
  • 转为方法区数据结构
  • 在Java堆中生成对应的java.lang.Class对象

1.2 链接 -> 验证

目的:保证Class流的格式是正确的

文件格式的验证

  • 是否以0xCAFEBABE开头
  • 版本号是否合理

元数据验证

  • 是否有父类
  • 继承了final类?
  • 非抽象类实现了所有的抽象方法

字节码验证 (很复杂)

  • 运行检查
  • 栈数据类型和操作码数据参数吻合
  • 跳转指令指定到合理的位置

符号引用验证

  • 常量池中描述类是否存在
  • 访问的方法或字段是否存在且有足够的权限

1.3 链接 -> 准备

分配内存,并为类设置初始值 (方法区中,关于方法区请查看Java内存区域

  • public static int v=1;
  • 在准备阶段中,v会被设置为0
  • 在初始化的<clinit>中才会被设置为1
  • 对于static final类型(常量),在准备阶段就会被赋上正确的值
  • public static final  int v=1;

1.4 链接 -> 解析

解析是唯一一个不确定顺序的过程,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

这里简要说明下 Java 中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对 Java 来说,绑定分为静态绑定和动态绑定:

  • 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对 Java,简单的可以理解为程序编译期的绑定。Java 当中的方法只有 final,static,private 和构造方法是前期绑定的。
  • 动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在 Java 中,几乎所有的方法都是后期绑定的。

符号引用替换为直接引用

符号引用只是一种表示的方式,比如某个类继承java.lang.object,在符号引用阶段,只会记录该类是继承"java.lang.object",以这种字符串的形式保存,但是不能保证该对象被记载。

直接引用就是真正能使用的引用,它是指针或者地址偏移量,引用对象一定在内存。最终知道在内存中到底放在哪里。

替换后,Class才能索引到它要用的那些内容。

1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。

2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,查找流程如下图所示:

从下面一段代码的执行结果中很容易看出来字段解析的搜索顺序:

class Super{  
    public static int m = 11;  
    static{  
        System.out.println("执行了super类静态语句块");  
    }  
}  

class Father extends Super{  
    public static int m = 33;  
    static{  
        System.out.println("执行了父类静态语句块");  
    }  
}  

class Child extends Father{  
    static{  
        System.out.println("执行了子类静态语句块");  
    }  
}  

public class StaticTest{  
    public static void main(String[] args){  
        System.out.println(Child.m);  
    }  
}  

执行结果如下:

执行了super类静态语句块
执行了父类静态语句块
33

如果注释掉 Father 类中对 m 定义的那一行,则输出结果如下:

执行了super类静态语句块
11

static变量发生在静态解析阶段,也即是初始化之前,此时已经将字段的符号引用转化为了内存引用,也便将它与对应的类关联在了一起,由于在子类中没有查找到与 m 相匹配的字段,那么 m 便不会与子类关联在一起,因此并不会触发子类的初始化。

至于为什么先执行super的类静态语句块,后执行父类静态语句块是因为在初始化阶段子类的<clinit>调用前保证父类的<clinit>被调用

最后需要注意:理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译。如果对上面的代码做些修改,将 Super 改为接口,并将 Child 类继承 Father 类且实现 Super 接口,那么在编译时会报出如下错误:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	The field Child.m is ambiguous

	at StaticTest.main(StaticTest.java:20)

3、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口

4、接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。

1.5 初始化

执行类构造器<clinit>

  • static变量 赋值语句
  • static{}语句

子类的<clinit>调用保证父类的<clinit>被调用

因此,在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object

接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成()方法。但是接口与类不同的是:执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。

实验:

package test;

public class Test extends A
{
	static {
		System.out.println("Test");
	}
	public static void main(String[] args)
	{
		A a = new A();
	}
}

class A {
	static {
		System.out.println("A");
	}
}

输出:

A
Test

<clinit>是线程安全的

那么Java.lang.NoSuchFieldError错误可能在什么阶段抛出呢?

很显然是在链接的验证阶段的符号引用验证时会抛出这个异常,或者NoSuchMethodError等异常。

2. 什么是类装载器ClassLoader

  • ClassLoader是一个抽象类
  • ClassLoader的实例将读入Java字节码将类装载到JVM中
  • ClassLoader可以定制,满足不同的字节码流获取方式(譬如从网络中加载,从文件中加载)
  • ClassLoader负责类装载过程中的加载阶段

2.1 ClassLoader的重要方法

public Class<?> loadClass(String name) throws ClassNotFoundException

载入并返回一个Class

protected final Class<?> defineClass(byte[] b, int off, int len)

定义一个类,不公开调用

protected Class<?> findClass(String name) throws ClassNotFoundException

loadClass回调该方法,自定义ClassLoader的推荐做法

protected final Class<?> findLoadedClass(String name)

寻找已经加载的类

2.2 系统中的ClassLoader

  • BootStrap ClassLoader (启动ClassLoader)
  • Extension ClassLoader (扩展ClassLoader)
  • App ClassLoader (应用ClassLoader/系统ClassLoader)
  • Custom ClassLoader(自定义ClassLoader)

每个ClassLoader都有一个Parent作为父亲( BootStrap除外)

3. JDK中ClassLoader默认设计模式

自底向上检查类是否被加载,一般情况下,首先先从AppClassLoader中调用findLoadedClass查看是否已经加载,如果没有加载,则会交给父类,ExtensionClassLoader去查看是否加载,还没加载,则再调用其父类,BootstrapClassLoader查看是否已经加载,如果仍然没有,自顶向下尝试加载类,那么从 BootstrapClassLoader到 AppClassLoader依次尝试加载。

即使两个类来源于同一个 Class 文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的 Class 对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用 instanceof 关键字对对象所属关系的判定结果。

从代码上可以很容易看出来,首先先自己查看这个类是否被调用,如果没有找到,则调用父亲的loadClass,直到BootstrapClassLoader(没有父亲)。

我们把这个加载的过程叫做双亲模式。

双亲委托机制的作用是防止系统jar包被本地替换

3.1 双亲模式的问题

这种模式下会有一个问题:

顶层ClassLoader,无法加载底层ClassLoader的类

Java框架(rt.jar)如何加载应用的类?

比如:javax.xml.parsers包中定义了xml解析的类接口
Service Provider Interface SPI 位于rt.jar 
即接口在启动ClassLoader中。
而SPI的实现类,在AppLoader。

这样就无法用BootstrapClassLoader去加载SPI的实现类。

3.2 解决

JDK中提供了一个方法:

Thread. setContextClassLoader()
  • 上下文加载器
  • 是一个角色
  • 用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题
  • 基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例

上下文ClassLoader可以突破双亲模式的局限性

3.3 双亲模式的破坏

  • 双亲模式是默认的模式,但不是必须这么做
  • Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent
  • OSGi的ClassLoader形成网状结构,根据需要自由加载Class

Reference:

1. http://www.cnblogs.com/snake-hand/p/3151381.html

2. http://www.cnblogs.com/Lawson/archive/2012/07/31/2616623.html

3. http://wiki.jikexueyuan.com/project/java-vm/class-loading-mechanism.html

© 著作权归作者所有

Hosee
粉丝 621
博文 135
码字总数 209956
作品 0
杭州
程序员
私信 提问
加载中

评论(0)

Java类装载体系中的隔离性

正 文 Java中类的查找与装载出现的问题总是会时不时出现在Java程序员面前,这并不是什么丢脸的事情,相信没有一个 Java程序员没遇到过ClassNotException,因此不要为被人瞅见自己也犯这样的错...

crazyinsomnia
2010/01/31
1.6K
0
java的反射和它的类加载机制

java 的类装载系统: 在java虚拟机中有两种类装载器: 启动类装载器 和 自定义类装载器。 前者是jvm的一部分,后者是java程序的一部分。不同的类装载器放在不懂得命名空间中。 类转载子系统涉...

Richard_sun
2013/01/18
505
0
java 中静态代码块执行的时机

1、简单认为JAVA静态代码块在类被加载时就会自动执行 是错误的 2、正解:static块真正的执行时机 一个类的运行分为以下步骤: 装载 连接 初始化 装载阶段 通过类型的完全限定名,产生一个代表...

职业搬砖20年
2018/07/06
20
0
从java的类装载器看安全

jave是安全的,这体现在多个方面,其中之一就是java类的加载,而且java类的加载安全机制体现出来的是一种原则,就是保持信息的单向流动而不是 杂糅的双向依赖总是一件好事,因为首先这样做会...

晨曦之光
2012/04/10
125
1
Java类加载器ClassLoader

JAVA类装载方式,有两种: 1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 2.显式装载, 通过class.forname()等方法,显式加载需要的...

兴国First
2018/10/14
0
0

没有更多内容

加载失败,请刷新页面

加载更多

远程桌面连接命令输入

远程桌面是微软公司为了方便网络管理员管理维护服务器而推出的一项服务。从windows 2000 server版本开始引入,网络管理员使用远程桌面连接程序连接到网络任意一台开启了远程桌面控制功能的计...

不添乱
今天
26
0
MySQL查看锁操作

MySQL查看锁操作 表名称是否被锁定。名称锁定用于取消表或对表进行重命名等操作。 show open tables where In_use > 0; 分析表锁定 show status like 'table_locks%'; 查看行锁 SHOW STATUS ...

我爱吃炒鸡
今天
22
0
应用统计学与R语言实现笔记(番外篇三)——缺失值的相关系数分析

昨天刚好有位同学来咨询R语言里计算相关系数的一些问题,所以来谈谈关于缺失值的相关系数分析问题,主要是在R语言中如何处理含缺失值数据的相关系数分析。 1 问题描述 相关分析可以说是数据分...

胖胖雕
今天
29
0
Gradle 概述

Gradle 是一个开源的构建自动化(build automation)工具。有关构建自动化的定义请参考:https://en.wikipedia.org/wiki/Build_automation。同时 Gradle 被设计得足够灵活可以被用来构建几乎...

honeymoose
今天
31
0
如何在没有换行符或空格的情况下进行打印? - How to print without newline or space?

问题: The question is in the title. 问题在标题中。 I'd like to do it in python . 我想用python来做。 What I'd like to do in this example in c : 我想在c中的此示例中做什么: In C......

技术盛宴
今天
23
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部