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

 

    我们知道 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 程序是通过执行 ClassLoaderTestmain 函数引导起来的,而当我们执行完 2 个打印语句之后, JVM 就停止了运行。这就是我们程序的生命周期。

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

1.           执行了 System.exit() 方法(具体可参见 JDK API 文档)

2.           程序正常执行结束

3.           程序在执行过程中遇到了错误或异常而异常终止

4.           由于操作系统出现错误而导致 JVM 进程终止

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

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

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

? 连接

1.           验证:确保被加载的类的正确性

2.           准备:为类的静态变量 分配内存,并将其初始化为默认值

3.           解析:把类中的符号引用转换为直接引用

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

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

1.           加载就是将二进制的字节码通过 IO 输入到 JVM 中,我们的字节码是存在于硬盘上面的,而所用的类都必须加载到内存中才能运行起来,加载就是通过 IO 把字节码从硬盘迁移到内存中。

2.           连接分为 3 个阶段,验证,准备和解析。

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

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

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

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

 

class Test{
private static int a = 1;
}
 

我们知道,这个类加载好之后, a 的值就是 1 ,但实际是这样子的,类在加载的连接阶段,将 a 初始化为默认值 0int 的默认值是 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 对象我们可以反射的获取某个对象的数据结构,访问对应数据结构中的数据!

 



  
JVM

内存模型

 

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

 



 

 加载

.class 文件有几种方式

1.    从本地系统中直接加载   (直接加载本地硬盘上的 .class 文件加载)

2.    通过网络下载 .class 文件   (通过 java.net.URLClassLoader 加载网络上的某个 .class 文件)

3.    zip jar 等归档文件中加载 .class 文件   (引入外部 zipjar 包)

4.    从专有数据库中提取 .class 文件   (不常用)

5.    Java 源文件动态编译为 .class 文件   (动态代理)

  转( http://yhjhappy234.blog.163.com )

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值