本文是JVM系列第三篇 依据Java虚拟机标准(Java Virtual Machine Specification)界说,将运转时数据区界说如下:
- 程序计数器(Program Counter Register):程序计数器是一块较小的内存区域,它能够看作是当时线程所履行的字节码的行号指示器。在Java虚拟机中,每个线程都有一个程序计数器。
- Java虚拟机栈(Java Virtual Machine Stacks):每个线程在创立时都会创立一个Java虚拟机栈,用于存储办法的局部变量、操作数栈、动态链接、办法出口等信息。每个办法在履行的一同都会创立一个栈帧用于存储这些信息,栈帧在办法履行结束后被毁掉。
- 本地办法栈(Native Method Stacks):本地办法栈与Java虚拟机栈类似,可是它为履行本地办法(Native Method)服务。Java虚拟机栈和本地办法栈的差异在于,Java虚拟机栈为Java办法服务,而本地办法栈为本地办法服务。
- Java堆(Java Heap):Java堆是Java虚拟机中最大的一块内存区域,也是被一切线程所同享的一块内存区域。Java堆用于存储目标实例和数组,是废物搜集器办理的首要区域。
- 办法区(Method Area):办法区用于存储现已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。办法区也是被一切线程所同享的一块内存区域。
- 运转时常量池(Runtime Constant Pool):运转时常量池是办法区的一部分,用于寄存编译期生成的各种字面量和符号引证,以及运转时生成的一些常量。
- 直接内存(Direct Memory):直接内存并不是Java虚拟机运转时数据区的一部分,可是它也是与Java堆密切相关的一块内存区域。直接内存是经过运用Native函数库直接分配堆外内存的办法来进步内存运用功率。
运转时数据区的区分也跟着JDK的发展不断变迁,如下图所示
在JDK 1.8 中加入了元数据区的概念,将原来保存在办法区中的运转时常量池和类常量池都包含其间,下面依据这个版别叙述运转时区域
Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack)是Java虚拟机运转时数据区之一,用于存储办法的局部变量、操作数栈、动态链接、办法出口等信息。每个线程在创立时都会创立一个Java虚拟机栈,Java虚拟机栈与线程同生命周期。
Java虚拟机栈能够看作是Java办法履行的内存模型。每逢一个办法被调用时,Java虚拟机就会为该办法创立一个栈帧(Stack Frame),用于存储该办法的局部变量、操作数栈、动态链接、办法出口等信息。当办法履行结束时,对应的栈帧就会被弹出,毁掉。
Java虚拟机栈由一系列栈帧(Stack Frame)组成,每个栈帧对应一个办法的履行。栈帧包含了办法的局部变量表、操作数栈、动态链接、办法出口等信息。
局部变量表
它界说为数字数组,首要用于存储办法参数和界说在方体内的局部变量,包含根本数据类型,目标引证,以及returnAddress类型。它建立在线程的栈上,是线程的私有数据,因而不存在数据的安全问题。 局部变量表所需的容量在编译期间确认,在运转期间是不改动其容量。
操作数栈
用于存储操作数和中间成果,它是一个后进先出的栈,在办法履行的进程中,依据字节码指令、往栈中写入或取出数据,即入栈/出栈。字节码指令将值压入操作栈,其他的字节码指令将操作数取出栈,进行操作之后再将成果压入栈。操作包含:仿制、沟通、求和等。
动态链接
动态链接用于指向运转时常量池中该办法所运用的符号引证 在介绍动态链接之前先说说静态链接,即字节码文件被装载进JVM内部时,假如被调用的目标办法在编译期可知,且运转期间保持不变时。这种情况下将调用办法的符号引证转换为直接引证的进程称之为静态链接。可是,假如被调用办法在编译期间无法被确认下来,只能在程序运转时将调用办法的符号引证转换为直接引证,由于这种引证转换的进程具备动态性,被称为动态链接。
办法回来地址
办法出口用于指向办法履行结束后的回来地址。
当一个办法开端履行后,能够经过两种办法退出该办法。
第一种是履行引擎遇到办法回来的字节码指令,此刻回来值会传递到上层调用者,这种办法称为正常完结出口。
别的一种退出办法是在办法履行中遇到反常,这个反常在办法体内没有得到处理,就会导致办法退出,这种办法称为反常完结出口。由于是反常退出,就不会给上层调用者任何回来值。
不管采取上面那种退出办法,办法都会到处调用它的方位,程序才干持续履行。办法在回来的时候需求在栈帧中保存一些信息,用来康复调用该办法的上层办法的履行状态。这里能够经过办法调用者的程序计数器寄存回来地址,假如是正常退出办法,上层办法会从程序计数器中保存的地址持续履行接下来的步骤。假如是反常退出的情况,回来地址就需求反常处理器来确认了。
Java虚拟机栈的巨细能够经过发动参数-Xss来设置,默认值为1M。假如Java虚拟机栈空间缺乏,将会抛出StackOverflowError反常;假如Java虚拟机无法为新的栈帧分配空间,将会抛出OutOfMemoryError反常。
程序计数器
程序计数器(Program Counter Register)是Java虚拟机运转时数据区的一部分,它能够看作是当时线程所履行的字节码的行号指示器。每个线程都有一个独立的程序计数器,用于记载当时线程正在履行的字节码的行号。
程序计数器在Java虚拟机中归于线程私有的内存区域,它的效果是记载当时线程履行的字节码指令地址。在Java虚拟机的多线程环境中,程序计数器能够确保线程切换后能够康复到正确的履行方位。
程序计数器在Java虚拟机中并不是一个内存区域,而是一个寄存器。它的巨细与所履行的Java虚拟机完结相关,通常为32位或64位。由于程序计数器仅仅一个指示器,它不参与办法调用和履行的进程,因而在Java虚拟机标准中并没有规定程序计数器需求遵循特定的内存模型。
程序计数器的首要效果有两个:一是确保线程切换后能够康复到正确的履行方位;二是为Java虚拟机的字节码履行引擎提供“跳转”、“循环”、“分支”、“反常处理”等功能,使得Java虚拟机能够正确地履行Java程序中的各种操控流句子。
本地办法栈
本地办法栈与虚拟机栈所发挥的效果是非常类似的,它们之间的差异是虚拟机栈为虚拟机履行Java办法(也就是字节码)服务,而本地办法栈为虚拟机所运用到的Native办法服务。本地办法栈也会抛出StackOverflowError和OutOfMemoryError反常。
说白了,本地办法(Native Method)就是一个Java调用非Java代码的接口。 当Java运用需求与Java之外的环境交互时就需求运用本地办法,特别与底层体系、操作体系以及硬件打交道时就会用到本地办法。我们能够把本地办法理解为一种沟通机制:它提供了一个对外的简练的接口,让我们无需去了解Java运用之外的细节。
那么JVM是如何运用Native Method的呢?当一个类第一次被运用时,类的字节码会被加载到内存,在字节码的进口维持着该类一切办法描述符的list,包含:办法代码来源,参数,办法描述符(例如:public)等等。
假如办法描述符是native,一同描述符块将有一个指向该办法完结的指针,而具体完结在DLL文件内,此刻DLL文件会被操作体系加载到Java程序的地址空间里。当一个带有本地办法的类被加载时,其相关的DLL并未被加载,因而指向办法完结的指针并不会被设置。当本地办法被调用之前, DLL才会被加载,即经过调用java.system.loadLibrary()完结的。
Java堆
Java堆是Java虚拟机所办理内存中最大的一块,在虚拟机发动时创立,被一切线程同享。Java目标实例以及数组都在堆上分配。堆的巨细能够是固定的,也能够依据计算的需求进行扩展,假如不需求更大的堆,则能够缩短。堆的内存不需求是接连的。Java虚拟机完结能够为程序员或用户提供对堆初始巨细的操控,假如能够动态扩展或缩短堆,还能够操控堆的最大和最小巨细。
Java堆是废物搜集器办理的首要区域,所以也被称为GC堆。从内存收回的视点来看,由于现在搜集器根本都选用分代搜集算法,所以Java堆中还能够细分为:新生代和老年代;新生代再细分就是:Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的视点来看,线程同享的Java堆中或许区分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不论如何区分,都与寄存内容无关,不管哪个区域,寄存的都仍然是目标实例;进一步区分的目的是为了更好的收回内存,或许更快地分配内存。
办法区
办法区和堆相同是线程同享的内存区域,它用来寄存被虚拟机加载的类型信息、运转时常量池、静态变量、JIT代码缓存、域信息、办法信息等。办法区(Method Area)与Java堆相同,是各个线程同享的内存区域,有如下特色:
- 办法区在JVM发动的时候被创立,而且它的实践的物理内存空间和Java堆区相同都能够是不接连的。
- 办法区的巨细,和堆空间相同,能够挑选固定巨细和可扩展。
- 办法区的巨细决定了体系能够保存多少个类,假如体系界说了太多的类,导致办法区溢出,虚拟机就会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGenspace或许 java.lang.OutOfMemoryError: Metaspace。
- 封闭JVM就会开释这个区域的内存。
元数据空间
在JDK 8中,新增了一个元数据区(Metaspace)来代替永久代(PermGen),用于存储类的元数据信息。元数据区是Java虚拟机运转时数据区的一部分,首要用于存储类的元数据信息,如类名、办法名、字段名、拜访修饰符、注解等。
在永久代中,存储类的元数据信息需求指定初始巨细和最大巨细,而且会遭到废物收回器的影响。而在元数据区中,存储类的元数据信息不再遭到初始巨细和最大巨细的约束,而是会依据需求动态地分配和收回内存空间。此外,元数据区也不再运用废物收回器进行废物收回,而是运用与Java堆相同的废物收回机制。
元数据区的长处在于,它能够防止永久代中出现的类加载器走漏和元数据区满导致的OutOfMemoryError等问题。一同,元数据区的动态分配和收回机制也能够更好地习惯不同的运用场景和运用需求。
元数据区的巨细能够经过发动参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize来设置,假如元数据区空间缺乏,将会抛出OutOfMemoryError反常。
堆、办法区和虚拟机栈的联系整理
在右边创立了AppMain 类,在运转时JVM 会把AppMain的信息放入到办法区,由于办法区会寄存类型信息。一同main 的办法自身也会放入到办法区。接下来的new Sample(“测验1”)的句子中Sample的自界说目标会放到堆里面,而对应的test1 运用会放入到虚拟机栈中,对应的test1.printName()办法的履行会在虚拟机栈中的栈帧中经过指令履行完结。别的下面的class Sample也是放到办法区中的,声明的private name,其间name的引证放在虚拟机栈中,name对应的目标放在堆中。对应的printName办法是放在办法区中的。
运转时常量池
运转时常量池(Runtime Constant Pool)是Java虚拟机运转时数据区的一部分,用于存储编译期生成的各种字面量和符号引证。在Java源代码编译为字节码文件时,其间包含了一些常量,如字符串、数字、类和办法的符号引证等,这些常量会被编译器放到运转时常量池中。
运转时常量池中存储的常量包含两种类型:字面量和符号引证。字面量常量包含字符串、数字、布尔值、字符等;符号引证常量包含类和接口的全限定名、字段的称号和描述符、办法的称号和描述符等。
运转时常量池的巨细是在类加载时确认的,而且能够经过发动参数进行设置。在运转时,Java虚拟机需求运用运转时常量池中的常量来履行字节码指令,如字符串拼接、办法调用等。假如常量池空间缺乏,将会抛出OutOfMemoryError反常。
在JDK 8及今后的版别中,运转时常量池(Runtime Constant Pool)被放置在Java堆(Heap)中。在JDK 7及以前的版别中,运转时常量池被放置在永久代(PermGen)中。
直接内存
直接内存(Direct Memory)是Java虚拟机运转时数据区的一部分,与Java堆和办法区等一同构成了Java虚拟机的内存模型。直接内存通常是经过JavaNIO库中的ByteBuffer.allocateDirect()办法来创立的。
直接内存与Java堆上的目标不同,它直接运用操作体系的内存空间,而不是受限于Java堆的巨细。因而,直接内存能够防止在Java堆和操作体系之间进行频繁的数据传输,从而进步了程序的功能。
直接内存的运用办法类似于Java堆中的内存,能够经过调用ByteBuffer的get()和put()办法来读写数据。在运用结束后,能够经过调用ByteBuffer的clear()办法来开释直接内存,或许经过调用System.gc()办法来触发废物收回器收回直接内存。
需求留意的是,直接内存的分配和开释不由Java虚拟机来办理,而是由操作体系来办理。因而,在创立很多直接内存时,需求留意防止出现OutOfMemoryError反常和操作体系资源的耗尽等问题。此外,直接内存也会遭到操作体系的约束,例如单个进程能够运用的直接内存巨细等。
参考: 1.17张图带你了解,JVM 运转时数据区