强烈推荐
不管是找工作还是提升水平,都建议读一下《深入理解Java虚拟机》这本书,详细讲解了JVM中的内存管理、类加载过程、垃圾回收以及最重要的性能调优实战。
本博客也是参考了这本书,有不对的地方还请指正。在这里给大家准备了电子版和视频教程,详情请扫右侧公告栏中二维码后,回复【java虚拟机】获得。
一、快速扫盲
1. JVM是什么
JVM是Java Virtual Machine的缩写,即咱们经常提到的Java虚拟机。虚拟机是一种抽象化的计算机,有着自己完善的硬件架构,如处理器、堆栈等,具体有什么咱们不做了解。目前我们只需要知道想要运行Java文件,必须先通过一个叫javac的编译器,将代码编译成class文件,然后通过JVM把class文件解释成各个平台可以识别的机器码,最终实现跨平台运行代码。
2. JDK、JRE、JVM之间的关系
- JDK:全称为Java Development Kit,汉语为java开发工具包,即所有有关java的东西都包含在里面,比如运行环境JRE、java的核心代码、JVM等等。
- JRE:全称为Java Runtime Environment,汉语为java运行环境,即想要运行java文件必须先有java的环境才行,jre就是提供了这么一个环境。
- JVM:上面已经提到了JVM,它是java最核心的部分。
简单用一张图来理解这三个的关系:
3. jvm的组成成分
不了解jvm的同学看到这张图后可能会有点懵逼,不过没关系,放这张图只是想让你了解jvm中有三块内容非常重要,1.java代码如何执行?2.内存如何管理?3.线程资源如何利用?脑袋里有个印象即可,带着问题去学习。文章最下方有本博客参考书和视频下载地址。
4. 运行java文件的大概流程
想要运行java的源文件,必须要经过javac编译器编译成.class文件,也就是字节码文件。然后通过jvm中的解释器,解释成特定机器上的机器码。每种机器上的解释器是不一样的,我们经常用的也就是windows和linux系统,这也是为什么java能够跨平台的原因。当一个程序从开始运行,虚拟机就开始实例化,多个程序运行就会存在多个虚拟机实例,程序退出或者关闭,虚拟机实例也将随之消亡,多个虚拟机之间的数据是不共享的。
二、JVM运行时数据区
1. 运行时数据区域组成
虚拟机在执行java程序时,会将自己管理的内存划分为几个区域,每个区域都有自己的用途,并且创建时间和销毁时间也不一样。在程序运行时的内存区域主要可以划分为五个,分别是:方法区、堆、虚拟机栈、本地方法栈、程序计数器。可以用下面的图来描述:
2. Java堆
Java堆是java虚拟机所管理的内存中最大的一块,是被所有线程都共享的内存区域。存在的唯一目的就是存放对象实例,几乎所有的对象实例都在这里进行分配内存。不过目前随着技术的不断发展,也并不是所有的对象实例都在堆中分配内存,可能也存在栈上分配。由于所占空间大,又存放各种实例对象,因此java虚拟机的垃圾回收机制主要管理的就是此区域,详细的垃圾回收方法以后会提到。JVM规范中规定堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。并且可以通过-Xmx和-Xms来扩展堆的内存大小,如果在堆中没有足够的内存为实例分配,并且堆也无法在扩展时,就会报OutOfMemoryError异常。
3 方法区
跟Java堆一样,方法区是各个线程共享的内存区域,此区域是用来存储类的信息(类的名称、字段信息、方法信息)、静态变量、常量以及编译器编译后的代码。JVM规范中并不区分方法区和堆,只把方法区描述为堆的逻辑部分,但是它却有一个别名叫做非堆(Non-Heap),目的就是与Java堆区分开。根据垃圾回收机制中分代回收的思想,如果在HotSpot虚拟机上开发,可以把方法区称为“永久代”(只是可以这么理解,但实质是不一样的),垃圾回收机制在Java堆中划分一个部分称为永久代,用此区域来实现方法区,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,而不必为方法区开发专门的内存管理器。
运行时常量池
运行时常量池是方法区的一个部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容会在类加载后进入方法区的运行时常量池中。Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。
4. 程序计数器
虽然在上图中程序计数器的面积很大,但实际上它是一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器。字节码解释器在工作中时下一步该干啥、到哪了,就是通过它来确定的。大家都知道在多线程的情况下,CPU在执行线程时是通过轮流切换线程实现的,也就是说一个CPU处理器(假设是单核)都只会执行一条线程中的指令,因此为了线程切换后能恢复到正确的执行位置,每个线程都要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。很明显,程序计数器就是线程私有的。如果线程正在执行的是一个java方法,程序计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的Native方法,程序计数器记录的值为空(Undefined),此内存区域是java中唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
5. Java虚拟机栈
我们经常会把java内存粗糙的分为两个部分,堆和栈,Java虚拟机栈就是栈这一部分,或者说是虚拟机栈中局部变量表部分。跟程序计数器一样,虚拟机栈也是线程私有的,它的生命周期跟线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame),每个栈帧对应一个被调用的方法,栈帧中用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法从开始执行到结束就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表:顾名思义,他就是用来存储方法中的局部变量(包括在方法中生命的非静态变量以及函数形参),对于基本数据类型,直接存值,对于引用类型的变量,存储指向该对象的引用。由于它只存放基本数据类型的变量、引用类型的地址和返回值的地址,这些类型所需空间大小已知且固定,所以当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全可以确定的,在方法运行期间也不会改变局部变量表的大小。
- 指向运行常量池的引用:在方法执行过程中难免会使用到类中定义的常量,因此栈帧中要存放一个指向运行时常量池的引用。
- 方法返回地址:当一个方法执行结束后,要返回到之前调用它的地方,因此在栈帧中需要保存一个方法返回地址。
6. 本地方法栈
本地方法栈与虚拟机栈的功能非常的相似,区别不过是虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机执行Native方法服务。有的虚拟机并不会区分本地方法栈和虚拟机栈,比如Sun HotSpot虚拟机直接将两个合二为一。