最近在学习 Java 虚拟机底层的工作原理,找来了一些相关的书籍看,在阅读过程中看到一个例子,觉得很不错,在此记录一下,主要讲述虚拟机是如何执行 main 方法中的第一条指令的。
要理解 Java 虚拟机,首先要意识到它可能是指以下三种不同的东西:
1.抽象规范。
2.一个具体的实现。
3.一个运行中的虚拟机实例。
知道了这点小小的区别以后,我们再往下看,要特别提醒的是,下文所描述的步骤,仅仅是虚拟机多种实现方式中的一种,不同的虚拟机实现可能会用完全不同的方式。
1 | package com.test; |
下面看一下 Java 虚拟机是如何执行 Demo 程序中 main 方法的第一条指令的。
要运行 Demo 程序,首先要以某种「依赖于实现」的方式告诉虚拟机「Demo」这个名字。之后,虚拟机将找到并读入相应的 class 文件「Demo.class」,然后它会从导入的 class 文件里的二进制数据中提取 Demo 这个类的信息并放到方法区中。通过执行保存在方法区中的字节码,虚拟机开始执行 main() 方法,在执行时,它会一直持有指向 Demo 类的常量池的指针。
注意,虚拟机开始执行 main() 方法字节码的时候,尽管 Lava 类还没被装载,但是和大多数虚拟机的实现一样,它不会等到把程序中用到的所有类都装载后才开始运行程序。恰好相反,它只在需要时才装载相应的类。
main() 方法的第一条指令告知虚拟机给列在常量池第一项的类分配足够的内存。所以虚拟机使用指向 Demo 类常量池的指针找到第一项,发现它是一个对 Lava 类的符号引用,然后它就检查方法区,看 Lava 类是否已经被装载了。
这个符号引用仅仅是一个给出了类 Lava 的全限定名「Lava」的字符串。为了能让虚拟机尽可能快地从一个名称找到类,设计者应当选择最佳的数据结构和算法。这里可以采用各种方法,如散列表、搜索树等等。同样的算法也可以用于实现 Class 类的 forName() 方法,这个方法根据给定的全限定名返回 Class 引用。
当虚拟机发现还没有装载过名为「Lava」的类时,它就开始查找并装载文件「Lava.class」,并把从读入的二进制数据中提取的 Lava 类信息放入方法区中。
紧接着,虚拟机以一个直接指向方法区 Lava 类数据的指针来替换常量池第一项(就是那个字符串「Lava」),以后就可以用这个指针来快速地访问 Lava 类了。这个替换过程称为常量池解析,即把常量池中的符号引用替换为直接引用。这是通过在方法区中搜索被引用的元素实现的,在这期间可能又需要装载其他类。在这里,我们替换掉符号引用的「直接引用」是一个本地指针。
终于,虚拟机准备为一个新的 Lava 对象分配内存。此时,它又需要方法区中的信息。还记得刚刚放到 Demo 类常量池第一项的指针吗?现在虚拟机用它来访问 Lava 类信息(此前刚放到方法区中的),找出其中记录的这样一个信息:一个 Lava 对象需要分配多少堆空间。
Java 虚拟机总能够通过存储于方法区中的类信息来确定一个对象需要多少内存,但是,某个特定对象事实上需要多少内存,是跟特定实现相关的。对象在虚拟机内部的表示是由实现的设计者来决定的。
当 Java 虚拟机确定了一个 Lava 对象的大小后,它就在堆上分配这么大的空间,并把这个对象实例的变量 speed 初始化为默认值0。假如 Lava 类的超类 Object 也有实例变量,则也会在此时被初始化为相应的默认值。
当把新生成的 Lava 对象的引用压入栈中,main() 方法的第一条指令也完成了。接下来的指令通过这个引用调用 Java 代码(该代码把 speed 变量初始化为正确值 5)。另外一条指令将用这个引用调用 Lava 对象的 flow() 方法。
总结:
通过上面的讲解,我们可以得出类加载器的基本工作顺序:
1.加载(Loading):JVM 通过全限定名查找到类,然后通过类加载器(ClassLoader)将类的 .class 文件加载到内存中,并将类的二进制数据转换为运行时数据结构。
2.连接(Linking),包括三个阶段:
验证(Verification):确保加载的类的正确性,要符合 Java 语言规范;
准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值;
解析(Resolution):将常量池中的符号引用转换为直接引用,例如将方法名转换为方法所在的内存地址。
3.初始化(Initialization):执行各种初始化操作(比如把类的静态变量初始化为正确的值、执行类的静态代码块)。
到这里类加载的过程就结束了,初始化完成后,类的实例就可以被创建了,并且类的成员变量可以被访问和修改。类加载器是一个非常重要的组件,它确保了类的正确加载和初始化,JVM 提供了三种类加载器:
(A)启动类加载器(Bootstrap ClassLoader):负责加载 Java 的核心类库(如 java.lang.* 等);
(B)扩展类加载器(Extension ClassLoader):负责加载扩展类库(如 lib/ext 目录下的 jar 包);
(C)应用程序类加载器(Application ClassLoader):负责加载应用程序的类路径(classpath)上的类。
此外,开发者还可以自定义类加载器,以满足特定的需求(如从网络上加载类、加密类的字节码等)。
Java 的类加载器默认采用双亲委派模型:当一个类加载器(如 Application ClassLoader)收到一个类的加载请求时,它首先将请求委托给它的父类加载器(如 Extension ClassLoader)去处理,如果父类加载器无法处理,才由自己处理。父类加载器收到加载请求后,同样也会先委托给它的父类加载器(如 Bootstrap ClassLoader)去完成,如果父类加载器能够找到并加载所需的类,那么它就直接返回这个类给子类加载器,子类加载器就不再需要自己去加载这个类了。并且,无论这个类是由哪个类加载器加载的,只要它被加载过,就会被缓存起来,当再次需要加载这个类时,类加载器会直接从缓存中获取,而不是重新加载。
这种双亲委派的机制可以防止恶意代码通过自定义类加载器来加载恶意的类,从而保护了 Java 应用程序的安全,同时也避免了类的重复加载。
(完)