JVM内存模型
JVM的内存模型也就是JVM中的内存布局,不要与java的内存模型(与多线程相关)混淆。
下图是jdk8 jvm内存模型图:
程序计数器
程序计数器是当前线程所执行的字节码的行号指示器。
JVM支持多个线程同时运行,每个线程都会根据CPU时间片来回切换,那么如果当前线程获得时间片了,怎么知道它目前要执行哪条指令呢,所以需要一个地方记录当前线程所执行的指令行号,这个地方就是程序计数器,每一个线程都有自己私有的程序计数器。
倘若当前执行的是JVM的方法,则该寄存器中保存当前执行指令的行号;倘若执行的是native方法,则PC寄存器中为空。
我们可以看一下程序计数器里面的具体内容。找一个class文件,使用javap命令输出对应的字节码,可以发现每个指令前面都有序号,可以认为他们就是程序计数器的内容。
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <Hello World>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈
每个线程有一个私有的虚拟机栈,随着线程的创建而创建,随着线程死亡而死亡。每当线程调用一个java方法时,jvm都会在该线程的虚拟机栈中压入一个新的栈帧,当方法调用完成后,对应的栈帧就会出栈,当虚拟机栈中所有的栈帧都出栈后,线程也就结束了。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈帧用于存储局部变量表、操作数栈、动态链接、方法出口(返回地址)等信息。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小,方法的参数也是存在局部变量表中。
操作数栈用来存储方法执行过程中产生的中间变量。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。我们知道Class文件的常量池有存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
虚拟机栈会有两种异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆
Java堆存放对象实例以及数组,可以根据虚拟机参数-Xmx和-Xms来控制堆的大小。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。Java堆中可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间。
方法区
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
方法区、永久代与元空间:
- 方法区是JVM的规范,而永久代和元空间是方法区的两种不同的实现。
- JDK7及以前方法区的实现为永久代,是在堆上分配。
- JDK8及以后方法区的实现为元空间,是在本地内存上分配。
元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
- -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
- -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
JVM中存在多个常量池,比如类文件常量池、字符串常量池。字符串常量池是直接分配在堆上(jdk8以前是永久代),而类文件常量池等是在方法区,也就是元空间。运行时常量池是在类加载后的一个内存区域,也在元空间。
直接内存
直接内存不是虚拟机运行时数据区的一部分,直接内存的分配不会受到JVM的限制,但是会受到物理内存的限制,内存不足时出现OutOfMemoryError。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
更多精彩内容关注本人公众号:架构师升级之路