赞
踩
目录
类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。Java类加载机制是指在Java程序运行时,将类文件加载到内存中的一系列步骤。Java的类加载机制遵循着“按需加载”的原则,也就是说,只有在需要用到某个类的时候,才会将这个类的相关信息加载到内存中。这种“按需加载”的设计使得Java程序具备了很好的灵活性和效率。
类的生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用、卸载。
类加载过程包括5个阶段:加载、验证、准备、解析、初始化。
step 1:加载
加载过程完成以下3件事:
(1)通过类的完全限定名称获取定义该类的二进制字节流;
(2)将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构;
(3)在内存中生成一个代表该类的Class对象,作为元空间区中该类各种数据的访问入口。
step 2:验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
step 3:准备
(1)类变量是被static关键字修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存;
(2)实例变量不会在这个阶段分配内存,它会在对象实例化时,随着对象一起分配在堆中。注意:实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次;
(3)初始值一般为0:
1)下面的类变量initValue被初始化为0而不是123:
static int initValue = 123;
2)当类变量被final关键字修饰(常量)时,initValue被初始化为123而不是0:
static final int a = 123;
step 4:解析
将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java
的动态绑定。
step 5:初始化
(1)初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>()方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源;
(2)<clinit>()是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问;
示例:以下代码中静态变量i只能赋值,不能访问,因为i定义在静态代码块的后面。
(3)父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
- public class Demo02 {
- public static void main(String[] args) {
- System.out.println(Son.b);//输出结果为2
- }
- }
-
- class Parent {
- static int a = 1;
- static {
- System.out.println("Parent类被加载!");
- a = 2;
- }
- }
-
- class Son extends Parent {
- static int b = a;
- static {
- System.out.println("Son类被加载!");
- }
- }
输出结果:
(4)接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>()
方法。但接口与类不同的是,执行接口的 <clinit>()
方法不需要先执行父接口的 <clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>()
方法。
(5)虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。
(1)当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时。
当 jvm 执行new指令时会加载类。即:当程序创建一个类的实例对象;
当 jvm 执行 getstatic指令时会加载类。即:程序访问类的静态变量;
当 jvm 执行 putstatic指令时会加载类。即:程序给类的静态变量赋值;
当 jvm 执行 invokestatic指令时会加载类。即:程序调用类的静态方法。
- public class Demo03 {
- public static void main(String[] args) {
- // Student student = new Student();//new指令
- // System.out.println(Student.a);//getstatic指令
- // Student.a = 456;//putstatic指令
- Student.dosth();//invokestatic指令
- }
- }
-
- class Student {
- static int a;
- static {
- System.out.println("Student被加载了!");
- }
- public static void dosth(){}
- }
(2)使用 java.lang.reflect包的方法对类进行反射调用时如 Class.forname("..."), 或newInstance() 等等。如果类没初始化,需要触发类的加载。
- public class Demo04 {
- public static void main(String[] args) {
- // try {
- // Class.forName("com.dytx.Student");
- // } catch (ClassNotFoundException e) {
- // e.printStackTrace();
- // }
-
- try {
- Class cls = Student.class;
- cls.newInstance();
- } catch (InstantiationException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
- }
- }
(3)加载一个类,如果其父类还未加载,则先触发该父类的加载。
(4)当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main() 方法的类),虚拟机会先加载这个类。
(5)当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
(1)通过子类引用父类的静态字段,不会导致子类加载。
(2)通过数组定义来引用类,不会触发此类的加载。
(3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
step 1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
step 2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式有 “指针碰撞” 和 “空闲列表” 两种。
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"。
step 3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
step 4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的haxi码、对象的GC分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
step 5:执行<init>构造方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java 程序的视角来看,对象创建才刚开始,<init>构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行new指令之后会接着执行<init>构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。