jvm将描述java类的.class的字节码文件加载到内存中,并对文件中的数据进行安全性校验、解析和初始化,最终形成可以被java虚拟机直接使用的java类型,这个复杂的过程为jvm的类加载机制
类从被java到虚拟机内存开始,直到被卸载出内存为止,整个生命周期如上图所示,分为:加载、验证、准备、解析、初始化、使用、卸载共7个阶段。
class类的初始化时机
虚拟机规范严格规定,有且只有5种情况,必须立即对类进行初始化:
- 1、遇到new(对应新建对象)、getstatic(对应访问类变量、static final除外)、putstatic(对应设置类变量、static final除外)、invokestatic(调用类方法即静态方法)这4条指令时,如果类没有初始化,则需要先触发其初始化。
- 2、对类进行反射调用时,如果类没有初始化,则必须触发初始化
- 3、当初始化一个类时,如果其父类没有初始化,则必须先初始化他的父类
- 4、虚拟机执行的main方法入口,这个类必须初始化
- 5、JDK1.7.动态语言支持时,如果java.lang.invoke.MethodHandler指向的是REF_getstatic,REF_putstatic、REF_invokestatic的方法句柄时,必须先初始化这个类
这两种情况不会进行类的初始化
- 1、申明数组类:Cat [] cat = new Cat[10];
- 2、如下,B类不会初始化,A类会初始化
public class Test {
public static void main(String[] args) {
System.out.println(B.value);
}
}
class A {
public static int value = 123;
}
class B extends A {
}
类加载的过程
加载
在加载阶段,虚拟机需要完成以下3件事情:
-
1、通过一个类的全限定名来获取定义此类的二进制字节流。对于字节流的获取,jvm并没有明确的限制
- 从ZIP包中读取
- 从网络中获取
- 运行时计算生成
- 从其他文件生成,如JSP技术
- 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
连接
连接是很重要的一步,过程比较复杂,分为三步 验证 》准备 》解析
- 验证:确保类加载的正确性。一般情况由javac编译的class文件是不会有问题的,但是可能有人的class文件是自己通过其他方式编译出来的,这就很有可能不符合jvm的编 译规则,这一步就是要过滤掉这部分不合法文件
- 准备:为类的静态变量分配内存,将其初始化为默认值 。我们都知道静态变量是可以不用我们手动赋值的,它自然会有一个初始值 比如int 类型的初始值就是0 ;boolean类型初始值为false,引用类型的初始值为null 。 这里注意,只是为静态变量分配内存,此时是没有对象实例的
- 解析:把类中的符号引用转化为直接引用。解释一下符号引用和直接引用。比如在方法A中使用方法B,A(){B();},这里的B()就是符号引用,初学java时我们都是知道这是java的引用,以为B指向B方法的内存地址,但是这是不完整的,这里的B只是一个符号引用,它对于方法的调用没有太多的实际意义,可以这么认为,他就是给程序员看的一个标志,让程序员知道,这个方法可以这么调用,但是B方法实际调用时是通过一个指针指向B方法的内存地址,这个指针才是真正负责方法调用,他就是直接引用。
初始化
- 初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步
- 当范围一个Java类的静态域时,只有真正声名这个域的类才会被初始化
-
类的主动引用(一定会发生类的初始化)
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当虚拟机启动,java Demo01,则一定会初始化Demo01类,说白了就是先启动main方法所在的类
- 当初始化一个类,如果其父类没有被初始化,则先初始化它父类
-
类的被动引用(不会发生类的初始化)
-
当访问一个静态域时,只有真正声名这个域的类才会被初始化
- 通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类的引用,不会触发此类初始化
- 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
-
jvm类加载器
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,实现这个动作的代码模块称为“类加载器”。JVM提供了3种类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOMElib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOMElibext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
- 用户自定义类加载器(Customized Class Loader):用户可以自己定义类加载器来加载类。所有的类加载器都要继承java.lang.ClassLoader类。
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
类加载器加载的class在堆内存中的模型:
认识双亲委派模型
对于某个特定的类加载器而言,应该为其指定一个父类加载器,当用其进行加载类的时候:
-
- 委托父类加载器帮忙加载;
-
- 父类加载器加载不了,则查询引导类加载器有没有加载过该类;
-
- 如果引导类加载器没有加载过该类,则当前的类加载器应该自己加载该类;
-
- 若加载成功,返回 对应的Class 对象;若失败,抛出异常“ClassNotFoundException”。
请注意:
双亲委派模型中的"双亲"并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器。上面的步骤中,有两个角色:
-
- 父类加载器(parent classloader):它可以替子加载器尝试加载类
-
- 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。
双亲模型如下图:
AppClassLoader的工作流程如下图:
双亲委派模型底层代码实现:
//提供class类的二进制名称表示,加载对应class,加载成功,则返回表示该类对应的Class<T> instance 实例
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经被当前的类加载器记载过了,如果已经被加载,直接返回对应的Class<T>实例
Class<?> c = findLoadedClass(name);
//初次加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果有父类加载器,则先让父类加载器加载
c = parent.loadClass(name, false);
} else {
// 没有父加载器,则查看是否已经被引导类加载器加载,有则直接返回
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父加载器加载失败,并且没有被引导类加载器加载,则尝试该类加载器自己尝试加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//是否解析类
if (resolve) {
resolveClass(c);
}
return c;
}
}
如何破坏双亲委托模型:
- 1、在构造类加载器时将parent设为null(构造函数参数为null即可,可以看下源码)
- 2、重写loadloadClass(String,boolean)方法,改变类的查找机制