本文参考的《深入理解Java虚拟机》一书第七章,属于个人对这一章节的总结笔记。《深入理解Java虚拟机》有更多的代码解释。
什么是java虚拟机的类加载机制?
就是java虚拟机讲描述类的数据从class文件中读取到内存中,对数据进行验证,转换解析和初始化,最终转换成能够被java虚拟机直接使用的java类型。
生命周期?
一个类从加载到卸载,经过7个步骤
加载–>验证–>准备–>解析–>初始化–>使用–>卸载
何时加载类?何时初始化类?
对于何时加载类,《JAVA虚拟机规范》并没有进行强制的规范,由虚拟机去自由把握。
但是对于初始化,《Java虚拟机规范》是有严格规范的,有且只有6种其情况会初始化类。
1、new,putstatic、setstatic,invokestatic指令的时候,而生成这些指令有以下几个
a、new一个对象
b、读取或设置类静态字段(final static 修饰的除外)
c、调用静态方法
2、反射方法获取类的时候
3、初始化子类的时候,其父类还没有初始化,会优先初始化父类
4、当虚拟机启动,主类main方法所在类会优先初始化
5、 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
6、当一个接口中定义了默认方法,如果该接口的实现类发生了初始化,则该接口要在其之前触发初始化
public class ClassLoading0 {
/*
类的初始化会执行静态块,这里用静态块来测试类是否被加载
*/
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
//使用new关键字时--》结果:会初始化类
//new ClassLoading01();
//读取或设置静态字段--》结果:会初始化类
//int attr1=ClassLoading01.staticAttr;
//读取final修饰的静态字段--》结果:不会初始化类,原因是final修饰的变量是常量,放于常量池,并且有static修饰。直接从常量池中拿。
//int attr2=ClassLoading01.finalStaticAttr;
//反射读取---》会初始化类
//ClassLoading01.class.newInstance();
//初始化子类时,会先去初始化父类
new ClassLoading02();
}
}
class ClassLoading01 {
public static int staticAttr=1;
public final static int finalStaticAttr=1;
public static void classLoading01Test(){
System.out.println("调用静态方法");
}
static {
System.out.println("ClassLoading01静态块");
}
}
class ClassLoading02 extends ClassLoading01{
static {
System.out.println("ClassLoading02静态块");
}
}
被动引用案例
1、当子类调用父类的静态变量时,子类不会初始化
2、当创建一个引用数组时,不会初始化该类
public class ClassLoading0 {
/*
类的初始化会执行静态块,这里用静态块来测试类是否被加载
*/
public static void main(String[] args) {
//使用子类引用父类的静态变量或者常量时---》结果是:子类并不会初始化
//int attr1 = ClassLoading02.staticAttr;
//int attr2 = ClassLoading02.finalStaticAttr;
//通过数组定义引用类,不会初始化该类,其父类也不会初始化
ClassLoading02[] classLoading02s = new ClassLoading02[1];
}
}
class ClassLoading01 {
public static int staticAttr=1;
public final static int finalStaticAttr=1;
public static void classLoading01Test(){
System.out.println("调用静态方法");
}
static {
System.out.println("ClassLoading01静态块");
}
}
class ClassLoading02 extends ClassLoading01{
static {
System.out.println("ClassLoading02静态块");
}
}
类加载的步骤
上面讲了类加载的整个生命周期是7步
类加载的过程是5步
加载–>验证–>准备–>解析–>初始化
加载:
虚拟机完成了以下3个过程
1、通过类的全限定名来获取此类的二进制字节流
2、将字节流的静态数据结构转成方法区的运行时数据结构
3、在堆区生成Class对象
验证:
文件格式验证
元数据验证
字节码验证
符号引用验证
准备:
正式为类中的静态变量分配内存,并进行初始化赋值
如
public static int a=1;
这时候会额为a进行赋初值,也就是为0。这个阶段一直都是0。把1赋值给a是在初始化阶段。
但是如果是
public final static a=1;
这个时候,由于final修饰,并不会赋初值,而是直接把1赋值给a
解析:
符号引用变成直接引用
初始化:
虚拟机执行类中的java程序
执行静态块和为静态变量赋值
类加载器
每个类加载器都有自己的独立命名空间(在方法区)
也就是加载器加载的类,是放在方法区的不同位置。因此不同类加载器加载统一个类,由于内存地址不同,则这两个类不相等。但是统一个类加载器只会加载一次同一个类,在进行加载类时,会根据类的全限定名寻找该类是否被加载。
Hotspot类加载器分为几种?
从虚拟机角度来看,分为两种,一种是由c++编写的启动类加载器,另一种就是由java编写的类加载器,继承自抽闲类java.Lang.ClassLoader
从开发人员的角度分为3种,启动类加载器,拓展类加载器,应用类加载器
*****启动类加载器加载位置–><JAVA_HOME>\lib目录或者-Xbooyclasspath参数指定的路径下存放的。
*****拓展类加载器加载位置–><JAVA_HOME>\lib\ext目录或被java.ext.dirs系统变量所指定的路径中的所有类库。
*****应用类加载器–>加载用户类路径(ClassPath)上的所有类库
以上3个加载器是虚拟机为我们提供加载类库的位置。当我们想从其他位置加载类时。我们可以自定义类加载器。
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader myClass = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf("." )+1) + ".class";
InputStream stream = getClass().getResourceAsStream(fileName);
if (null == stream) {
return super.loadClass(name);
}
byte[] bytes = new byte[stream.available()];
stream.read(bytes);
return defineClass(name,bytes,0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object instance = myClass.loadClass("com.cjx.cjxtest.String").newInstance();
System.out.println(instance.getClass());
}
}
双亲委派机制
双亲委派机制就是,当一个类加载器收到请求加载某个类时,这个类并不会马上去加载这个类,而是委派给父类去加载,每一层类加载器都是如此,最终加载请求都会委派到最顶层的启动类加载器,当父类在自己的搜索范围内找不到该类时,会反馈给子类加载器,子类加载器才会自己去尝试加载。
为何要有双亲委派机制?
如果每一个类都被多个加载器加载,这时候在虚拟机就会存在多个该类,但是这个类并不是同一个,也不相等,在使用起来分不清。
例如:Java.Lang.Object类是在rt.jar包中,无论哪个类加载器加载这个类都会委派到最顶层的启动类加载器中,确保了这个类在虚拟机中只有一份,
如果我在classpath下创建一个Java.Lang.Object类,编译会通过,但是并不会去加载这个类。
打破双亲委派
双亲委派很好的解决了各个类加载器加载基础类的一致性问题。但是基础类如果需要回调用户代码,那就成了成模型的弊端。例如,Driver接口是定义在jdk中的,DriverManger也在jdk中,启动类加载器只能加载JAVA_HONE的lib下的文件,而数据库厂商提供的Driver接口的实现类在classPath路径下,由应用类加载器加载,所以启动类加载器委派子类来加载Driver实现。
SPI机制
在MATE_INF下的services下创建文件,文件中放接口实现类的权限定名,而文件名则为接口的全限定名。