1.类加载的时机与作用
一段java程序在被执行的过程中,需要经历以下几个阶段
1.编译器将.java文件编译成.class 文件。
在class文件中,包含了对应的类或接口的定义信息。可以理解为,class文件是.java文件的二进制字节流表示。内部存放的数据有:常量池,访问标志,当前类索引、父类索引和接口索引的集合,字段表集合(类中声明的变量),方法表集合等,他们共同描述了一个类的信息。值得注意的是,每个class文件一定对应一个类,但反过来未必成立,例如动态生成的类信息,直接生成二进制字节流送入类加载器完成类加载。因此广义上来讲,class并不一定要是一个文件,也可以仅仅就是一串二进制字节流。
编译
2.类加载过程
class文件本质上是对某个类的静态描述,他需要被加载到内存,转化成运行时数据,才能被虚拟机执行,这个加载到内存的过程就是类加载过程。类加载完成之后,在方法区内存放了类的类型信息、常量、静态变量(jdk8之后随类对象存储在堆内)等信息,在堆中存放了与class文件对应的Class对象。顺带提一嘴,通过Class对象,可以获取到类的字段、方法、构造器等信息,它是反射的基础。
image.png
3.字节码执行引擎解释执行指令
在这个阶段,字节码执行引擎根据指令,去操作内存中的数据,完成计算任务。
类加载的作用
可以看到,类加载在程序执行的过程中起到了承上启下的作用,将静态的二进制字节流数据转化为了运行时数据,供执行引擎去操作数据。如图,加载-验证-准备-解析-初始化这五个阶段都属于类加载过程。
类型的生命周期
类加载的时机
虚拟机规范并没有严格规定什么时候开始类加载。但是,规定了6种必须对类进行初始化的情况,它们被称为主动引用。由于初始化类对象需要在加载、验证、准备之后进行,因此这三步必然要在这之前完成。这里前4种是非常常见的,需要深刻掌握。
- 遇到new, getstatic, putstatic, invokestatic这四条字节码指令的时候,如果类型还没有被初始化,则需要初始化。关于这四条指令,也比较好理解。
- new :实例化对象
- getstatic/putstatic: 读取/设置类的静态字段(被final修饰的静态常量除外)
- incokestatic: 调用类的静态方法
- 遇到new, getstatic, putstatic, invokestatic这四条字节码指令的时候,如果类型还没有被初始化,则需要初始化。关于这四条指令,也比较好理解。
- 通过反射对类进行调用的时候,需要确保类已经被初始化过。也好理解,反射的核心是Class对象。
- 当前类被初始化时,要先确保其父类已被初始化。
- 虚拟机启动时,要执行的主类(包含main方法的那个类)要先被初始化。
- 接口中定义了默认方法(被default修饰,可以有方法体的方法,比较少见),当该接口的实现类初始化时,该接口需要先被初始化
- 不理解,没见过,先不写。
除了这6种外,所有其他对类的引用都不会触发类的初始化,他们被称为被动引用。 可以参考《深入理解jvm第三版》p264的例子来理解主动引用与被动引用。
2.类加载的过程
1. 加载
加载是类加载过程的第一步,在类加载器的控制下,将二进制字节流转化为运行时数据。加载阶段需要完成3件事。
- 根据类的全限定名获取对应的二进制字节流
- 将二进制字节流转化成方法区的运行时数据结构
- 在堆中创建代表这个类的java.lang.Class对象 ,作为方法区内数据的访问入口。
这里二进制字节流的获取,有多种方式,源文件也可以有多种形式。比较常见的形式有:
- 从压缩包中获取,如jar包,war包等。
- 在程序运行时,动态计算产生。应用场景:动态代理。
- 最常见的,编译.java文件生成.class文件
2. 验证
验证的作用是确保Class文件内的信息符合虚拟机规范的要求,保证程序运行过程中的安全。
3. 准备
为类变量(即静态变量)分配内存,并设初始值。(0, null, false ...)。
有两点需要留意:
- 从概念上讲,应当在方法区内为静态变量赋初值,但实际上jdk8以后,静态变量随着类对象一起存放在堆内存中。
- 准备阶段并不会为非静态变量(即实例变量)分配内存,实例变量会在对象实例化的时候,分配内存并赋初始。
4. 解析
将常量池中符号引用替换成直接引用。举个例子,在解析完成之前,被引用的目标还没有被加载到内存中,只能先用一个符号来表示,如"java.lang.Object"。解析的作用就是,在引用的对象被加载到内存中以后,将引用替换成指向该对象的指针或句柄。需要被解析的引用有:类或接口的解析、字段解析、方法解析、接口方法解析。
解析的发生时间并没有严格规定,它并非一定发生在准备和初始化之间。
5. 初始化
在初始化之前,加载-验证-准备这3步必然是完成了,部分的解析工作也可能完成了。类对象中的类变量都是系统默认的初始值,初始化阶段的作用就是给这些类变量赋予我们在代码中指定的值。
在初始化阶段,需要执行类构造器(与实例对象的构造器区分开来)。类构造器并非我们直接编写的方法,而是编译器收集类变量的赋值语句和static代码块的产物。说人话就是,初始化阶段就是对静态变量赋值和执行静态代码块的过程。
3.类加载器
1.类与类加载器--如何确定类的唯一性
对于同一份二进制文件,如果将它载入内存的方式不同,那么得到的类也不一样。对于任意一个类,它在java虚拟机中的唯一性由类本身和类加载器共同决定。
2.双亲委派模型
对于程序员而言,类加载架构为3层加载器、双亲委派模型。
类加载模型
-
模型说明:
- 启动类加载器主要用于负责加载<JAVA_HOME>\lib目录下和jt.rt等系统类库下的类,不能被程序直接引用
- 扩展类加载器用于加载扩展类库的类,如<JAVA_HOME>\lib\ext目录下的类
- 应用类加载器用于加载用户类路径下的所有类库。
-
模型工作流程
- 阶段1:向上委派。 类加载器收到加载某个类的请求,它并不直接去加载这个类,而是将请求委派给父类去完成,直到委派到最上层的类加载器。
- 阶段2: 加载。 只要最上层的类加载器能完成加载,那么就由最上层的加载。如果不能,就向下去找剩余的里面最上层能加载的去加载。
-
作用和缺点
作用:优先级保证,确保类的唯一性。举个例子:对于java.lang.Object类,在双亲委派模型下,无论哪个类加载器要加载这个类,都要通过启动类加载器加载,因此在系统中只可能有一个java.lang.Object。如果没有双亲委派模型,那么就可能出现多个Object类,这显然是不合理的。
缺点: 参考破坏双亲委派模型,暂时无法理解。
网友评论