一、JVM 简介
【概述】
JVM是Java虚拟机(Java Virtual Machine)的简称,是一种用于计算设备的规范,是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。我们学习使用的基本都是HotSpot虚拟机。
【优点】
优点 | 说明 |
实现跨平台运行 | 让底层代码和运行环境分离开,编写好一份代码后,不用再次修改内容,只用通过安装不同的JVM环境自动进行转换即可运行,在各种系统中无缝连接。 |
自动内存管理 | 专门设计了垃圾回收机制,来自动进行内存的管理,极大的优化了操作。 |
数组下标越界检查 | 提供了数组下标越界的自动检查机制,在检测到越界后,会在运行时自动抛出 |
多态 | 通过相同接口,不同的实例进行实现,完成不同的业务操作 |
二、JVM 内存区域
(一) 简图
(二) 各区域详解
1. Java虚拟机栈
【概述】
Java虚拟机栈绝对算的上是 JVM 运行时数据区域的一个核心,除了一些Native
方法调用是通过本地方法栈实现的(下面会提到),其他所有的Java方法调用都是通过虚拟机栈来实现的。
【特点】
- 线程私有,生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡,不存在垃圾回收问题;
- Java方法调用的数据需要通过虚拟机栈栈进行传递;
- 每一次方法调用都会有一个对应的栈帧被压入栈中(先进后出);
- 每一个方法调用结束后,都会有一个栈帧被弹出(后进先出);
- 栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
- 每个方法执行的同时会创建一个栈帧,一个方法就对应一个栈帧
Java方法有两种返回函数的方式,不管使用哪种方式,都会导致栈帧被弹出。
- 正常的函数返回,使用
return
指令; - 抛出异常(没有
try catch
)。
【栈溢出】
StackOverFlowError
: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出该错误。OutOfMemoryError
: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出该异常。
【栈帧】
这里参考《深入理解 Java 虚拟机》这本书,只是做了简单的总结,具体的细节想了解的可以去读。
名称 | 说明 |
局部变量表 | 用于存放方法参数和方法内部定义的局部变量,在 Java 程序被编译为 Class 文件时确定最大容量。 |
操作数栈 | 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。 |
动态链接 | 指向运行时常量池的方法引用 |
方法返回地址 | 方法正常退出或者异常退出的定义 |
在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在Class
文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
2. 本地方法栈
- 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的
Native
方法服务。 - 在HotSpot虚拟机中和Java虚拟机栈合二为一;
- 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息;
- 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现
StackOverFlowError
和OutOfMemoryError
两种错误。
3. PC寄存器
【概述】
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
【作用】
- 存指向下一条指令的地址,也即将要执行的代码。由执行引擎读取下一条指令;
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
【特点】
- 线程私有:若干个线程是交替执行的,在线程与线程之间切换的时候,需要记录每个线程执行到那一步了;
- 唯一个在Java虚拟机规范中没有规定任何
OutOtMemoryError
情况的区域; - 它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
4. 堆
【概述】
Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。
【特点】
- 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存;
- Java堆是垃圾收集器管理的主要区域,因此也被称作
GC
堆; - JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被使用,则可以直接在栈上分配内存。
- 默认情况下,JVM分配的总内存是电脑内存的 ,初始化的内存是电脑内存的 。
【空间分类】
- 新生代
-
Class
诞生和生长的地方,甚至死亡;Eden
:所有的对象都是在Eden
区new
出来的;Survivor
:Survivor
分1区和2区,两者是动态的。
- 老年代
-
- 对象年龄达到阈值后进入老年代:默认情况下,对象在新生代经历了15次GC后;
- 大对象直接进入老年代:通过以下JVM参数进行设置:
-XX:PretenureSizeThreshold=5242880
;
原因:
- 大对象需要连续的内存空间,而新生代为了安放大对象可能需要多次进行GC,增加开销;
- 新生代种伊甸园区和幸存者区常采用复制算法,需要经常复制对象到不同的区域,而大对象在复制时开销较大。
-
- 动态地根据对象的年龄以及新生代空间使用情况选择对象进入老年代。
HotSpot虚拟机并不一定会严格按照设置的年龄阈值,满足以下条件也能直接进入老年代:
Survivor
区中,年龄从 1 到 n 的对象大小之和超过Survivor
区的50%
时,新生代中年龄大于等于 n 的对象将进入老年代。(这个对象大小总和是按年龄从小到大累加的,并不是同龄对象!)
- 永久代(元空间)
-
- 这个区域用来存放JDK自身携带的
Class
对象; - 存放
Interface
元数据,主要是Java运行时的一些环境或类信息; - 该区域不存在垃圾回收?关闭虚拟机就会释放这个区域的内存。
- JDK1.8以后,永久代被元空间替代
- 这个区域用来存放JDK自身携带的
【大致过程】
对象都会首先在Eden
区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入S0
或者S1
,并且对象的年龄还会加 1(Eden
区->Survivor
区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
来设置。
【内存溢出】
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
: 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小)
5. 方法区
【概述】
方法区别名叫做非堆(Non-Heap),目的就是要和堆分开,被所有线程共享。所有字段和方法字节码以及一些特殊方法,如构造函数、接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域上。但是实例变量存在堆内存中,和方法区无关。
【特点】
- 方法区和堆一样,是各个线程共享的区域;
- 方法区在JVM被启动时创建;
- 方法区的大小跟堆空间一样,可以选择固定大小或扩展;
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出溢出错误
OutOfMemoryError
; - 关闭JVM就会释放这个区域的内存。
【JDK1.8变化】
- JDK1.6:方法区保存在内存结构中,叫做永久代,里面存储了运行时的常量池(包含串池StringTable)、类的信息、类加载器;
- JDK1.8:方法区做为一个概念,保存在本地内存中,叫做元空间,里面存储了运行时的常量池、类的信息、类加载器,此时串池(
StringTable
)储存在堆之中。
【方法区和永久代以及元空间的关系】
方法区和永久代以及元空间的关系很像Java中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是HotSpot虚拟机对虚拟机规范中方法区的两种实现方式。
6. native关键字
【概述】
native
用来修饰方法,凡事带了native
关键字的方法,说明Java的作用范围达不到了,需要去调C语言的库。
【语法】
- 修饰方法的位置必须在返回类型之前;
- 不能用
abstract
修饰,也没有方法体,也没有左右大括号; - 返回值可以是任意类型;
【特点】
- 调用带
native
关键字的方法,该方法会进入本地方法栈; - 本地方法栈会调用本地方法接口(
JNI
);
JNI作用:
通过JNI,我们就可以通过 Java 程序(代码)调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的系统的功能;同时其他技术和系统也可以通过 JNI 提供的相应原生接口开调用 Java 应用系统内部实现的功能。
【存在原因】
Java诞生的时候,C
、C++
横行,要想立足必须调用他们的程序。所以Java专门在JVM的内存区域中开辟了一块标记区域本地方法栈,用来登记native
方法,在最终执行的时候,通过本地方法接口(JNI
)加载本地方法库中的方法。
【应用场景】
我们在日常编程中看到native
修饰的方法,只需要知道这个方法的作用是什么,在企业级应用中很少见。一般涉及硬件系统相关开发可能会用到,比如Java程序驱动打印机。
7. 执行引擎
参考:
[1] 周志明. 深入理解 Java 虚拟机(第3版).