在JVM类要通过类装载器(ClassLoader)进行装载后,才能进行执行。本篇总结了类装载器的一些知识。
一、class装载验证流程
在第一篇总结中介绍了JVM的内存结构:
可以看到class文件首先要通过“类加载器子系统”,才能被加载到内存中处理。那么class文件是怎么通过类加载器加载至内存中的呢?
下面是class装载验证的流程:
加载--->连接--->初始化
其中链接又可以分为“验证”、“准备”和“解析”:
下面逐一分析:
1.加载
加载操作是装载类的第一个阶段,此时会取得类的二进制流,然后将该二进制流转为方法区的数据结构,最后在Java堆中生成对应的java.lang.Class对象。
2.链接
(1)验证
验证的目的就是保证Class流的格式是正确的。那么如果校验Class流格式呢?一般验证以下三点:
● 文件格式的验证
- 是否以0xCAFEBABE开头(Cafebabe咖啡宝贝)
- 版本号是否合理(JDK6不可能生成JDK7的版本)
● 元数据验证
- 是否有父类
- 是否继承了final
● 字节码验证(很复杂)
- 运行检查
- 栈数据类型和操作码数据参数吻合(例如操作码数量超过了设置的栈空间)
- 跳转指令到合理的位置(字节码的偏移量位置错误,跳到不存在的地方)
● 符号引用验证
- 常量池描述类是否存在
- 访问的方法或者字段是否存在且有足够的权限(尝试访问权限不够的方法或字段)
(2)准备
在准备阶段,会为该class分配内存,并且为类设置初始值(方法区中)。
例如下面的代码:
public static int a=1;
在准备阶段,a会被初始化为0(int类型默认值为1),而在初始化的<clinit>中才会被设置为1。
对于static final类型:
public static final int a=1;
在准备阶段就会被赋上正确的值。
(3)解析
此阶段会将符号引用替换为直接引用。
符号引用简单来说就是一个字符串,例如超类(没有继承的类),在class常量池有一个字符串,字符串的内容为“java.lang.Object”,此时“java.lang.Object”就是一个符号引用,而符号引用并不能直接被使用,它只是一种使用方式,如果需要使用的话,需要将符号引用改为直接引用,所谓的直接引用就是我要知道这个对象在内存的哪里,及是指针或者地址偏移量。
具体的解析分为以下几类
1)类或接口的解析
一般类或者接口中都有各种变量,该解析就是判断这些变量是基本数据类型,还是普通的对象类型,进而通过不同的方式解析为直接引用。
2)字段解析
对类的目标字段进行解析时,首先会在本类中查找是否有与目标名称和字段描述相同的字段,如果有,则查找结束;如果没有,则会从下到上递归搜索该类实现的各个接口以及它们的父接口;如果还没有找到,则下到上递归搜索该类继承的各个父类以及它们父类的父类。
例如:
public class FieldParsingTest {
public static void main(String[] args) {
System.out.println(C.a);
}
}
class A{
public static int a = 1;
}
class B extends A{
public static int a = 2;
}
class C extends B{
}
结果:
说明加载器先是去C本身找a字段,没找到,然后去找他的父类B,然后在B里面找到了,直接返回了a的值,不再去A里寻找a字段了。而此时如果我们在B和C中都不定义a,这个时候就会输出A中的a字段的值。
3)类方法解析
解析该类的方法,与字段解析,但是先搜索父类,再搜索接口。
4)接口方法解析
解析该类的接口方法,递归向上搜索父接口。
3.初始化
该阶段类构造器<clinit>为static变量赋值,执行static{}语句。
类的初始化称之为“class init”,而执行类构造器就是<clinit>,为英文的缩写。执行类构造器<clinit>,会将static变量变量赋值,并执行static{}语句。
这里子类的<clinit>调用前保证父类的<clinit>被调用,例如先执行了父类的static静态语句块,再执行子类的,例如:
public class ClinitTest {
public static void main(String[] args) {
Son son = new Son();
}
}
class Parent{
static{
System.out.println("I'am Parent");
}
}
class Son extends Parent{
static{
System.out.println("I'am Son");
}
}
运行结果:
重点注意!!!面试中经常将static{}语句的加载和字段解析混在一起,这里我们记住两点,一个是字段解析时从下向上寻找的,而static{}语句是从上向下执行的,且如果在仅仅调用静态字段而不去new类的话,static{}语句会执行到找到静态字段的那个类而停止向下执行:
public class ClinitTest {
public static void main(String[] args) {
//Son son = new Son();
System.out.println(Son.a);
}
}
class GrandParent{
public static int a = 1;
static{
System.out.println("I'am GrandParent");
}
}
class Parent extends GrandParent{
public static int a = 2;
static{
System.out.println("I'am Parent");
}
}
class Son extends Parent{
static{
System.out.println("I'am Son");
}
}
运行结果:
这里可以看到,寻找a字段是从下向上的,而static{}语句是从上向下执行的。这里因为a不会与子类Son关联在一起,因此并不会触发子类Son的初始化,所以不会执行它的static{}语句。
<clinit>是线程安全的,一个线程进去之后,其它线程就会等待。
问题:Java.lang.NosuchFieldError错误可能在什么阶段抛出?
答:NoSuchFieldError表示没有对应的字段,即可能是class中的属性名拼写错误,此时应该是类加载器进行链接阶段时,进行“符号引用验证”来判断常量池描述类是否存时报出的异常,当然,如果一个需要使用的类无法在系统中找到,此时会抛出NoClassDefFoundError的异常,,一个方法无法找到,会抛出NoSuchMethodError异常。
二、什么是类装载器ClassLoader
1.ClassLoader的特点
(1)CLassLoader是一个抽象类
(2)CLassLoader的实例将读入Java字节码将类装载到JVM中
(3)CLassLoader可以定制,满足不同的字节码流获取方式。
(4)CLassLoader负责类装载过程总的加载阶段。
2.CLassLoader的重要方法
(1)public Class<?> loadClass(String name) throws ClassNotFoundException
载入并返回一个Class
(2)protected final Class<?> defineClass(byte[] b, int off, int len)
定义一个类,不公开调用
(3)protected Class<?> findClass(String name) throws ClassNotFoundException
loadClass回调该方法,自定义ClassLoader的推荐做法
(4)protected final Class<?> findLoadedClass(String name)
寻找已经加载的类
三、JDK中ClassLoader默认设计模式
与应用程序相关的ClassLoader有以下几种:
(1)BootStrap ClassLoader (启动ClassLoader)
(2)Extension ClassLoader (扩展ClassLoader)
(3)App ClassLoader (应用ClassLoader/系统ClassLoader)
(4)Custom ClassLoader(自定义ClassLoader)
这里每个ClassLoader都有一个Parent作为父亲( BootStrap除外)。
下面是ClassLoader的结构图:
在该图中说明了类加载的ClassLoader的协同工作步骤:
1)自底向上检查类是否已经加载
从Custom ClassLoader/App ClassLoader向上开始检查类是否加载,如果有,则返回;如果没有,就向上面的ClassLoader请求检查该类是否加载,如果全部的ClassLoader都检查没有加载该类的话,就要尝试进行加载。
2)自顶向下尝试加载类
先由BootStrap ClassLoader做加载,如果没有加载成功,则向下面的ClassLoader请求加载。
3)类加载范围
BootStrap ClassLoader加载的核心jar包就是JVM中的rt.jar,系统的核心类都在这个jar里面,而BootStrap ClassLoader会在JVM启动的时候加载这个jar中的类。我们也可以在启动参数中设置“-Xbootclasspath”,即启动类的路径,该路径下的类也会被BootStrap ClassLoader加载。
Extension ClassLoader加载的是%JAVA_HOME%/lib/ext/下的所有jar包。
App ClassLoader加载的是ClassPath下的所有类。
Custom ClassLoader为自定义加载器,它会根据开发人员的编写逻辑加载指定的类。
四、双亲委派模型
下面是:ClassLoader中loadClass的源代码:
在加载类的时候,先执行findLoadedClass看一下该加载器是否已经加载了该类,如果该类已经加载则返回该类,如果没有进行加载,则看一下是否存在父加载器,如果存在,就去请求父加载器进行加载,如果不存在父加载器,则直接请求启动加载器BootStrap ClassLoader进行加载。也即是递归加载,直到顶部的启动加载器BootStrap ClassLoader。
(1)模型解读
上面的加载机制就叫做双亲委派模式,双亲委派模型要求除顶层启动类加载器外其余类加载器都应该有自己的父类加载器;类加载器之间通过复用关系来复用父加载器的代码。
(2)模型作用
任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性,而JVM中两个类是否“相等”(Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定),必须是由同一个类加载器加载的才可以判定为相等。
该模式的好处是:
1)Java类随着它的类加载器一起具备了一种带有优先级的层次关系
2)防止内存中出现多份同样的字节码
层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
3)防止自定义类与系统核心类重名,导致程序混乱
jdk等核心类库中的类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统的核心类库提供的类,自己仿写的jdk核心类根本没有机会得到加载。
倘若没有双亲委派模型,而是由各个类加载器自行加载的话,如果开发者尝试编写一个与rt.jar类库中重名的Java类(如java.lang.Object类),虽然可以正常编译,但是永远无法被加载运行(因为内存中有两个一模一样,但逻辑完全不同的类)。
所以,在双亲委派模式下,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载。
但有些时候可能我们需要破坏双亲委派模型,例如基础类会调用户的代码(JNDI)服务。下一篇我们来介绍如何解除双亲委派模式,让顶层的类加载器能加载底层的类。
标签:Java,虚拟机,class,public,探究,static,Son,ClassLoader,加载 From: https://blog.51cto.com/u_16012040/6166630