深入理解Java虚拟机—Java内存区域

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙
墙外面的人想进来,墙里面的人却想出来

运行时数据区域

Java虚拟机会把它所管理的内存划分为若干个不同的数据区域
这些区域各司其职
有的区域随着虚拟机进程的启动而存在,如Java堆
有些区域则依赖用户线程的启动和结束而建立和销毁,如程序计数器、虚拟机栈和本地方法栈

1.程序计数器

程序计数器的作用是给出下一条字节码指令的地址
占用很少的内存空间

Java虚拟机的多线程都是通过线程轮流切换并分配处理器执行时间的方式来实现的
因此,为了线程切换后能够恢复到正确的执行位置
每条线程都需要有一个独立的程序计数器
各条线程之间的计数器互不影响,独立存储
我们称这类内存为“线程私有”的内存

如果线程正在执行一个Java方法,这个程序计数器记录的是正在执行的虚拟机字节码指令的地址
如果执行的是Native方法,这个计数值则为空(Undefined)

程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况

2.虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的
它的生命周期与线程相同

虚拟机栈描述的是Java方法执行的内存模型:
每个方法被执行的时候都会创建一个栈帧
栈帧:用于存储局部变量表、操作栈、动态链接、方法出口等信息
局部变量表:存放编译器可知的各种基本数据类型、对象引用和returnAddress

每一个方法被调用至执行完成的过程
就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

局部变量表所需的内存空间在编译器完成分配
当进入一个方法时,这个方法需要在栈中分配多大的局部变量是完全确定的
在方法运行期间不会改变局部变量表的大小

在Java虚拟机规范中,虚拟机栈这个区域规定了两种异常情况:
如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常
如果虚拟机可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常

3.本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别不过是:
虚拟机栈为虚拟机执行Java方法服务
本地方法栈则是为虚拟机使用到的Native方法服务

与虚拟机栈一样,本地方法栈会区域也会抛出StackOverflowError
和OutOfMemoryError异常

4.Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存

Java堆也是垃圾收集器管理的主要区域
因此很多时候也被称为“GC堆”

从内存回收的角度看,Java堆还可以细分为:
新生代和老年代;
再细致一点的有:
Eden空间、From Survivor空间、To Survivor空间

如果从内存分配的角度看:
线程共享的Java堆中可能划分出多个线程私有的分配缓冲区

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存中
只要逻辑是连续的即可

如果在堆中没有内存完成实例分配,将会抛出OutOfMemoryError异常

5.方法区

方法区与Java堆一样,是各个线程共享的内存区域
它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据
在虚拟机规范中,它是堆的一个逻辑部分,具体实现看具体的虚拟机

对HotSpot虚拟机来说,不同的JDK版本其方法区的实现就不同
JDK6和JDK7的时候,方法区的实现为永久代(PermGen space)
但在JDK8中,方法区的实现为元空间(Metaspace)
PS:对于其他虚拟机(如BEA JRockit,IBM J9等)来说是不存在永久代概念的
根据Java虚拟机规范,当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常

6.运行时常量池

运行时常量池是方法区的一部分

Class文件中有一项信息时常量池(Constant Pool Table)
它用于存放编译期生成的各种字面量和符号引用
这部分内容将在类加载后存放到方法区的运行时常量池中

运行时常量池和相对于Class文件常量池的另外一个重要特征是具备动态性
Java语言并不要求常量一定只能在编译期产生,运行期间也可能将新的常量放入池中
这种特性被开发人员利用得比较多的是String类的intern()方法

对象访问

在Java语言中,对象是如何进行访问的?
Object obj = new Object();
假设这句代码出现在方法体中,那么"Object obj"这部分的语义将会反映到
Java栈的本地变量表中,作为一个引用类型数据出现

而“new Object()”这部分的语义将会反映到Java堆中
将在堆中开辟一块存储了Object类型所有实例数据值的结构化内存
实例数据:对象中各个实例字段的数据

另外,在堆中还必须包含能查到此对象类型数据的地址信息
这些类型数据存储在方法区中
类型数据:如对象类型、父类、实现的接口,方法等

那么,如何通过栈的引用来定位或者访问到堆中对应的内存呢?
不同的虚拟机有不同的实现方式
主流的访问方式有两种:使用句柄和直接指针

1.使用句柄访问

Java堆中会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址
而句柄中则包含了对象实例数据和对象类型数据各自的具体信息

2.使用直接指针访问

reference直接存储的就是对象地址
但在Java堆中就需要考虑如何存放对象的类型数据
对于虚拟机Sun HotSpot而言,其采用的就是这种访问方式

两种方法的比较

使用句柄访问方式的最大好处是reference中存储的是稳定的句柄地址
在对象被移动时只会改变句柄中的实例数据指针
而reference本身不需要修改

使用直接指针访问的最大好处是速度更快,节省了一次指针定位的时间开销

以上内容均摘自于周志明—《深入理解Java虚拟机-JVM高级特性与最佳实践》

原文地址:https://www.cnblogs.com/ASE265/p/12761847.html