java 内存结构
最近在看深入理解java虚拟机这本书,所以想写写自己看这本书的总结。
首先引用这本书里面的一句话:Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙内的想出来。感觉很诗意,有点像《围城》里面的一句话:城里的人想出来,城外的人想进去。
废话不多说了,我本篇博客的主要内容如下:
- java内存结构(运行时数据区域的划分)
- 对象的创建,内存布局和访问定位
java内存结构
根据Java虚拟机规范,Java虚拟机管理的内存会包括以下几个运行时数据区域,如下图:
程序计数器
程序计数器(Proram Counter Register)是一块比较小的内存空间,他可以看做当前线程所执行的字节码的行号指示器。和操作系统中的程序计数器很相似。
先看回想一下CPU的总线结构,CPU有三个总线,数据总线,控制总线,地址总线,RAM和CPU交互的时候其实就是逐条的通过一些命令字透过控制总线发送命令,并且将数据通过数据总线进行来回交互,Java在运行时期,其实也是内存在和cpu来回往返的发送各种命令字,并且交换数据。
java代码会通过java编译器最终转换成一些底层的命令字(class文件->本地方法将class文件解析转换成标准的命令字)既然是一堆命令字相关的东西,也就存在先运行什么?调用哪个方法,获取那个数据,进行如何的操作等等,在程序计数器中存放的就是这些东西。
我们知道cpu执行的时间和分配是有cpu根据某种cpu的算法规则确定的,因此在某一时刻,cpu只能执行一条命令,在执行完这条命令之后,需要能够确保回到下一个执行命令位置的正确地点。而java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,因此java将程序计数器设计成私有的/独享的,不同的线程对应不同的程序计算器。
另外由于每个程序计数器只需要记录当前正在执行的的虚拟机字节码指令位置,所需要的空间非常小,且当执行的是native方法时,不会为程序计数器分配内存空间,所以不会出现OutOfMemoryError的情况,并且也是唯一的一个没有规定任何OutOfMemoryError情况的区域。
虚拟机栈
java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期和线程一样。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。(这几块存储的内容我会在后面说明)
在java虚拟机规范中,对这个区域规定了俩种异常情况:
- 线程的请求的栈的深度大于堆虚拟机所允许的深度,将抛出Stack OverflowError异常。
- 如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可动态扩展,Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展无法申请到足够的内存,就会抛出OutOfMemoryEror异常。
本地方法栈
本地方法栈(Native Method Stack)。与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机执行Native方法服务。Java虚拟机没有规定如何去实现这部分,由于和虚拟机栈非常类似,甚至有的虚拟机,类如sun提供的JDK中本地方法区和虚拟机栈合二为一。
在java虚拟机规范中,对这个区域规定了俩种异常情况:
- 线程的请求的栈的深度大于堆虚拟机所允许的深度,将抛出Stack OverflowError异常。
- 如果本地方法栈扩展无法申请到足够的内存,就会抛出OutOfMemoryEror异常
Java堆
Java堆是虚拟机所管理的内存中最大的一块,java堆是被所有线程所共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都会在这里分配。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作GC堆(Garbage Collected Heap)。
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上市连续的即可,这部分的内存是可以扩展的,在启动虚拟机时我么可以通过-Xms ,-Xmx来控制。如果在Java堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。
其实这个时候就可以联想一下我们以前学习的时候,经常说的堆和栈的概念,其实堆就是指Java堆,也就是这一部分描述的,栈指的就是虚拟机栈,也就是上面描述的。是不是感觉恍然大悟。
方法区
方法区(Method Area)与Java堆一样,也是各个线程共享的内存区域,他用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等数据。此部分有另外一个别名叫做Non-Heap(非堆),目的是为了和Java堆区分开来。
当方法区无法满足内存分配需求是,将会抛出OutOfMemoryError异常。
运行常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中处理有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面常量和符号引用,这部分内容在类加载后进入方法去的运行时常量池中存放
java在编译的时候会将我们定义的final类型做自动的优化存放在常量池中,这样可以提高访问和寻址的速度,因为常量不会再运行期间变化,也就是说他的数据单元地址不会发生改变,一次寻址即可,他其实是方法区的一部分,在android中执行完编译之后除了有class文件还会有一个idx文件,该文件其中的一些数据就是将java文件中常量字面量,这也是为什么一些有经验的人在编写代码的时候非常喜欢用final进行类型的修饰,在方法的参数中,方法体中,类变量中,只要是认为不可变的都进行final声明,试图告诉java虚拟机,这样的变量存放在常量池中,提高寻址速度。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfmemoryError异常出现,因此拿到这里来说。
在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配对外内存,然后通过一个在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据
虽然本机直接内存的分配不会受到Java堆大小的限制,但是既然是内存,肯定还是会受到本机总内存的限制,因此在分配内存不恰当的时候,这部分也可能导致OUtOfMemoryError异常出现。