一、初识JVM
我们写好一份Java代码,要将其部署到线上的机器去运行,就要将其打包成.jar
或者.war
后缀的包,再进行部署。其中关键的一步是编译,也就是要把.java
文件编译成.class
字节码文件,有了字节码文件可以通过Java命令来启动一个JVM
进程,由JVM
来负责运行这些字节码文件。所以说,在某个机器上部署某个系统后,一旦启动这个系统,实际上就是启动类JVM
。
我们写好一个个类是通过类加载器把字节码文件加载到JVM
中的,JVM
会首先从main()
方法开始执行里面的代码,它需要哪个类就会使用类加载器来加载对应的类,反正对应的类就在.class
文件中。
注意:如果一个项目中有多个main()
方法,在启动一个.jar
包的时候,就制定了是走哪个main()
方法,所以入口是唯一的。
程序运行机制
二、初识JVM类加载器机制
2.1 引入
问题:JVM什么时候会加载一个类?
最简单的例子是直接从main()
开始执行,比如:
public class kafka{
public static void main(String[] args){
}
}
如果碰到了实例化对象的操作,才把实例化的这个类的.class
文件加载到内存(之前是没有加载进来的)
public class Kafka{
public static void main(String[] args){
ReplicaManager replicaManager = new ReplicaManager();
}
}
首先是包含main()
方法的主类会在JVM启动之后首先被加载到内存中,然后开始执行main()
方法中的代码,碰到需要使用的类,才去加载这个类对应的字节码文件,也就是说按需加载。
2.2 类加载过程
类加载的过程为:加载、验证、准备、解析、初始化、使用、卸载。
2.2.1 加载
加载是类加载的一个阶段,加载过程完成以下三件事:
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个带表该类的
Class
对象,作为方法区中该类各种数据的访问入口。
2.2.2 验证
确保Class
文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
2.2.3 准备
给类
分配内存空间,其次为类变量
分配内存空间,并设定一个默认值。不执行赋值!
类变量是被static
修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为0值,例如下面的类变量value
被初始化为0,而不是123。
public static int value = 123;
如果类变量是常量,那么它将初始化为表达式所定义的值而不是0。例如下面的常量value
被初始化为123而不是0。
public static final int value = 123;
2.2.4 解析
将常量池的符号引用替换为直接引用的过程。
符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在
Class
文件中它以CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methordref_info
等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个Java类将会编译成一个Class文件。在编译时,Java类并不知道所引用的类的实际地址,因此只能使用符号引用类代替。比如org.simple.People
类引用了org.simple.Language
,在编译时People
类并不知道Language
类的实际内存地址,因此只能使用符号org.simple.Language
(假设是这个,当然实际中是由类似于CONSTANT_Class_info
的常量来表示的)来表示Language
类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。直接引用:直接引用可以是
- 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- 相对偏移量(比如,指向实例变量,实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
直接引用是和虚拟机布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载到内存中了。
2.2.5 初始化
正式执行类初始化的代码,在这里才是执行赋值代码等操作,准备阶段
仅仅为类和变量开辟空间。在这个阶段执行初始化操作有很多,比如对于静态代码块的初始化就是在这个阶段执行的(JVM设计者设计先执行静态代码块的机制就是希望开发者把类使用之前的准备工作在这准备好类级别的数据)。要记住,类的初始化就是初始化这个类,和里头的对象无关,只有new
关键字才会构造一个对象。
什么时候会初始化一个类呢?
一般来说包含main()
方法的类是必须立马初始化的,或者说执行到new
对象了,就会把这个对象的类初始化,如果这个类初始化过了,就不用进行第二次初始化。初始化重要的一个规则是:初始化一个类的时候,如果该类的父类没有初始化,(如果父类也没有加载的话)必须先加载并初始化它的父类!
public class ReplicaManager extends AbstractDataManager{
//ReplicaManager继承AbstractDataManager,在初始化ReplicaManager时必须先初始化它的父类
}
2.3 类加载器和双亲委派机制
2.3.1 Java中的类加载器
-
启动类加载器
:负责加载机器上安装的Java目录下的核心类,Java安装目录下有个lib
文件夹存放了Java的核心库,JVM启动后,首先会依托启动类加载器
去加载lib
。 -
扩展类加载器
:就是加载lib/ext
目录,和启动类加载器差不多,但它是启动类加载器的儿子。 -
应用程序类加载器
:负责加载ClassPath
环境变量指定路径中的类,就是把写好的代码加载进内存。 -
自定义类加载器
:自己写的类加载器,继承ClassLoader
类,重写类加载方法。
2.3.2 双亲委派机制
JVM的加载器是有亲子结构的,如图所示,提出了双亲委派机制。
双亲委派机制
:如果应用程序要加载一个类,首先会委派自己的父类加载器去加载,直至传到最顶层的加载器去加载,如果父类加载器在自己的职责范围内没有找到这个类,就会把加载权力下放给子类加载器。总的来说,就是先找父类去加载,不行再由儿子来加载
。先从顶层加载器开始,发现自己加载不到,往下推给子类,这样能保证绝不会重复加载某个类
。
双亲委派的好处:避免了类的重复加载,如果两个不同层级的类加载器可以加载同一个类,就重复了。这使得Java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如java.lang.Object
存放在rt.jar
中,如果编写另外一个java.lang.Object
并放到ClassPath
中,程序可以编译通过。由于双亲委派模型的存在,所以在rt.jar
中的Object
比在ClassPath
中的Object
优先级更高,这是因为rt.jar
中的Object
使用的是启动类加载器,而ClassPath
中的Object
使用的是应用程序类加载器。rt.jar
中的Object
优先级更高,那么程序中所有的Object
都是这个Object
。