JVM真香系列:轻松理解class文件到虚拟机(上)
回复“000”获取大量电子书
JVM初探
class文件到JVM中,就相当于我们吃饭,食物吃进了肚子里,不同的营养成分被身体不同的器官吸收。
查找class文件并导入到JVM中
(1)通过一个类的全限定名,获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构,转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class
对象,作为对方法区中这些数据的访问入口
获取class文件有哪些方式
.class文件也是需要查找的,以下是查找.class文件的常用方式:
从本地文件系统中加载.class文件
从jar包中或者war包中加载.class文件
通过网络或者从数据库中加载.class文件
把一个Java源文件动态编译,并加载
加载进来后就,系统为这个.class文件生成一个对应的Class对象。
生成Class对象的有哪些方式
1.对象获取:调用person类的父类方法getClaass()
;
2.类名获取,每个类型(包括基本类型和引用)都有一个静态属性,class。
3.Class类的静态方法获取。forName("字符串的类名")写全名,要带包名。 (包名.类名)
1/**
2 * 反射:就是通过class文件对象,去使用该文件中的成员变量,构造方法, 成员方法。
3 * 要想这样使用,首先你必须得到class文件对象,其实也就是得到Class类的对象。
4 * Class类:
5 * 成员变量 Field
6 * 构造方法 Constructor
7 * 成员方法 Method
8 * <p>
9 * 获取class文件对象的方式:
10 * A:Object类的getClass()方法
11 * B:数据类型的静态属性class
12 * C:Class类中的静态方法
13 * public static Class forName(String className)
14 * <p>
15 * 字符串:而不是一个具体的类名。这样我们就可以把这样的字符串配置到配置文件中。
16 */
17public class ClassDemo {
18 public static void main(String[] args) throws ClassNotFoundException {
19 // 方式1
20 User user= new User();
21 Class c = user.getClass();
22 User user1 = new User();
23 Class c2 = user1.getClass();
24 System.out.println(user == user1);// fals
25 System.out.println(c == c2);// true
26
27 // 方式2
28 Class c3 = User.class;
29 // int.class;
30 // String.class;
31 System.out.println(c == c3);
32
33 // 方式3
34 // ClassNotFoundException
35 //我们刚开始学jdbc的时候就见过这种方式
36 Class c4 = Class.forName("com.tian.demo.test.User");
37 System.out.println(c == c4);
38 }
39}
一个Class对象对应一个.class字节码文件。
比如说:User.class。当系统把它找到,并导入进来后,会为它生成一个对应的Class对象。
class字节码文件到Class对象的过程
回到官网中
https://docs.oracle.com/javase/specs/jvms/se8/html/
[Loading] 其实就是我们上面查找class文件并导入到JVM中。
[Linking] 就是对整个class内容进行一系列的校验、为一些变量进行数据准备、把字节码中符号进行解析等操作。
[Initializing] 就是初始化,创建我们使用的对象;User user=new User();
也是对应了网上很多文章以及很多书里所说的类加载过程分
其中链接这里包括验证、准备、解析。
所有就有了这么一个关系图:
上面介绍了这个class文件转载到JVM的过程,那么问题来了。什么问题?
可以自定义一个String类?
我们在自己的项目里创建一个目录java.lang。并在这个目录下创建一个String类。
1package java.lang;
2
3/**
4 * 咱们也搞个String类
5 *
6 * @author 田维常
7 * @version 1.0
8 * @date 2020/11/5 14:22
9 */
10public class String {
11 public static void main(String[] args) {
12 System.out.println("11111");
13 }
14}
这段代码看起来没毛病吧。但是然后运行这段代码居然报异常。
他居然说我的这个类里没有main方法。这不是扯淡吗,这个衣长如果你上网一查,发现说是我们没有编译成class文件
但是我们已经编译成String.class对象了。
这里并不是我们这里代码String类有问题,有问题的是Java自带String类,并且他是有限被找到的,所以当去Java中自带的String类中找main方法,肯定没有。
这就是如果我们系统里存在一个全路径名完全一样的类,类名称一毛一样的情况下,JVM
是不知道我们到底要用哪个类。这个问题暂时先搁置,后面再细聊。
链接阶段做了什么?
JVM
是如何把class文件里的东西存放的呢?
在官网的这个目录下
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
Method Area 方怡过来就是方法区,对,他就是很多人很害怕的JVM运行时数据区之一的方法区。
大致意思:
方法区是各个线程共享的内存区域,在虚拟机启动时创建。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError
异常。
通过这段翻译,我们就大致知道了,上面我们把.class字节码文件导入进来后是存放在方法区的。
导入.class字节码文件的时候,如果是你来解析,你觉得需要做些什么?
验证
首先肯定是要保证被加载类的正确性,也就是做一些校验罢了;
文件格式验证
元数据验证
字节码验证
符号引用验证
这些校验完毕,没问题,那么我们继续。
准备
为类的静态变量分配内存空间,并将其初始化为默认值。
比如说User.java
中有个变量int a;
1 public class InitialDemo {
2 static int a = 10;
3
4 public static void main(String[] args) {
5 System.out.println(a);
6 }
7 }
在这个阶段,会对这些static修饰的变量进行赋值,附一个初始值,这里就是给
1int a =0;
因为int类型的初始值就是0;
如果是String类型,那么初始值就是null。
解析
初始值搞定后,还有就是有部分对象引用的,在.class字节码文件中还是符号,得给指定一个真实引用地址。
换言之,把符号引用变成直接引用。
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
例如,在Class文件中通过javap
命令能查看,它以
CONSTANT_Class_info
、
CONSTANT_Fieldref_info
、
CONSTANT_Methodref_info
等类型的常量出现。
符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。
在编译时,java
类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
比如org.simple.People
类引用了org.simple.Language
类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language
(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。
各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用
直接引用可以是以下三种场景:
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。
如果有了直接引用,那引用的目标必定已经被加载入内存中了。
初始化过程
对类的静态变量,静态代码块执行初始化操作。
1 public class InitialDemo {
2 static int a = 10;
3
4 public static void main(String[] args) {
5 System.out.println(a);
6 }
7 }
这是才正式的给a赋值为10;
在准备阶段是给静态变量a附一个初始值,在初始化阶段才给变量a定义的值。
其初始化顺序为:
面试题
下面这段代码能否通过编译?能编译的话,输出什么?
1public class InitialDemo {
2
3 static int a = 0;
4
5 static {
6 a = 1;
7 b = 2;
8 }
9
10 static int b = 0;
11
12 public static void main(String[] args) {
13 System.out.println(a);
14 System.out.println(b);
15 }
16
17}
结果是:
11
20
a 和 b 唯一的区别就是它们的 static 代码块的位置。
这个结果需要结合准备阶段来理解才更好理解为什么输出1和0.
今天暂时讲到这里,《class文件到虚拟机》相关内容还有挺多,我们明天再接着聊~