深入理解java虚拟机——运行时数据区域

 2019-12-22 10:38  阅读(828)
文章分类:JVM

声明:此系列博客为学习《深入理解java虚拟机 JVM高级特性与最佳实战》笔记

更加详细的情况请参考书本内容

1.概述

在C、C++程序开发中,对于内存的管理,开发者拥有操作每一个对象的权利,可以控制内存的使用和对象声明周期的控制,这给了开发者很大的自主权,但同时意味着开发人员需要花费很大的精力去应对内存溢出等问题。

java将内存管理工作交给虚拟机的内存管理机制来进行分配,开发人员在new一个对象的时候,不需要在手动的定义delete/free代码,内存泄露的问题也交与jvm去完成,正是因为内存控制的权利都被移交给了jvm,导致我们在处理内存溢出等问题的时候变得不那么容易了,这时候,为了排查问题,优化代码,我们需要对java的内存管理进行更深一步的探索。

2.运行时的数据区域

java虚拟机在执行java程序的过程中会将他管理的内存区域分为如下的几个数据区域,这些区域都有自己的声明周期,对运行时涉及到的数据进行管理,具体的分区情况如下:

2019120001170\_1.png

– 程序计数器(线程私有)

程序计数器主要是用来记录程序所执行的字节码的行号位置(指示器),拥有一个很小的内存空间,在虚拟机的概念模型中,字节码解析器工作时本质上就是改变程序计数器的值来确定下一条需要执行的字节码指令,程序中的分支,循环,异常,线程恢复等基础功能都是在此计数器的基础上完成的。

在单核电脑中(多核处理器会有不同),一个处理器在同一时刻只能执行一个线程中的指令,面对多个线程的程序,处理器是通过时间分片技术来完成多线程任务的处理,给我们的感觉上是“同时进行”的,其实是给每个线程分配时间片,哪个线程获取了时间片,就会被处理器执行。此时,为了确保切换线程后处理器能回到正确的执行位置(上一次执行的位置),每个线程都会有一个属于自己的程序计数器,与其他线程间的计数器互不干涉,独立存储,这就是我们所说的线程私有

如果线程执行的是native(本地方法)方法,这个计数器的值则为空(undefined),此内存区域是唯一一个在java虚拟机中没有规定任何OutOfMemoryEooro情况的区域。

如果线程执行的是java方法,此技术器记录的是正在执行的虚拟机字节码指令的地址

– 虚拟机栈(线程私有)

虚拟机栈的生命周期与线程相同,其描述的是Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,主要是用来存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

时下比价流行的Java内存区被分为堆内存(heap)和栈内存(Stack),这种划分方式其实比较粗糙,真实的Java内存区域的划分要更加复杂,这里的栈指的就是我们现在所讲的虚拟机栈,更准确的说,指的是虚拟机中局部变量表部分。

局部变量表中存放的内容主要有

– 编译期可知的各种基本类型数据(boolean、byte、char、short、int、long、float、double)

– 编译期对象引用(可能是一个指向对象起始地址的一个引用指针,也可能是一个指向一个代表对象的句柄或其他与此对象相关的位置)

– 编译期returnAddress类型(指向了一条字节码指令的地址)

其中有64位长度的double和long两种基本类型数据会占有两个局部变量空间,其余的数据类型占据一个。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法的时候,这个方法所需要的在帧中的内存空间是定的,在方法运行期间不会该表局部变量表的大小

2019120001170\_2.png

java虚拟机中的两种异常:

正常情况下,如果线程请求的栈帧深度大于虚拟机允许的深度,那么就会报StackOverflowError异常

如果虚拟机栈可以动态扩展,若果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常(大部分java虚拟机都可以动态扩展)

– 本地方法栈(线程私有)

本地方法栈与虚拟机栈类似,它们之间存在的差异主要是:

– 虚拟机栈为虚拟机执行Java方法(也就是字节码而服务)

– 本地方法栈为虚拟机使用到的Native方法服务

虚拟机规范中对本地方法中方法使用的语言、使用方式与数据结构并没有强制性规定(意思是其也可以执行c语言等其他语言的函数),因此具体的虚拟机可以自由的实现它,甚至有的虚拟机(Sun HotSpot)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StrackOverflowError和OutOfMemoryError异常。

– 方法区(线程公有)

方法区是各个线程共享的区域,主要存储的是已经被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范将方法区描述成为一个堆的逻辑部分,但是它却有一个别名为Non-Heap(非堆),其目的就是为了与java的堆区分出来

Java虚拟机规范对方法区的限制比较宽松,与堆一样不需要连续的内存和可以固定大小,可扩展,除此之外还可以选择不实现垃圾回收机制,相对而言,垃圾回收在这块区域出现的概率不高,主要是用来回收常量池和对类型的卸载

&bsp; 当方法区无法满足内存分配需求时,会抛出OutOfMemoryError异常

– 堆(线程公有)

大多数应用中,Java堆是java虚拟中所管理的内存中最大的一块,是被所有线程共享的一块内存区域,虚拟机启动的时候创建,目的是为了存放java的实例对象,但是这并不是绝对的,随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会产生一些微妙的变化。

堆内存区域也是垃圾回收器主要工作的地方,从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以堆内存还可以分为新生代和老年代。在细致的划分为Eden空间、From Survivor空间、To Survivor空间等,线程共享的Java堆中可能划分出多个线程私有的缓存区,但是不论如何划分,其本质上存储的还是对象,只是为了管理才分出这诸多类别

根据Java虚拟机的规范规定,Java堆可以处于物理上的不连续的内存空间中,只要逻辑上连续即可。当前主流的虚拟机嗾使可扩展的,如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

– 运行时常量池

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

java虚拟机对Class文件每一部分(包含常量池)的格式都有严格的规定,每一个字节用于存储何种数据都需要符合规范要求才会被运行,但是对于常量池,java虚拟机没有做任何细节的要求,一般来说,除了保存Class文件中描述的符号引用外,还会将翻译的直接引用也存储在运行时常量池中

运行时常量池相对于Class文件来讲,还有另外的一个特性——动态性,并非只有经历编译器产生的常量才可以被放入到运行时常量池中,程序在运行期间产生的常量也可以动态的添加到常量池中,使用比较多的是String类的intern()方法

与上面类似,当常量池无法再申请到内存时也会报警OutOfMemoryError异常

2019120001170\_3.png

– 直接内存

直接内存不属于虚拟机运行数据区的一部分,也不属于java虚拟机规范中定义的内存,而是本机内存中的堆外内存、

JDK 1.4引入了NIO的概念,一种基于通道与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DircctByteBuffer对象作为这块内存的引用进行操作,这样就避免了在Java堆和Native堆中来回复制数据,从而达到提高效率的目的

虽然直接内存不熟java堆内存的制约,但是确是在总机内存内的,服务器管理者在配置虚拟机的参数时,往往会根据需求设置-Xmx等参数信息,可是经常会忽略直接内存,从而造成内存溢出

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解java虚拟机——运行时数据区域

相关推荐