我们知道 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. 执行了 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 初始化为默认值 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. – 从 zip , jar 等归档文件中加载 .class 文件 (引入外部 zip 、 jar 包)
4. – 从专有数据库中提取 .class 文件 (不常用)
5. – 将 Java 源文件动态编译为 .class 文件 (动态代理)
转( http://yhjhappy234.blog.163.com )