Java虚拟机整体篇幅如下:
本篇文章主要讲解JVM运行时数据区,所以我们按照线程是否私有的维度将本篇文章一分为二,分为线程私有数据区和所有线程共有的数据区。而在线程私有的数据区又可以分为程序计数器、虚拟机栈、本地方法栈;所有线程共有的数据区又可以分为Java堆、方法区。
思维导图如下:
JVM运行时数据区.png
事实上,JVM在执行Java代码时都会把内存分为几个部分,即数据区域来使用,这些区域都有自己的用途,并随着JVM进程的启动或者用户线程启动和结束或销毁。接下来我们通过下面这幅图,我们一个一个细数一下JVM运行时的数据区结构。
JVM运行时数据区.png
所以本片文章的主要内容也如此:
程序计数器(Program Counter Register),也称作PC寄存器。想必学过汇编语言的朋友对程序计数器这个概念应该并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序机器便自动加1或者根据转移指针得到下一条指令的地址,依次循环,直至执行完所有的指令
虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言的程序计数器的功能在逻辑上是等同的,也就是说用来指示执行那条指令的。由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能相互干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
记录当前线程执行到的字节码的行号,字节码的解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
JVM的多线程是通过线程轮流切换并分配由处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有程序计数器。
此内存区域是JVM里面唯一一个不会发生内存溢出(OOM OutOfMemoryError)的区域。
虚拟机栈也就是我们常常说的栈,跟C语言的数据段中栈类似,事实上,Java栈是Java方法执行的内存模型。Java栈中存放的是一个个栈帧。并且是线程私有,生命周期与线程相同,描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),由于存储局部变量表、操作数栈、动态链链接、方法出口等信息。每一个方法的执行就是对应栈帧在虚拟机栈中的入栈、出栈的过程。当一个线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧移除栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java的栈顶。讲到这里,大家就应该会明白为什么在使用递归方法的时候容易导致栈内溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个Java栈的模型
虚拟机栈.png
栈.png
描述Java方法执行的内存模型。每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需要的数据,称为栈帧,一个栈帧包含如:局部变量表、操作数栈、动态链接、方法出口等信息。
JVM是基于栈的,所以每个方法从调用到执行结束,就对应一个栈帧在虚拟机栈中入栈和出栈的整个过程。
局部变量表(编译器可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息。值得注意的是:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
存放编译器可知的各种基本数据类型、对象引用类型和returnAddress类型(指向一条字节码指令的地址:函数返回地址)。long、double、占用两个局部变量控件的Slot。局部变量表所需要的内存空间在编译器确定,当进入一个方法时,方法在栈帧中所需要分配的局部变量控件是完全确定的,不可动态改变大小。
后进先出LIFO,最大深度由编译期决定。栈帧刚建立时,操作数栈为空,执行方法操作时,操作数栈用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。操作数栈可以存放一个JVM中定义的任意数据类型的值。在任意时刻,操作数栈都有一个固定的栈深度,基本类型除了long、double占用两个深度,其他占用一个深度。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的服务引用为参数。这些符号引用,一部分会在类加载阶段或者第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接应用,这部分称为动态链接。
如果线程请求的栈深入大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机栈可以动态扩展(大部分虚拟机允许动态扩展,也可以设置固定大小的虚拟机栈),但是无法申请到足够的内存,会抛出OutOfMemorError。
本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出和StackOverflowError和OutOfMemoryError异常。在JVM规范中,并没有对本地方法的具体实现方法以及数据结构做强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
为JVM所调用到的Native即本地方法服务
如果线程请求的栈深入大于虚拟机所允许的深度,将抛出StackOverflowError异常。
Java堆可以说是虚拟机中最大的一块内存了。它是所有线程共享的内存区域,几乎所有的实例对象都是在这块区域中存放。当然,随着JIT(just in time,及时编译技术) 编译器的发展,所有对象在"堆"上分配也变得不那么"绝对"了。同时Java堆也是垃圾收集器管理的主要区域。由于现在收集器基本上采用的都是分带收集算法,所有Java堆又可以细分为:"新生代"和"老年代"。再细致分就是把新生代分为:Eden空间、From Survivor空间、To Survivor空间。
所有线程共享的一块内容区域,在虚拟机开启的时候创建。
存放对象实例,几乎所有对象的实例都在这里进行分配。堆可以处理物理上不连续的内存空间,只要逻辑上连续的就可以。
堆可以是固定大小的,也可以通过设置配置文件来设置该为可扩展的。如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常
方法区在JVM中也是一个非常重要的区域,在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。它与堆一样,是被线程共享的区域,很容易理解,我们在写Java代码时,每个线程都可以访问同一个类的静态变量。在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。在方法去还有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表现形式,在类和接口被加载到JVM后,对应的运行时常量池,在运行期间也可以将新的常量放入运行时常量池,比如String的intern方法。
在JVM规范中,没有强制要求方法区必须实现垃圾回收,很多人习惯将方法区称为"永久代",是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾处理器可以像堆区一样管理这部分的区域,从而不需要专门为这部分设计垃圾回收机制。不过JDK8之后,Hotspot虚拟机将运行时常量池从永久代移除了。然后引入了一个新的概念"元空间"。
用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。
对运行时常量池、常量、静态变量等数据做出了规定。
运行时常量池(具有动态性)、已被虚拟机记载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
JDK8 HotSpot JVM使用本地内存来存储类元数据信息并称之为:元空间(Metaspace)。这与Oracle JRockit 和 IBM JVM 很相似。意味着不会再有"ava.lang.OutOfMemoryError: PermGen问题",也不需要你进行调优及监控内存空间的使用。
持久代.png
其实移除永久代的工作从JDK 1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到Java Heap或者Native Heap。但永久代仍存在于JDK1.7中,并没有完全移除,譬如符号引用(Symbols)转移到native heap;字面量(interned strings)转移到了Java Heap;类的静态变量(class static)转移到了Java heap。
类加载器将字节码载入内存之后,执行引擎以Java字节码指令为目标,读取Java字节码,问题是,现在的Java字节码机器是读不懂的,因此还必须想办法将字节码转化为平台相关的机器码。这个过程可以由解释器来执行,也可以有即使编译器(JIT Compiler)来完成。
执行引擎.png
先用一张图演示今天的内容:
image.png
JVM只不过是运行在你操作系统的一个进程而已,这一些的魔法始于一个Java命令。正如任何一个操作系统进程那样,JVM也需要内存来完成它的于运行时操作。JVM也可以理解为以硬件的一层软件抽象,在这之上才能够运行Java程序,也才有了我们所吹嘘的平台独立性以及"write-once-run-anywhere "(一次编写,处处运行)。
老规矩 先抛出一个问题:为什么JVM在运行时数据区设计成如此模型?
每个人都有每个人的理解,我先说下我的理解
上面就是我对JVM运行时数据区的理解,希望能帮助到大家!
大家喜欢就点赞,您的每一次点赞,都是我努力和进步的动力!您可能想不到:您的小小一按,可能就会对另外一个人产生翻天覆地的影响。!最后谢谢您的支持与厚爱