JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,本质上就是一个程序。Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。但是它没有寄存器,所以指令集是使用Java栈来存储中间数据的。 – 百度百科*JVM
线程(英语:thread)是独立调度和分派的基本单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。–百度百科*线程
此类区域每个线程都有独属于自己的,线程之间相互隔离,不相互影响。此类区域生命周期与线程一致,随着线程的产生而产生,随着线程的销毁而回收。此类区域包含:程序计数器(PC),虚拟机栈(VM Stack),本地方法栈(Native Method Stack)。
此类区域属于JVM层次的,JVM中的所有线程共享这类区域。此类区域生命周期与JVM一致,随着JVM的启动而产生,随着JVM的关闭而销毁。此类区域包含:JAVA堆,方法区
此类区域不属于JVM,它的生命周期与JVM无关。也不受JVM垃圾回收(GC)所管理。可以理解为JVM所在的计算机的直接内存。JAVA有一套方法来操作这部分内存。例如一个txt文件,你可以通过JAVA来操作这个文件。但是它实际是直接存储在计算机内存中,它所在的内存区域不属于JVM,这块区域也不会因为JVM的关闭而被销毁。
一块较小的内存区域。如果是执行的JAVA方法,存放的是当前线程正在执行的字节码指令地址。线程因为各种原因挂起,然后在恢复时,程序计数器中存放的地址可以让CPU需要知道这个线程在挂起之前已经执行到哪一步了,以便继续执行。
如果执行的是一个Native方法,则存放的内容为空。但是存放为空,如果线程被挂起恢复时又如何确定执行到哪一步呢?
这里的“pc寄存器”是在抽象的JVM层面上的概念——当执行Java方法时,这个抽象的“pc寄存器”存的是Java字节码的地址。实现上可能有两种形式,一种是相对该方法字节码开始处的偏移量,叫做bytecode index,简称bci;另一种是该Java字节码指令在内存里的地址,叫做bytecode pointer,简称bcp。
对native方法而言,它的方法体并不是由Java字节码构成的,自然无法应用上述的“Java字节码地址”的概念。所以JVM规范规定,如果当前执行的方法是native的,那么pc寄存器的值未定义——是什么值都可以。
上面是JVM规范所定义的抽象概念,那么实际实现呢?
Java线程总是需要以某种形式映射到OS线程上。映射模型可以是1:1(原生线程模型)、n:1(绿色线程 /用户态线程模型)、m:n(混合模型)。 以HotSpotVM的实现为例,它目前在大多数平台上都使用1:1模型,也就是每个Java线程都直接映射到一个OS线程上执行。此时,native方法就由原生平台直接执行,并不需要理会抽象的JVM层面上的“pc寄存器”概念——原生的CPU上真正的PC寄存器是怎样就是怎样。就像一个用C或C++写的多线程程序,它在线程切换的时候是怎样的,Java的native方法也就是怎样的。
作者:RednaxelaFX
链接:https://www.zhihu.com/question/40598119/answer/87381512
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
另外,程序计数器存的就是一个地址,其内存大小是可预见的。因此这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。
虚拟机栈的栈元素为栈帧(Stack Frame)。方法的调用和结束就对应着栈帧的入栈和出栈。栈帧可以理解为一个方法的运行空间。栈帧的内部分为局部变量表,操作数栈,动态链接,方法返回地址,帧数据区。
do {
int i = 1;
} while (false);
int j = 0;// 执行到这一步时,已经超出了i的作业域,此时j就会复用i的空间
int a = 1;int b = 2; int c = a + b;
对于此过程a和b会在局部变量表i和i+1处,然后将a取出压入操作数栈,将b取出压入操作数栈,然后将栈顶的两个元素出栈,执行加法运算,将结果入栈并放入局部变量表i+2处。对于虚拟机栈线程请求的栈深度大于JVM所允许的深度时会报错StackOverflowError 。JVM允许动态扩展,但是无法申请到足够内存时报错OutOfMemoryError。
本地方栈的作用和虚拟机栈的作用类似。区别仅仅是虚拟机栈执行的是Java方法,本地方法栈执行的则是Native方法。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。例如某个虚拟机是使用C连接模型实现的本地方法接口,那个他的本地方法栈就是C栈。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
当线程调Java方法时,JVM会创建一个栈帧并压入虚拟机栈。当线程调用一个本地方法时,JVM并不会向虚拟机栈中压入新栈,JVM只是简单的动态链接并直接调用指定的Native方法。如果本地方法要回调JVM中的JAVA方法,那么虚拟机会保存本地方法栈的状态,并进入Java虚拟机栈。
Java 堆(Java Heap)在虚拟机启动时被创建。是所有线程共享的内存区域。实例化的对象以及数组就存储在这个区域(jdk1.7之后字符串常量以及类的静态变量也移到了这个区域),因此这个区域是垃圾回收(GC)的主要场所。从GC的角度,如果是采用分代GC(当前大多数JVM都采用的这种GC算法),堆内存又可以分为新生代(Young)和老年代(old),新生代又可以分为Eden区,From Survovir,To Survovir.
栈中的实例引用如果引用的是堆中的实例的话,实质上话就是记录了该实例在堆中的地址。栈中的引用在超出其作用域时空间被回收,而堆数据即使超出其作用域依然不会被销毁,空间的回收只能等待GC来做。
JVM规范规定堆空间可以是物理上不连续的空间,只要逻辑上连续即可。其空间大小可以设置为固定大小,也可以设置为可扩展的。( -Xms设置初始堆大小, -Xmx设置最大堆大小)。
如果JVM允许动态扩展,但是堆无法申请到足够内存时报错OutOfMemoryError。
方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。
方法区,永久代,元数据区实质上表示的一个意思,方法区是JVM规范的概念,而永久代是HotSpot JVM在JVM1.8之前对JVM方法区规范的实现。元数据区则是HotSpot JVM在1.8及之后对JVM方法区规范的实现。可以理解为方法区是一个接口,任何类型的虚拟机都要实现这个接口,它是一个概念上的规范。而永久代和元数据区则是 HotSpot JVM对于方法区这个规范概念在1.8前后的具体实现。
对于HotSpot JVM来说,要理解方法区,首先要看看其JDK版本,不同的版本有不同的是实现。
不同版本的HotSpot JVM除了方法区的实现上的不同,方法区所承载的功能也随着JDK的版本产生了变迁:
Klass元数据信息:类的基本信息,如类型的全限定名,超类的全限定名,直接超接口的全限定名,类型标志(该类是类类型还是接口类型),类的访问描述符(public、private、default、abstract、final、static)等等
每个类的运行常量池:类中的字段,方法,类,接口等的符号引用。要注意这里放的是符号引用,而不是直接引用,即使是基础数据类型也是符号引用,这里是元数据区,是不会有一般对象的实例的。而符号引用也只是一个说明这个变量是什么的标记(符号具体见2.2.3动态链接的注释部分)。
oop(Ordinary Object Pointer(普通对象指针)):就是Class实例.通过this.getClass获得的Class对象就是它.
全局字符串常量池:对于字符串,JVM会将以常量形式的字符串放在字符串常量池中,这样下次使用的时间就可以复用这个字符串的内存空间,而不用每使用一次就分配一次内存空间去浪费内存。很多人在初学java的时间都会碰到这种问题:new String(“A”)创建了几个对象?str1==str2的结果是true还是false。如何解释这种问题,关键就在字符串常量池。
要想了解这个问题,首先要了解String的intern()方法。因为JVM方法区的变迁,intern()方法在不同的版本也有些许不同。
所以对于以下代码:
String str = new String("prefix") + new String("suffix");
System.out.println(str==str.intern());
JDK1.6中会输出false,JDK1.6与JDK1.8中则会输出true.
为什么会这样呢,首先要知道new String("prefix")
创建了几个对象。
可以看到无论是1.6之前还是之后,都是创建了两个对象,1个在堆,1个在字符串常量池。这时候执行String.intern()方法,String.intern()会去检查字符串常量池,发现字符串常量池存在prefix字符串,所以会直接返回,不管是jdk1.6还是jdk1.7和jdk1.8都是检查到字符串存在就会直接返回,所以str1==str1.intern()得到的结果就都是false,因为一个在堆,一个在字符串常量池。
而执行new String("prefix") + new String("suffix");
则会创建5个对象,3个在堆中,2个在字符串常量池。
至此可以看到,除了字符串常量池所在的位置不同之外,堆栈的内容并没有区别。但是JDK1.6与JDK1.7的改动体现在str.intern()方法上面,使得System.out.println(str==str.intern());
出现了截然不同的结果,JDK1.6中str.intern()在字符串常量池中发现并没有prefixsuffix字符串,那么就会在字符串常量池中创建一个字符串。而JDK1.7则是再去堆中查看,发现有prefixsuffix字符串对象,那么就会创建一个指向堆中字符串对象的引用放在常量池中。
这就会导致JDK1.6会返回false,而JDK1.7则会返回true。
参考资料:
JVM 内存区域 (运行时数据区域)
深入分析Java虚拟机堆和栈及OutOfMemory异常产生原因
细说虚拟机栈
详细解析Java虚拟机的栈帧结构
探究 Java 虚拟机栈
Java-JVM 栈帧(Stack Frame)
类中成员方法和实例方法
Java 八大基本数据类型
栈帧中动态连接的理解
符号引用与直接引用
JVM学习笔记-本地方法栈(Native Method Stacks)
浅谈Java中的栈和堆