JVM学习笔记
一、初识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类加载过程
类加载的过程为:加载、验证、准备、解析、初始化、使用、卸载
-
验证
:就是看字节码文件是否符合规范。 -
准备
:给类分配内存空间,其次为类变量
分配内存空间,并设定一个默认值。不执行赋值! -
解析
:符号引用替换为直接引用的过程 -
初始化
:正式执行类初始化的代码,在这里才是执行赋值代码等操作,准备阶段
仅仅为类和变量开辟空间。在这个阶段执行初始化操作有很多,比如对于静态代码块的初始化就是在这个阶段执行的(JVM设计者设计先执行静态代码块的机制是希望开发者把类使用之前的准备工作在这准备好类级别的数据)。要记住,类的初始化就是初始化这个类,和里头的对象无关,只有new关键字才会构造出一个对象。 -
什么时候会初始化一个类呢?
一般来说包含
main()
方法的类是必须立马初始化的,或者说执行到new对象了,就会把这个对象的类初始化,如果这个类初始化过了,就不用进行第二次初始化。初始化重要的一个规则是:初始化一个类的时候,如果该类的父类没有初始化,(如果父类也没有加载的话)必须先加载并初始化它的父类!public class ReplicaManager extends AbstractDataManager { //ReplicaManager继承AbstractDataManager,在初始化ReplicaManager时必须先初始化它的父类 }
2.3类加载器和双亲委派机制
1、Java中的类加载器
启动类加载器
:负责加载机器上安装的Java目录下的核心类,Java安装目录下有个lib
文件存放了Java的核心库,JVM启动后,首先会依托启动类加载器
去加载lib。扩展类加载器
:就是加载lib/ext
目录,和启动类加载器差不多,但它是启动类加载器的儿子。应用程序类加载器
:负责加载ClassPath
环境变量指定路径中的类,就是把你写好的代码加载进内存。自定义类加载器
:自己写的类加载器,继承ClassLoader
类,重写类加载方法
2、双亲委派机制
JVM的加载器是有亲子结构的,如图所示,提出了双亲委派机制。
双亲委派机制
:如果应用程序要加载一个类,首先会委派自己的父类加载器去加载,直至传到最顶层的加载器去加载,如果父类加载器在自己的职责范围内没有找到这个类,就会把加载权利下放给子类加载器。总的来说,就说先找父类去加载,不行再由儿子来加载。先从顶层加载器开始,发现自己加载不到,往下推给子类,这样能保证绝不会重复加载某个类。
双亲委派的好处:避免了类的重复加载,如果两个不同层级的类加载器可以加载同一个类,就重复了。
三、初识JVM内存区域
我们写好的代码中有很多的类,类中有许多的方法,同时方法中也有许多变量,它们都需要放到合适的区域,这就是JVM为什么要划分出不同内存区域的原因,下面介绍下JVM中的内存区域分类。
3.1存放类的区域——方法区
方法区
主要存放从class
文件中加载进来的类,JDK 1.8后这块区域改名为Metaspace
,即元数据空间,放的还是我们自己写的各种类相关的信息。
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
}
}
假设我们有上面这个例子,JVM首先类加载Kafka.class
到方法区,当程序运行到实例化对象那句,就把ReplicaManager.class
加载到方法区。如果Kafka
类中有静态变量,也一样会进入方法区。
3.2程序计数器——执行代码指令用的
我们写好的Java文件被编译成.class文件后就应对成了一行行的字节码指令,JVM加载类信息到方法区后,会去执行编译出来的字节码指令,执行的时候用到了程序计数器
,它用来记录我们的程序执行到了哪一行字节码指令。由于JVM是支持多线程的,如果写好的代码开启了多个线程,那么每个线程都会有对应的程序计数器,来表示当前线程执行到了哪一条指令。
3.3存放方法的区域——Java虚拟机栈
在main()
方法中有程序计数器来记录指令执行到哪了,但在方法中我们也会定义一些局部变量
,JVM有一块区域是专门用来放局部变量
的,即Java虚拟机栈
,同样的,对于每个线程,它们有自己的Java虚拟机栈
,如果一个线程执行了一个方法,会为这个方法调用创建一个对应的栈帧
,栈帧
中包含着局部变量表、操作数栈、动态链接、方法出口等。比如main线程执行了main()
方法,那就为main()
方法创建了一个栈帧
,并将其压入了Java虚拟机栈
,同时在main()
方法的栈帧中也存放着它的局部变量。
//含有main方法的Kafka类
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.loadReplicasFromDisk();
}
}
public class ReplicaManager {
public void loadReplicasFromDisk() {
Boolean hasFinishedLoad = false;
if (isLocalDataCorrupt()) {
}
}
public Boolean isLocalDataCorrupt() {
Boolean isCorrect = false;
return isCorrect;
}
}
比如上面这段代码,对应的Java虚拟机栈如下。在栈帧里存放了这个方法对应的局部变量之类的数据,包括这个方法执行的其他相关的信息,方法执行完毕之后就出栈。如果isLocalDataCorrupt
方法执行完毕了,就会把isLocalDataCorrupt
方法对应的栈帧从Java虚拟机栈里给出栈,然后如果loadReplicasFromDisk
方法也执行完毕了,就会把loadReplicasFromDisk
方法也从Java虚拟机栈里出栈。
3.4存放对象的区域——Java内存堆
在main方法里创建了ReplicaManager对象的时候,就会在main方法对应的栈帧的局部变量表里,让一个引用类型的“replicaManager”局部变量来存放ReplicaManager对象的地址。
注意:
- 新建的实例在堆内存,实例变量(对象变量)也是在堆内存的
总结一下这个过程:JVM进程启动后,首先会加载含有main
方法的Kafka
类到内存里(方法区),生成一个main线程,然后为这个线程分配一个程序计数器,开始执行程序。先生成main方法的栈帧,压入Java虚拟机栈,然后执行到new ReplicaManager()
时,就会把ReplicaManager
类加载进内存(方法区),接着发现要实例化一个对象,把这个对象的内存分配在Java内存堆中,并在main方法的栈帧里的局部变量表引入一个replicaManager
局部变量,把它指向堆内存中的地址,然后main线程开始执行ReplicaManager
对象中的方法依次把方法压栈,执行完出栈。
总结:类都是加载到方法区里的,而且是按需加载,只加载一次,要new对象时候,对象被分配在堆汇总,执行方法时为这个方法生成栈栈压入虚拟机栈,然后对应变量指向堆内存中的地址,完成方法则出栈。
四、初识垃圾回收机制
一个方法执行完后的结果是怎样的?如图,也就是没有变量指向这个变量了,由于我们在内存堆里创建的对象都是占用内存资源的,所以需要回收它。JVM本身带有垃圾回收机制,它是后台自动运行的线程。它检索这个对象是否有人引用,如果没有任何人指向他,就会把这个对象给回收掉,从内存中清除。