什么是类加载
通过类加载器,将已经编译为.class格式的文件加载进内存,而其中类型的加载、连接和初始化过程都是在程序运行阶段运行的,这些会对提前编译造成困难和工作量,确为java应用提供了极高的扩展性和灵活性。
类加载的时机
- 使用new关键字实例化对象的时候。
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
的时候。 - 调用一个类型的静态方法的时候。
- 反射
- 初始化时,有父类要先初始化父类
- 执行main方法
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有
这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类的生命周期
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(例如多态)。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入
口。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚
拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,按照逻辑上来讲,这些变量都会在方法区中分配内存,JDK8之后,类变量也随着类变量储存在堆中。
初始值通常为数据类型的零值,而把实际值赋给变量的操作要到类加载的初始化(clinit)阶段才会进行。
也会存在特殊情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,如类变量被final修饰。
解析
class对象中符号引用转换为直接引用的过程。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
初始化阶段就是执行类构造器
结束生命周期
- 执行了 System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致 Java 虚拟机进程终止
类加载器
启动类加载器
BootstrapClassLoader
,负责加载存放在JDK\jre\lib
(JDK 代表 JDK 的安装目录,下同)下,或被-Xbootclasspath
参数指定的路径中的,并且能被虚拟机识别的类库(如 rt.jar,所有的 java.开头的类均被BootstrapClassLoader
加载)。启动类加载器是无法被 Java 程序直接引用的。
扩展类加载器
ExtensionClassLoader
,该加载器由sun.misc.Launcher\$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext
目录中,或者由java.ext.dirs
系统变量指定的路径中的所有类库(如 javax.开头的类)。开发者可以直接使用扩展类加载器。
应用类加载器
ApplicationClassLoader
,该类加载器由sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)所指定的类。开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。
因为 JVM 自带的 ClassLoader 只是懂得从本地文件系统加载标准的 java class 文件,因此如果编写了自己的 ClassLoader,便可以做到如下几点:
在执行非置信代码之前,自动验证数字签名。
动态地创建符合用户特定需要的定制化构建类。
从特定的场所取得 java class,例如数据库中和网络中。
类加载机制
- 全盘负责
当一个类加载器负责加某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。 - 父类委托/双亲委派
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。系统类防止内存中出现多份同样的字节码 - 缓存机制
缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区寻找该 Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区。这就是为什么修改了 Class 后,必须重启 JVM,程序的修改才会生效。
类加载方式
类加载有三种方式:
- 命令行启动应用时候由 JVM 初始化加载。
- 通过 Class.forName()方法动态加载。
- 通过 ClassLoader.loadClass()方法动态加载。
Class.forName()和 ClassLoader.loadClass()区别:
Class.forName()
:将类的.class 文件加载到 jvm 中之外,还会对类进行解释,执行类中的 static 块;ClassLoader.loadClass()
:只干一件事情,就是将.class 文件加载到 jvm 中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 块。Class.forName(name,initialize,loader)
带参函数也可控制是否加载 static 块。并且只有调用了 newInstance()方法采用调用构造函数,创建类的对象 。