文章目录
以下陈述的内容都已HotSpot为基准。
类加载机制
类加载机制是什么
一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程
就是虚拟机的 类加载机制 。
类加载机制的特点
与那些在编译时
需要进行连接工作的语言不同,在Java语言里面,类型的加载和连接都是在程序运行期间
完成,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性多态就是依赖运行期动态加载和动态链接这个特点实现的。例如,如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现。这种组装应用程序的方式广泛应用于Java程序之中。
类加载机制的过程
程序主动
使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化
三个步骤对类进行加载也叫初始化(这里的叫法不太严谨,但是一般都这么说,注意和每个子步骤中的加载和初始化的定义做区别)。如果没有意外,JVM会连续
完成这三个步骤,所以一般把这三个步骤统称为类的加载或类初始化。另外,如果该类的直接父类还没有被加载,则先加载该类的父类。
加载、验证、准备、初始化和卸载这5个阶段的开始顺序是确定的
,类的加载过程必须按照这种顺序按部就班地开始,而解析
阶段则不一定
:它在某些情况下可以在初始化阶段之后
再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
特别需要注意的是,类的加载过程必须按照这种顺序按部就班地“开始”,而不是按部就班的“进行”或“完成”,因为这些阶段通常都是相互交叉地混合式进行的,也就是说通常会在一个阶段执行的过程中调用或激活另外一个阶段。
下图:类的整个生命周期
加载
类的加载,指类加载器
根据查找路径找到并类的.class文件读入内存
,并为之创建
一个java.lang.Class对象
。
类也是一种对象,是java.lang.Class的实例
关于类加载器:
JVM的类加载器通常被称为系统加载器。除了根类加载器之外,其他类加载器都是通过Java语言编写的,开发者可以通过集成ClassLoader基类来创建自己的类加载器。
类加载的来源
- 本地文件系统
- JAR包
- 网络
- 动态编译Java源文件后加载
类加载的时机
什么情况下虚拟机需要开始加载一个类呢?虚拟机规范中并没有对此进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
- 使用前
预先
加载 - 首次
使用时
加载 - 类加载的原则:延迟加载,能不加载就不加载。
连接
类的连接,即把类的.class文件中的内容合并到JRE中,具体可分为三个阶段:
- 验证:检查.class文件内部结构的正确性
- 准备:为类的
静态变量
分配内存
并设置默认初始值
,这些变量所使用的内存都将在方法区
中进行分配。
注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化的时候随对象一起分配到堆中。
- 这些内存都将在方法区中分配
- 非常量(final)的静态数据类型被默认赋对应类型的零值(如0、0L、null、false等),而不是程序中显示的赋值。
- 解析:将类的符号引用替换为直接引用
符号引用和直接引用的区别与关联:
- 符号引用:
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。- 直接引用:
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
初始化
定义
对静态变量显示初始化和静态代码块执行初始化。
注意:
- 此时才是
开始使用程序代码
去初始化类变量和其他资源,之前除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。初始化的顺序为代码排列顺序
。如静态代码块在静态变量声明前,静态变量a在静态代码块和静态变量中分别做了赋值,则a先被静态代码块赋值,后被静态变量赋值语句赋值。并且静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
。//证明:初始化的顺序为代码排列顺序 public class Main { static int a = 1; static { a = 2; } public static void main(String[] args) { System.out.println("a = "+a); //输出 a = 2 } } public class Main { static { a = 2; } static int a = 1; public static void main(String[] args) { System.out.println("a = "+a); //输出 a = 1 } } //证明:可以赋值,但是不能访问 public class Main { static { a = 2; System.out.println("a= "+a); } //报错:非法前向引用(可以编译,不可运行) static int a = 1; public static void main(String[] args) { System.out.println("a = "+a); } } >public class Main { static { a = b; } //报错:无法通过编译 static int a = 1; static int b = 2; public static void main(String[] args) { System.out.println("a = "+a); } }
类构造器<clinit>()
-
定义: 初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有
类变量的赋值动作
和静态语句块static{}中的语句
合并产生的 -
特点:
类构造器<clinit>()
与实例构造器<init>()
不同,它不需要程序员进行显式调用
,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()先执行完毕
。由于父类的构造器<clinit>()先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器<clinit>()对于类或者接口来说并不是必需的
,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()。 -
多线程时: 虚拟机会保证一个类的类构造器<clinit>()在多线程环境中
被正确的加锁、同步
,如果多个线程同时去初始化一个类,那么只会有一个线程去执行
这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法
,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。//静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。 public class Test{ static{ i=0; System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前应用) } static int i=1; }
类初始化的时机
更详细讲述
- 创建类的实例
- 调用某个类的静态变量或者静态方法
- 初始化某个类的子类
- 使用反射机制强制创建某个类或接口对应的java.lang.Class对象
- 直接使用java.exe命令来运行某个主类
对于常量(final)静态(static)类变量,在程序编译时期就可以确定该值(是个常量),所以不会加载对应的类。
类实例化
类实例化的时机
类实例化的过程
-
前提: .class文件已经被加载完毕(经过加载、解析和初始化)
-
系统赋初值:在堆内存中为Xxx类以及其父类(包括间接父类)中的
非静态成员变量
开辟空间,并赋系统默认初值
(0,null,false),因为成员变量有初值,函数才能使用。即使子类覆盖了父类成员变量,仍会给父类所有成员变量分配空间,此时子类中多了一个属性,分态性,分空间存储,分别存储父类和子类的成员变量
-
父类成员变量的显示初始化和构造代码块的初始化
两者的执行顺序就是代码排列顺序
(和静态变量显示初始化和静态构造代码块初始化一样)。但须注意,和前面静态一样,当构造代码块在前而成员变量声明在后时,构造代码块中只能赋值,不能访问
。另外,实际上两者会被自动装入父类构造函数的最前面
。 -
父类构造函数执行
Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有类的构造函数的第一条语句必须是超类构造函数
的调用语句,以保证所创建实例的完整性。 -
成员变量的显示初始化和子类构造代码块初始化
注意事项和父类一样。 -
构造函数初始化
每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义
构造函数,那么JVM会自动生成一个默认无参的
构造函数。如果自己定义了
,则不会再自动生成任何构造函数
。在编译生成的字节码中,这些构造函数会被命名成<init>()方法,参数列表与Java语言书写的构造函数的参数列表相同。
常问问题
一个实例变量在对象初始化的过程中会被赋值几次?
我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。
类的加载机制和实例化的区别
类的加载机制 | 类的实例化 | |
---|---|---|
是什么 | 把类从.class文件加载到虚拟机,为实例化做准备 | 创建一个类的实例(对象)的过程 |
实例化的对象 | 只有类变量按照程序代码被赋值(<clinit>()方法的执行) | 非静态变量的初始化,以及构造方法的执行(<init()方法的执行) |
执行次数 | 一个类只会执行1次 | 一个类可执行多次 |
参考文献
https://blog.csdn.net/ns_code/article/details/17881581 类加载机制
https://blog.csdn.net/ns_code/article/details/17675609 符号引用和直接引用
https://blog.csdn.net/justloveyou_/article/details/72466105 深入理解类的加载时机和加载过程
https://blog.csdn.net/justloveyou_/article/details/72466416 深入理解Java对象的创建过程:类的初始化与实例化