关于类的加载,我很早就学习过,只是不是很熟练,经常忘记,所以今天复习了一下,决定把之前学过的东西记下来,便于随时查看。
现在我们来看一个经典的程序:
package com.why.classloader;
public class Singleton {
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
//private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
package com.why.classloader;
public class MyTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("count1 = " + singleton.counter1);
System.out.println("count2 = " + singleton.counter2);
}
}
在运行MyTest之后得到的结果是:
count1 = 1
count2 = 0
然而,如果我们将Singleton.java中第8行的注释去掉,将第5行注释掉,再次运行MyTest之后的结果是:
count1 = 1
count2 = 1
要想知道这个结果产生的原因,就需要了解类的加载机制。类的加载机制大概流程如下:
•加载:查找并加载类的二进制数据
•连接
–验证:确保被加载的类的正确性
–准备:为类的静态变量分配内存,并将其初始化为默认值
–解析:把类中的符号引用转换为直接引用
•初始化:为类的静态变量赋予正确的初始值
•Java程序对类的使用方式可分为两种
–主动使用
–被动使用
•所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
•主动使用(六种)
–创建类的实例
–访问某个类或接口的静态变量,或者对该静态变量赋值
–调用类的静态方法
–反射(如Class.forName(“com.why.Test”))
–初始化一个类的子类
–Java虚拟机启动时被标明为启动类的类(Java Test)
•除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化
现在我们开始讨论上面程序中的第一种情况。在MyTest中我们调用了Singleton类的静态方法,因此构成了对Singleton类的主动使用,那么Singleton类就会被初始化,在初始化之前应该先加载、连接。在连接阶段,Singleton类的静态变量会被初始化为默认是,也就是说:
singleton=null
counter1=0
counter2=0
连接阶段过了之后就会进行初始化,初始化按照类文件中的顺序依次执行:
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
//private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
第一步:singleton先初始化,调用构造函数,counter=1, counter2=1第二步:counter1初始化,因为没有对counter1赋值,则counter1保持原值不变为1
第三步:counter2初始化,counter2=0
所以counter1=1,counter2=0
第二种情况就请读者自己思考了。
类的加载
•类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
•加载.class文件的方式
–从本地系统中直接加载
–通过网络下载.class文件
–从zip,jar等归档文件中加载.class文件
–从专有数据库中提取.class文件
–将Java源文件动态编译为.class文件
•类的加载的最终产品是位于堆区中的Class对象
•Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口•有两种类型的类加载器
–Java虚拟机自带的加载器
•根类加载器(Bootstrap)
•扩展类加载器(Extension)
•系统类加载器(System)
–用户自定义的类加载器
•java.lang.ClassLoader的子类
•用户可以定制类的加载方式•类加载器并不需要等到某个类被“首次主动使用”时再加载它
•JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)
•如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
类的连接
类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。类的连接有三个步骤,上面有写。验证的主要是出于安全性考虑的,避免一些人伪造一些不合乎规范的恶意的字节码,进而在虚拟机中执行。类的解析就是把类的二进制数据中的符号引用替换为直接引用,直接引用就是指针。
类的初始化
类在初始化之前会首先初始化父类,这个跟C++里边是一样的,然后再初始化自己。Java虚拟机在初始化一个类的时候会要求它的所有父类都已经初始化,但是这条规则并不适用与接口,一个接口不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时才会导致该接口的初始化。只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用,类或接口才会被初始化。调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
类的加载器有三种,如下图所示:
类都是有类加载器加载的,类的加载机制使用的是父委托加载机制。
上图中的loader1和loader2都是用户自定义的类加载器,loader1是loader2的父类加载器。下面是一个自定义加载器:
package com.why.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String name; // 类加载器的名字
private String path = "C:\\";
private final String fileType = ".class";
public MyClassLoader(String name) {
super();
this.name = name;
}
public MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
@Override
public String toString() {
return this.name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = this.loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
InputStream input = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
name = name.replaceAll(".", "\\");
input = new FileInputStream(new File(path + name + fileType));
baos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = input.read()) != -1) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
} finally {
try {
input.close();
baos.close();
} catch (Exception ex) {
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("c:\\myapp\\serverlib\\");
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("c:\\myapp\\clientlib\\");
MyClassLoader loader3 = new MyClassLoader(null, "loader3");
loader3.setPath("c:\\myapp\\otherlib\\");
test(loader2);
test(loader3);
}
public static void test(ClassLoader classloader) throws Exception{
Class clazz = classloader.loadClass("Sample");
Object obj = clazz.newInstance();
}
}
从程序中可以看出loader2的父类加载器是loader1,如果在serverlib中有Sample.class文件,不论clientlib中是否有Sample.class文件,Sample都由loader1加载,这就是父类加载机制的一个例子。加载器之间的父子关系是指加载器对象之间的包装关系,而不是加载器类之间的继承关系。
命名空间
每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。在同一个命名空间中不会出现类的完整名字相同的两个类,但是在不同命名空间中则可以出现类的完整名字相同的两个类。
运行时包
只有属于同一个运行时包的类才能互相访问包可见,但是反射可以突破这个限制。举例说明。假设用户自定义了一个类java.lang.Spy,这个类由用户自定义加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的类加载器加载,它们属于不同的运行时包,所以java.lang.*不能访问核心类库java.lang中的包可见成员。(这个我也不是很懂)