虚拟机类加载机制


前言

 1.classLoader的介绍及加载过程

 
  与普通程序不同的是,Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader。所以classLoader的目的在于把class文件装入到jvm中。
  
2、那么classLoader又在那里的啦?又由谁调用呢?

  其实classLoader只是jvm的一个实现的一部分。Jvm提供的一个顶级的classLoader(bootStrap classLoader),bootStrap classLoader负责加载java核心的API(也就是java的自身的jar包)以满足java程序最基本的需求。Jvm还提供的两个classLoader,
Extension ClassLoader负责加载扩展的Java class(jar包),Application ClassLoader负责加载应用程序自身的类(.class文件)。
而Extension ClassLoader和Application ClassLoader则由bootStrap classLoader加载。

3.classLoader加载的基本流程

  当运行一个程序的时候,JVM启动,运行bootstrap classloader,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。

1、类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括,加载、验证、准备、解析、初始化、使用、卸载7个阶段,其中验证、准备、解析3个被称为连接

WX20180411-180608@2x

类加载 在加载、验证、准备、初始化、卸载 这5个阶段顺序是固定的,解析阶段则不一定,在某些情况下,解析阶段有可能在初始化阶段结束后开始,以支持Java的动态绑定。

关于第一个阶段加载:虚拟机规范中并没有强制约束,这个交给虚拟机自由把控,但是对于初始化,有且只有5种情况必须立即对类进行初始化(当然它前面的三个肯定执行了)

1、遇到new 的时候,以及调用一个被static修饰的字段,(被final除外,因为它以及放到常量池中去了)

2、遇到反射调用的时候,记得是调用哦

3、当初始化一个类的时候,如果发现父类还有没有经过初始化,需要先初始化父类,尊重父亲懂吧

4、当虚拟机启动的时候,执行的main的主类

5、不太懂,到时候再说吧,

下面使用static进行说明下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
1、父类
public class SuperClass {
static{
System.out.println("super class init!");
}
public static int a = 1;
public final static int b = 1;
}

2、子类
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}
}
3、测试
public class Test {

static{
System.out.println("test class init!");
}

public static void main(String[] args){
System.out.println(SubClass.a);
}
}

控制台
test class init!
super class init!
1

解释:很明显没有输出子类SubClass中的static代码块的信息,对于静态字段,只有直接定义这个类的字段的类才会被加载。

当子类添加一个静态字段的时候

public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}

public static int s = 2; //对于静态字段,只有指定定义这个字段的类才会初始化
}

public class Test {

static{
System.out.println("test class init!");
}

public static void main(String[] args){
System.out.println(SubClass.s);
}
}

控制台
test class init!
super class init!
SubClass init!
2

解释:当初始化类的时候,如果他的父类还没有被初始化,则需要先初始化它的父类,和上面的其实也不冲突啦



public final static int s = 2; //变成final常亮的时则不会初始化任何,因为它放到了常亮池中,并不是在类中获取的,所以不需要初始化,控制台只会打印出2


2、类加载的过程

也就是加载、验证、准备、解析、初始化这5个阶段的具体过程,

2.1 、加载

加载是类加载的第一个阶段,这个阶段,虚拟机完成3件事情,

1、通过一个类的全限定名来获取定义此类的二进制字节流,这个地方就厉害了,JAVA开发团队说的很模糊啊,充满智力的开发人员在这个基础上,从jar获取,从war获取,从动态代理proxy中获取等等

2、将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构(方法区就是用来放这些类型,常亮,静态常亮,方法描述等)

3、在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2.2、验证

确保class文件的字节流中包含的信息符合当前虚拟机的要求

1、文件格式验证

1、是否魔数为0XCAFEBABE开头
2、主次版本号是否在范围

2、元数据验证

判断是否符合java语法规范的眼球
是否有父类,是否继承了不允许继承的父类(final修饰)

3、字节码验证

操作栈总放了int,但是使用的时候却使用了long

4、符号引用验证

这个校验发送在解析阶段
符号引用的类,字段,方法的访问性质(private,protected等是否可以被当前类访问)

2.3、准备

准备阶段是正式为变量分配内存并设置变量初始值的阶段,需要注意的是仅包括static静态变量

2.4、解析

1、类或接口的解析

2、字段解析

比如,如果实现了接口,就会看看把父接口的拿下来

3、类方法解析

4、接口方法解析

2.5、初始化阶段

真正开始执行程序员java代码

3、类加载器

虚拟机设计团队,把类加载阶段中的,通过一个类的全限名称来描述二进制字节流,这个动作放到外部去实现,让应用程序自己决定如何去获取,所需要的类,实现这个动作的代码模块成为类加载器

从java虚拟机的角度看看,只存在两种不同的类加载器,一种是启动类加载器Bootstrap ClassLoader ,这个类加载器使用C++实现,是虚拟机自身的一部分,另一个种是由java语言实现,独立于虚拟机外部,并且全都继承自抽象类Java.lang.ClassLoader

比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。那就是说明jar包不同了,笨蛋

下面3中类加载器使我们经常遇到的
启动类加载器

1)启动类加载器(Bootstrap ClassLoader):前面已经介绍过,这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用

2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher.AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图所示。

WX20180411-195230@2x

图中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

      双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
      
      双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,如以下代码所示,逻辑清晰易懂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected synchronized 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);
}
}
if(resolve){
resolveClass(c);
}
return c;
}



先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。双亲委派的具体逻辑就实现在这个loadClass()方法之中


Author: jony
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source jony !
 Previous
项目中遇到的java堆溢出解决 项目中遇到的java堆溢出解决
前言报错提示 12Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space 1、以上报错很明显是java堆溢出分析
2018-07-02
Next 
Class文件结构 Class文件结构
前言 1、魔数每个class文件开头的4个字节成为魔数,它的唯一作用就是识别它是一个什么文件,在git或者是jpeg等文件开头都有魔数的存在 ,因为仅仅通过扩展名来判断是不能保证正确性的, class文件的魔数值为 0XCAFEBABE
2018-04-11
  TOC