[Java虚拟机]Java类的加载
文章目录
一、类的加载(类初始化)
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。
在java代码中,类型的加载、连接、与初始化过程都是在程序运行期间完成的(类从磁盘加载到内存中经历的三个阶段)
- 类型:定义的类、接口或者枚举称为类型而不涉及对象。
- 程序运行期间:并没有在编译器就完成了加载。
注意事项
- 类加载器并不需要等到某个类被 “首次主动使用” 时再加载它。
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载它。
- 如果在预先加载的过程中遇到了
.class
文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError
错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
二、类的生命周期
(重点是类的主动使用)
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是固定确定的,类的加载过程必须按照这种顺序开始。而解析阶段可以在初始化后再开始,这是为了支持 Java 语言的运行时绑定【也就是java的动态绑定/晚期绑定】。
1. 加载
将类的.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class
对象(HotSpot虚拟机将其放在方法区中),用来封装类在方法区内的数据结构。
类的加载的最终产品是位于堆区中的 Class
对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
Class对象是存放在堆区的,不是方法区。 类的元数据才是存在方法区的。类的元数据包括类的方法代码,变量名,方法名,访问权限,返回值等。
JDK7创建Class实例存在堆中;因为JDK7中JavaObjectsInPerm参数值固定为false。
JDK8移除了永久代,转而使用元空间来实现方法区,创建的Class实例依旧在java heap(堆)中
编写一个新的java类时,JVM就会帮我们编译成class对象,存放在同名的.class文件中。在运行时,当需要生成这个类的对象,JVM就会检查此类是否已经装载内存中。若是没有装载,则把.class文件装入到内存中。若是装载,则根据class文件生成实例对象。
加载阶段总结:
.class
文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口
加载 .class文件的方式:
类的加载由类加载器完成,类加载器包括① JVM提供的类加载器(系统类加载器)、② 开发者通过继承ClassLoader
基类来创建自己的类加载器。通过使用不同的类加载器,可以从不同来源加载类的二进制数据。
2. 验证
目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
-
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以
0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 -
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了
java.lang.Object
之外。 -
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
-
符号引用验证:确保解析动作能正确执行。
验证阶段不是必须的,对程序运行期没有影响,如果所引用的类经过反复验证,可以考虑采用 -Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3. 准备 *
JVM 便会开始为类变量分配内存并初始化。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
内存分配的对象:Java 中的变量有类变量以及类成员变量两种类型,类变量指的是被 static 修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM 只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始。
初始化的类型: 这里的初始化指的是为类变量赋予 Java 语言中该数据类型的默认值,而不是用户代码里初始化的值。但如果一个变量是常量(被 static final
修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。
4. 解析
(了解即可)
虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
5.初始化 *
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。
Java程序对类的使用方式可分为两种:主动使用与被动使用。一般来说只有当对类的首次主动使用
的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。
类的主动使用 **
-
创建类的实例,也就是
new
的方式 -
访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰或更准确的说是在编译器把结果放入常量池的静态字段除外)
-
调用类的静态方法
-
反射(如 Class.forName(“com.gx.yichun”))
-
初始化某个类的子类,则其父类也会被初始化
-
Java虚拟机启动时被标明为启动类的类(
JavaTest
),还有就是Main
方法的类会首先被初始化
**注意:**对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显。
6. 使用与卸载
JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
7. 结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
- 执行了
System.exit()
方法 - 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
三、接口的加载过程
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
四、类的主动使用例题
package com.jvm.classloader;
class Father2{
public static String strFather="HelloJVM_Father";
static{
System.out.println("Father静态代码块");
}
}
class Son2 extends Father2{
public static String strSon="HelloJVM_Son";
static{
System.out.println("Son静态代码块");
}
}
public class InitativeUseTest2 {
public static void main(String[] args) {
System.out.println(Son2.strSon);
}
}
运行结果:
Father静态代码块
Son静态代码块
HelloJVM_Son
Son2.strSon
是调用了Son
类自己的静态方法属于主动使用,所以会初始化Son
类,又由于继承关系,类继承原则是初始化一个子类,会先去初始化其父类,所以会先去初始化父类。
package com.jvm.classloader;
class YeYe{
static {
System.out.println("YeYe静态代码块");
}
}
class Father extends YeYe{
public static String strFather="HelloJVM_Father";
static{
System.out.println("Father静态代码块");
}
}
class Son extends Father{
public static String strSon="HelloJVM_Son";
static{
System.out.println("Son静态代码块");
}
}
public class InitiativeUse {
public static void main(String[] args) {
System.out.println(Son.strFather);
}
}
运行结果:
YeYe静态代码块
Father静态代码块
HelloJVM_Father
要注意子类Son类没有被初始化,也就是Son的静态代码块没有执行!
Son.strFather
是子类Son
访问父类Father
的静态变量strFather
,之前提过对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),所以对于静态字段strFather
,直接定义这个字段的类是父类Father
,所以在执行 System.out.println(Son.strFather)
; 这句代码的时候会去初始化Father
类而不是子类Son
!
package com.jvm.classloader;
class YeYe{
static {
System.out.println("YeYe静态代码块");
}
}
class Father extends YeYe{
public final static String strFather="HelloJVM_Father";
static{
System.out.println("Father静态代码块");
}
}
class Son extends Father{
public static String strSon="HelloJVM_Son";
static{
System.out.println("Son静态代码块");
}
}
public class InitiativeUse {
public static void main(String[] args) {
System.out.println(Son.strFather);
}
}
运行结果:HelloJVM_Father
Son.strFather
所对应的变量便是final static
修饰的,在准备的时候就已经分配内存且初始化为"HelloJVM_Father"了,因此并不会初始化任何类(除了main),仅仅执行System.out.println(Son.strFather);
。这是主动使用的第二点的例外实例,但是final不是重点,重点是编译器把结果放入常量池,如下面这题
package com.jvm.classloader;
import sun.applet.Main;
import java.util.Random;
import java.util.UUID;
class Test{
static {
System.out.println("static 静态代码块");
}
// public static final String str= UUID.randomUUID().toString();
public static final double str=Math.random(); //编译期不确定
}
public class FinalUUidTest {
public static void main(String[] args) {
System.out.println(Test.str);
}
}
运行结果:
static 静态代码块
0.7338688977344875
因为当一个常量的值并非编译期可以确定的,那么这个值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,所以这个类会被初始化。因此final不是重点。
package com.jvm.classloader;
public class ClassAndObjectInitialize {
public static void main(String[] args) {
System.out.println("输出的打印语句");
}
public ClassAndObjectInitialize(){
System.out.println("构造方法");
System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
}
{
System.out.println("普通代码块/初始化块");
}
int ZhiShang = 250;
static int QingShang = 666;
static
{
System.out.println("静态初始化块");
}
}
运行结果:
静态初始化块
输出的打印语句
( 先看 十、初始化块与执行顺序 )
实际上Java代码编译成字节码之后,最开始是没有构造方法的概念的,只有类初始化方法 和 对象初始化方法 。
类初始化方法:编译器会按照其出现顺序,收集:类变量(static变量)的赋值语句、静态初始化块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
对象初始化方法:编译器会按照其出现顺序,收集:成员变量的赋值语句、普通代码块(初始化块),最后收集构造函数的代码,最终组成对象初始化方法,值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。
这题因为没有对类ClassAndObjectLnitialize
进行实例化!只是单纯的写了一个输出语句,因此没有执行对象初始化。
首次主动使用
Java程序对类的使用方式可分为两种:主动使用与被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化
也就是当第二次对类进行主动使用时,类的初始化并不会发生!
package com.jvm.classloader;
class Father{
public static int a = 1;
static {
System.out.println("父类静态代码块");
}
}
class Son{
public static int b = 2;
static {
System.out.println("子类静态代码块");
}
}
public class OverallTest {
static {
System.out.println("Main方法静态代码块");
}
public static void main(String[] args) {
Father father;
System.out.println("======");
father=new Father();
System.out.println("======");
System.out.println(Father.a);
System.out.println("======");
System.out.println(Son.b);
}
}
运行结果:
Main方法静态代码块
======
父类静态代码块
======
1
======
子类静态代码块
2
① 类的主动使用的第六点:Main方法的类会首先被初始化,首先执行main方法静态代码块
② Father father
只是声明了一个引用不会执行什么。
③ 类的主动使用的第一点:new实例变量,父类Father
被初始化,执行类初始化方法。
④ 类的主动使用的第三点:调用类的静态方法。但是父类不会再次初始化一次println(Father.a)
输出1
⑤ 同④,先初始化Son,然后输出2。
五、ClassLoader类加载器
ClassLoader类加载器的作用就是将.class
文件加载到JVM虚拟机中去。
public class ClassLoaderDemo {
public static void main(String[] args) throws ClassNotFoundException {
//获取类加载器
ClassLoader classLoader= ClassLoaderDemo.class.getClassLoader();
//常用三种方式加载类
// 使用ClassLoader.loadClass()来加载类,不会执行初始化块
System.out.println("‐‐ClassLoader.loadClass()‐‐");
classLoader.loadClass("com.gx.reflect.Test1");
// 使用Class.forName(clssName)来加载类,默认会执行初始化块
System.out.println("‐‐Class.forName(className)‐‐");
Class.forName("com.gx.reflect.Test2");
// 使用Class.forName(className,initialize,ClassLoader)来加载类,并指定ClassLoader,// 参数:类名,是否初始化,类加载器
System.out.println("‐‐Class.forName(className,initialize,ClassLoader)‐‐"
Class.forName("com.gx.reflect.Test3", false, classLoader);
}
}
class Test1 {
static {
System.out.println("Test1 静态初始化块");
}
}
class Test1 {
static {
System.out.println("Test2 静态初始化块");
}
}
class Test1 {
static {
System.out.println("Test3 静态初始化块");
}
}
‐‐ClassLoader.loadClass()‐‐
‐‐Class.forName(className)‐‐
Test2 静态初始化块
‐‐Class.forName(className,initialize,ClassLoader)‐‐