JVM学习01——jvm运行时数据区域

JVM学习01

程序计数器(program counter register)

书是这么说的:

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

讲的复杂,其实意思就是:java代码总是一行一行执行的嘛,程序计数器就是记录下一行执行什么代码。

线程私有

多线程是通过上下文切换执行的,进行了线程切换后要保证复原到之前运行的状态,因此每条线程都需要各自独立的计数器

为什么没有OutOfMemory

程序计数器是字节码文件的行号,这个范围是可知晓的,从一开始就可以分配一个绝对不会溢出的内存

为什么执行Native方法,值为空?

Native方法大多是通过C实现并未编译成需要执行的字节码指令,也就不需要去存储字节码文件的行号了。

如果执行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(混合模型)。
以HotSpot VM的实现为例,它目前在大多数平台上都使用1:1模型,也就是每个Java线程都直接映射到一个OS线程上执行。此时,native方法就由原生平台直接执行,并不需要理会抽象的JVM层面上的“pc寄存器”概念——原生的CPU上真正的PC寄存器是怎样就是怎样。就像一个用C或C++写的多线程程序,它在线程切换的时候是怎样的,Java的native方法也就是怎样的。

摘自****博主「西域刀羊」的原创文章

原文链接:https://blog.****.net/jiangyang100/article/details/90614280

虚拟机栈(JAVA Virtual Machine Stacks)

书是这么说的:

与程序计数器一样,JAVA虚拟机栈(JAVA Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

简单来说就是:栈元素是栈帧。方法调用,栈帧入栈,反之出栈。

栈帧:一个方法的运行空间。

栈帧包括以下的内容:

局部变量表:方法定义的局部变量、方法的参数存在该表。实例方法中有个隐含参数“this”,所以实例方法可以访问该类的实例变量和其他实例方法。局部变量表中变量的存放顺序:this(如果是实例方法)=>参数(如果有)=> 定义的局部变量(如果有)slot是局部变量表中的空间单元,虚拟机规范规定:1、32位之内的数据,用一个slot来存放,如int、short、float等;2、64位的数据用连续两个slot来存放,如long、double;3、引用类型可32位亦可64位。

**操作数栈:**用来存放操作数。局部变量表中的变量是不可直接使用的,如需使用必须通过相关指令将其加载至操作数栈中作为操作数使用。

**方法返回地址:**一个方法开始执行后,只有两种方式可以退出这个方法。第一种:执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到的何种方法返回指令来决定,这种退出的方式称为正常完成出口。另一种:方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。

**动态链接:**每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

备注:

Note:

虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出“每个Slot占用32位长度的内存空间”是有一些差别的,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。

java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常:如果虚拟机栈可以动态扩展,如果扩增时无法申请到组构的内存,就会抛出OutOfMemoryError异常

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈作为虚拟机使用的Native服务。在虚拟机规范中对本地方法栈使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如HotSpot)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常

java堆

对于大多数应用来说,java堆(Java Heap)是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的反正与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有对象都在堆上也渐渐变得不是那么绝对了。

堆还可以继续细分为年轻代,老年代。年轻代可以再继续细分为伊甸区,两个生还区。创建对象放在伊甸区,进行一次垃圾回收到一个生还区,再一次垃圾回收到另一个生还区,再一次垃圾回收回到这个生还区。倒腾来倒腾去几次之后到老年区,老年区满了进行一次full gc。目前先谈这么多。

方法区

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。虽然JAVA虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non Heap,需要和堆区分开来。

简单来说就是:静态变量 + 常量 + 类信息(构造方法/接口定义) + 运行时常量池存在方法区中

方法区里的class文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。
在Class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值,参见下图。

JVM学习01——jvm运行时数据区域

因此class文件信息和class文件常量池的关系如下图:

JVM学习01——jvm运行时数据区域

class文件常量池中存储了哪些内部呢?
我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
每个class文件都有一个class常量池。

动态常量池

运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。

运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。

静态常量池和动态常量池的关系以及区别

静态常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量,符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引。
动态常量池是当class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到动态常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

jvm中的常量池

字符串常量池(String Constant Pool):

字符串常量池在Java内存区域的哪个位置?
在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;

在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。

字符串常量池是什么?
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。

在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;

在JDK7.0中,StringTable的长度可以通过参数指定:

-XX:StringTableSize=66666

字符串常量池里放的是什么?
在JDK6.0及之前版本中,String Pool里放的都是字符串常量;

在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。

需要说明的是:字符串常量池中的字符串只存在一份!

class常量池(Class Constant Pool):

我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
每个class文件都有一个class常量池。

运行时常量池(Runtime Constant Pool):

运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

常量池的好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

  1. 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  2. 节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。

直接内存

在JDK1.4中新加入了NIO(New lnput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制。但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。