Java与C++之间有一堵由内存动态分配和自动垃圾回收技术围城的墙,墙里面的人想出来,墙外面的人想出去。
关于虚拟机的内存区域和内存异常,分两个部分,第一个部分是运行时数据区及其对应的异常类型,第二个部分是在内存区域中创建对象的原理。本篇主要涉及第一个部分,第二个部分参考JAVA运行时数据区对象创建原理
运行时数据区
java虚拟机运行时数据区包括五部分,平常大家最关心的也是最常说的就是堆和栈,严格意义上来讲并不是十分准确。java虚拟机运行时数据区包括如下五部分:
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
-
方法区
如下图:
image.png
下面就分别说一下这几个区域的相关信息。
按照声明周期或者与线程的关系,可以分为两类。线程私有和线程共享:线程私有的为程序计数器、虚拟机栈、本地方法栈;线程共享的为堆和方法区。
程序计数器
程序计数器是很小的一块内存空间,记录的是当前程序执行的字节码的行号显示器。虚拟机需要执行哪条字节码就是由程序计数器的内容确定的。不管是分支、循环、跳转、异常处理等等,都是通过修改程序计数器的值来达到控制虚拟机执行哪条字节码指令。当然,如果当前程序调用了本地方法,那么程序计数器的值有可能是空的。
该区域在java虚拟机规范中唯一一块没有规定任何内存异常信息的区域。
总结来讲,程序计数器可以概括为:
- 内存空间小
- 声明周期跟所属线程相同
- 内容是程序执行的字节码行号
- 内容可以为null
- 没有任何内存异常
虚拟机栈
虚拟机栈跟程序计数器一样,也是线程私有的,其生命周期跟所属线程相同。
虚拟机栈的内容描述的是java方法执行时的内存模型。java方法的每次调用和执行完成返回都会对应一个叫做栈帧的数据结构在虚拟机栈中的入栈和出站操作。栈帧存储的是java方法执行时的局部变量表,操作数、动态链接和方法出口灯信息。栈帧的具体接口这里不做展开,后边会专门讲。
局部变量表中存储的是编译期可知的基本数据类型,对象引用和returnAddress类型。基本数据类型就是java中大家所熟知的八种基本数据类型(boolean、byte、char、short、int、float、double、long),对象引用可能是一个对象实例起始地址的指针,可能是指向对象地址的一个句柄,也可能是代表该对象位置相关的地址(关于是指针还是句柄,参看JAVA运行时数据区对象创建原理 中的 对象定位 小节 )。基本数据类型中,long和double占用了两个局部变量空间,由于long和double都是占用了8个字节,所以可知一个局部变量空间是4个字节。
在java虚拟机规范中对该区域规定了两种异常:如果线程请求的栈深度超过虚拟机允许的深度,将抛出StackOverflowError异常,典型的场景就是递归调用。虚拟机栈动态扩展申请内存时,如果申请不到足够的内存,则会抛出OutOfMemoryError。
总结来讲,虚拟机栈可以概括为:
- 声明周期跟所属线程相同
- 内容描述的是java方法执行时的内存模型,包括局部变量表、操作数、动态链接、方法出口灯信息
- 局部变量表中存储的是编译器就确定的基本数据类型,大小在编译器就确定,运行过程中大小不会改变
- 会抛出两种运行时区域的异常:StackOverflowError和OutOfMemoryError
本地方法栈
本地方法栈和虚拟机栈类似,只不过是虚拟机栈是虚拟机执行java方法时所需要的,而本地方法栈是虚拟机使用到Native方法是所需要的。
堆
堆是java虚拟机管理的内存中空间最大的一块。该区域是被所有线程共享的一块内存区域。在虚拟机启动的时候创建,其大小可以通过-Xmx和-Xms来设置最大值和最小值。此区域的内容是存放对象实例,也包括数组。不过随着JIT编译器的发展和逃逸分析技术的不断成熟,栈上分配和标量替换也会导致所有对象实例都在堆上分配不是那么绝对了。
从内存垃圾回收的角度来看,堆也是垃圾回收器管理的主要区域。可以分为年轻代和年老代,更详细一些可分为Eden空间,From Survivor空间和To Survivor空间。从内存分配角度来看的话,虽然该区域是线程共享的,但是可能会划分出多个线程私有的分配缓冲区(TLAB)。但是不管从哪个角度,如何划分,都与存放的内容无关,无论哪个区域,存放的都是对象实例。
当申请新的内存是,如果没有足够的内存空间来申请,则会抛出OutOfMemoryError异常。
总结来讲,堆可以概括为:
- 堆是虚拟机管理的空间中最大的一块内存空间
- 堆的声明周期是不随线程变化的,跟虚拟机相同
- 存放的内容是java对象实例,但是随着JIT编译技术的发展和逃逸分析技术的成熟,所有的对象都在堆中分配不是特别绝对了,也有可能在栈上分配或者标量替换
- 这是垃圾回收的主要区域,线程共享的该区域可能会划分出多个线程私有的分配缓冲区(TLAB:Thread Local Allocation Buffer,这个区域的左右是为了在共享内存中多个线程都并发申请分配内存时解决内存冲突的一种手段)
- 会抛出OutOfMemoryError异常
方法区
方法区是线程共享的一块内存区域,虽然叫方法区,但是却与方法没有任何关系。方法区存储的内容是虚拟机加载的类信息、静态变量、常量以及及时编译器编译后的代码数据。
也有人称方法区为“永久代”,但是使用“永久代”代替方法区并不是很好的选择,容易遇到内存溢出的问题。从1.7开始,原本放在该区域的字符串常量池已经被移除,该有Native Memory来实现了。跟区域和java堆一样,可以不需要连续的内存(物理上不一定是连续的,逻辑上是连续的即可),可以选择固定大小和扩展。不同的是可以选择不进行垃圾回收,该区域垃圾回收主要操作是对常量池的回收和类型的卸载。
当方法区无法满足申请内存需求时,也会抛出OutOfMemoryError。
总结来说,方法区可以概括为:
- 线程共享的内存区域
- 生命周期跟虚拟机一样,与线程无关
- 可以不实现垃圾收集,如果收集,主要是针对常量池的回收和类型的卸载
- 方法区大小可以固定,也可以扩大或缩小,方法区内存不需要是连续物理空间
- 会抛出OutOfMemoryError异常
运行时常量池
运行时常量池是方法区的一部分。Class类文件中除了有类的版本、字段、方法、接口等元数据外;还有一项信息是常量池,存放的编译器生成的各种字面量和符合引用,也就是静态变量和常量。这些内容将在类加载后进入方法区的运行时常量池。
类的常量池和运行时常量池有两个不同:
一、类的常量池是被java虚拟机规范严格规定的,每个字节用于存储那种数据必须符合规范上的要求,虚拟机才可以正确的解析。但对于运行时常量池而言,虚拟机规范没有任何细节上的要求。
二、类的常量池是静态的,一旦编译后就确定了,而运行时常量池是动态的,典型的就是String的intern()方法会在运行期间产生新的字符串常量存入运行时常量池中。
直接内存
直接内存不是java虚拟机规范中定义的内存区域,也不是java虚拟机运行时的数据区域,而是机器的内存。这部分区域也会被频繁的使用,如果-Xmx等参数设置不合理,也可能造成OutOfMomeryError异常。
自从jdk1.4加入了NOI后,引入了通道(Channel)和缓冲去(Buffer)的方式,可以使用Natvie函数库直接分配对外内存,然后通过一块存储在java队中的DirectByteBuffer对象作为这块直接内存的引用直接进行操作,这样避免了java对和Native堆也就是直接内存来回复制数据