深入剖析Classloader(一)--类的主动使用与被动使用

时光总嘲笑我的痴心妄想 提交于 2020-03-01 21:28:25

原文地址:http://yhjhappy234.blog.163.com/blog/static/3163283220115573911607

我们知道java运行的是这样的,首先java编译器将我们的源代码编译成为字节码,然后由JVM将字节码load到内存中,接着我们的程序就可以创建对象了,我们知道JVM将字节码load到内存之后将将建立内存模型(JVM的内存模型我们将在稍后阐述),那JVM是怎么将类load到内存中的呢?对了,是通过Classloader,今天我们就来深入探讨一下Classloader

首先我们来看一段诡异的代码(一段单实例测试代码)。

package com.yhj.jvm.classloader;

/**

 * @Description:单例初始化探究

 * @Author YHJ  create at 2011-6-4 下午08:31:19

 * @FileName com.yhj.jvm.classloader.ClassLoaderTest.java

 */

class Singleton{

    private static Singleton singleton=new Singleton();

    private static int counter1;

    private static int counter2 = 0;

    public Singleton() {

       counter1++;

       counter2++;

    }

    public static int getCounter1() {

       return counter1;

    }

    public static int getCounter2() {

       return counter2;

    }

    /**

     * @Description:实例化

     * @return

     * @author YHJ create at 2011-6-4 下午08:34:43

     */

    public static Singleton getInstance(){

       return singleton;

    }

}

/**

 * @Description: 测试启动类

 * @Author YHJ  create at 2011-6-4 下午08:35:13

 * @FileName com.yhj.jvm.classloader.ClassLoaderTest.java

 */

public class ClassLoaderTest {

    /**

     * @Description:启动类

     * @param args

     * @author YHJ create at 2011-6-4 下午08:30:12

     */

    @SuppressWarnings("static-access")

    public static void main(String[] args) {

       Singleton singleton=Singleton.getInstance();

       System.out.println("counter1:"+singleton.getCounter1());

       System.out.println("counter2:"+singleton.getCounter2());

    }

}

我们先猜测一下运行结果

然后我们再来调换一下单实例生成的顺序,将

private static Singleton singleton=new Singleton();

    private static int counter1;

    private static int counter2 = 0;

修改为

private static int counter1;

    private static int counter2 = 0;

    private static Singleton singleton=new Singleton();

再猜测一下结果,然后运行一下,看和你的猜测一致不?(是不是感觉很诡异),好吧,我们先不看这段程序,先介绍相关的内容,等介绍完了你就明白这段诡异的代码为什么这么执行了!我们知道我们运行刚才这段java程序是通过执行ClassLoaderTest的main函数引导起来的,而当我们执行完2个打印语句之后,JVM就停止了运行。这就是我们程序的生命周期。

在以下几种情况下JVM将结束自己的生命周期

  1. 1.         执行了System.exit()方法(具体可参见JDKAPI文档)
  2. 2.         程序正常执行结束
  3. 3.         程序在执行过程中遇到了错误或异常而异常终止
  4. 4.         由于操作系统出现错误而导致JVM进程终止

类通过JVMClassloader加载到内存经过以下几个步骤

加载 --> 连接 --> 初始化

?加载:查找并加载类的二进制数据

?连接

  1. 1.         验证:确保被加载的类的正确性
  2. 2.         准备:为类的静态变量分配内存,并将其初始化为默认值
  3. 3.         解析:把类中的符号引用转换为直接引用

?初始化:为类的静态变量赋予正确的初始值

我来分别解释一下这三个阶段都做了什么事情

  1. 1.         加载就是将二进制的字节码通过IO输入到JVM中,我们的字节码是存在于硬盘上面的,而所用的类都必须加载到内存中才能运行起来,加载就是通过IO把字节码从硬盘迁移到内存中。
  2. 2.         连接分为3个阶段,验证,准备和解析。

