前导说明:
本文基于《深入理解Java虚拟机》第二版
和个人理解完成,
以大白话的形式期望能用大家都看懂的描述讲清楚虚拟机内幕,
后续会增加基于《深入理解Java虚拟机》第三版
内容,并进行二个版本的对比
。
JAVA栈(Stack)
线程独享此区域。
它的生命周期与线程相同,描述的是Java方法执行的内存区域。即用来运行方法的地方。线程中调用任何一个方法都会在此区域创建一个
栈帧
用于存储局部变量表
、操作数栈
、动态链接
、方法返回地址
等信息。每一个方法从调用直到执行完成,都对应着一个栈帧在JVM中入栈到出站的过程。当然出站后栈帧就会释放。
方法调用的字节码指令,就是由线程执行方法时发出的,这个在方法区-方发表章节已经讲过了,大家可以回顾一下。
- 1.invokevirtual
- 2.invokeinterface
- 3.invokespecial
- 4.invokestatic
- 5.invokedynamic
线程调用方法创建栈帧的大小,或者说是栈帧中局部变量表的大小是确定的,因为线程调用方法时,是直接从方法区的对应的class文件的方法表的属性集合的code属性中直接把方法的字节码指令拿过来,而此部分在编译期就已经确定了大小了。且在整个方法运行过程中局部变量表的大小都不会改变。
异常:
- 1.StackOverflowError:当线程请求的栈的深度大于虚拟机设定的深度,则会抛出此异常。
可以通过动态扩展的配置来抵消此异常。- 2.OutOfMemoryError:如果可以动态扩展,但是扩展时无法申请到足够的内存,就会抛出此异常。
本地方法栈
线程独享此区域。
用于调用native本地方法时使用。其他和虚拟机栈(Java栈)基本一样。
寄存器(程序计数器)
线程独享此区域。
用于保存将线程要执行的下一条指令地址。
之所以要线程独享,因为JVM的多线程实际是靠上下文切换,即cpu处理器切换来实现不同线程的轮询执行的,实际上一个时刻还是执行的一个线程,那么不同的线程有不同的运行进度,为了能中断后恢复线程的正确执行状态,必须保障每个线程都要有自己的寄存器,这样恢复后才能各自从各自的状态继续执行。
异常:
该内存区域JVM规范中指定不会出现OutOfMemoryError错误。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。
分析一下书中的这段直接内存的描述,就是直接内存占用的是机器物理内存的某一块内容,而我们物理内存会通过逻辑划分分成用户空间和内核空间,拿32位操作系统而言,最大支持4G内存,那么其中1G会作为内核空间,另外3G作为用户空间,内核空间多数用来执行系统内核的指令运行,而用户空间一般运行第三方应用程序或我们自己开发的程序。而我们JVM当然就运行在用户空间了,那么用户空间和内核空间又是什么东西?我们在读取磁盘内容到内存的过程需要使用io,而io又分为标准io和直接io,我们一般都是用标准io,即磁盘的内容先进入内核空间,然后由内核空间拷贝到用户空间,即一次读取要经过2次拷贝,这个我们在nio章节会详细讲解,此处暂时了解下。
既然IO中会有2个步骤的文件拷贝,即磁盘到内核空间,内核空间到程序用户空间。在nio章节我们会提到实现nio的epoll方法的实现中他会创建一个mmap来保存数据,而mmap占用的内存是比较特殊的,它是内核空间和用户空间共用的一块内存区域,也即mmap的数据即在内核空间也在用户空间,那么从io的角度他就不需要从内核空间拷贝到用户空间,也就是说省略了一步拷贝,换句话说从磁盘读取过来的数据可以直接操作了,而这个mmap占用的内存就是直接内存。在nio中我们也可以通过特殊的api来在直接内存中创建一个字符数据ByteBuffer用来读入/读出数据。
注意:虽然直接内存不占堆内存,但是他也会占用物理内存,所以特别是使用nio操作的程序,一定要注意不要忽略这个区域的内存分配,否则出问题不好排查。另外直接内存因为是在堆外的空间,所以不会被gc垃圾收集管理,而此处的管理需要单独进行,可以通过api来管理,具体我们在讲nio时会讲到。
配置:可以通过-XX:MaxDirectMemorySize指定分配大小,不指定默认与java堆最大值(-Xmx)一样大。注意分配内存的时候不要忽略掉这个。