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