1)  验证这里可能大家会疑问了,我们的类不是通过JVM编译成的字节码的吗,为什么这里还要验证加载类的正确性,难道通过Java虚拟机的javac编译器生成的字节码还会有错误不成?当然,javac编译出来的类都是正确的,但是如果是通过其他途径生成的字节码呢?是不是正确的呢?就比如你自己建一个文本文件,然后重命名该文件为Test.class,然后让JVM来运行这个类,显然是错误的。当然因为JDK的源码是开放的,所以JVM字节码的生成规则也是公开的,所以也有一些第三方的软件可以生成符合JVM规范的字节码文件,如CGlib

2)  准备:为类的静态变量分配内存,并将其初始化为默认值,这里我们一定要看清楚是为静态变量分配内存,而不是我们的实例变量,为什么我要强调静态变量,因为实例变量是什么时候产生的,是生成实例的时候产生的,而我们一般是在new一个对象的时候才对这个类进行实例化(前提是这个类已经被加载),而我们现在还没有加载完类,所以这个时候只能对静态变量分配内存空间(静态变量是属于这个类的而不属于某个对象),这个一定要分清楚。然后为该静态变量初始化为默认值(这个大家应该不陌生,int类型是0boolean就是false,引用类型是null等)。

3) 解析:把类中的符号引用转换为直接引用,这个我们等下在讨论(后面我们会讲什么是符号引用,什么是直接引用)

  1.3 初始化:这个似乎与上面的初始化为默认值有点矛盾,我们再看一遍:为累的静态变量赋予正确的初始值,上面是赋予默认值,这里是赋予正确的初始值,什么是正确的初始值,就是用户给赋予的值。我们来看一个例子

class Test{
private static int a = 1;

}

我们知道,这个类加载好之后,a的值就是1,但实际是这样子的,类在加载的连接阶段,将a初始化为默认值0(int的默认值是0),然后在初始化阶段将a的值赋予为正确的初始值1. 我们看到最终a的值是等于1,但是实际的运行中是有一个将0赋予a的过程,这个过程放生在连接的准备阶段。类的初始化还有另外的一种形式,代码如下

class Test{

private static int a ;

static{

a=1;

  }

}

这里强调一点,这个时候还是没有类的实例生成的,这点一定要注意!《深入java虚拟机第二版》里面有一个图阐述了对应的关系,如下

Java程序对类的使用方式可分为2种,主动使用和被动使用。所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用时才初始化他们。”

主动使用(六种)

1)   –创建类的实例 (如new Integer())

2)   –访问某个类或接口的静态变量,或者对该静态变量赋值 (读写静态变量)

3)   –调用类的静态方法

4)   –反射

(如Class.forName(“com.yhj.jvm.classloader.ClassLoaderTest”))

5)   –初始化一个类的子类 (初始化子类的过程中会主动使用父类的构造方法)

6)   –Java虚拟机启动时被标明为启动类的类(含有main方法并且是启动方法的类)

除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化 (除了上述6种情况以外,都不会执行初始化,只会执行加载和连接)

好了,讲到这里我们大概知道类加载的几个步骤,那我们现在来详细的介绍一下类加载这个过程中的一些细节!

类的加载:累的加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区里面(具体的JVM内存模型我们会在后面讲到,这里可以参考下面JVM的内存模型图),然后在堆区创建一个java.lang.Class的对象,用于封装类在方法区内的数据结构!我们知道我们对于一个类可以创建很多个对象,但是这些对象共享同样的数据结构,而这个数据结构就是在加在过程中创建的这个class对象。我们可以通过 类名.class或者对象名.getClass()获取这个对象!无论创建了多少个实例对象,这个class的对象始终只有一个,类里面所有的结构都可以通过class对象获取,因此class对象就像一面镜子一样,可以反射一个类的内存结构,因此class是整个反射的入口!通过class对象我们可以反射的获取某个对象的数据结构,访问对应数据结构中的数据!

内存模型 《深入java虚拟机第二版》上面一个实例描述了一个类在加载过程中的内存模型,如下

加载

.class文件有几种方式

  1. –从本地系统中直接加载 (直接加载本地硬盘上的.class文件加载)
  2. –通过网络下载.class文件 (通过java.net.URLClassLoader加载网络上的某个.class文件)
  3. zipjar等归档文件中加载.class文件 (引入外部zip、jar包)
  4. –从专有数据库中提取.class文件 (不常用)
  5. –将Java源文件动态编译为.class文件 (动态代理)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!