学习Java也有一段时间了,总感觉有些东西学的不是很精通。例如Java内存区域到底是怎么样的?程序是怎么跑的?对象是怎么存放的?这些都影响了我对自己的程序运行的熟悉程度。
一. 运行时数据区域Java虚拟机在执行java程序的过程中,会把它所管理的内存划分成若干个不同的数据区域(每当运行一个java程序都会启动一个虚拟机)。有一本书叫做《Java虚拟机规范》 【PDF 版下载见 】,讲述了Sun公司对Java虚拟机实现的相关规范,其中讲了虚拟机将所管理的内存分为以下几个部分:
程序计数器
虚拟机栈
本地方法区
堆
方法区
其中方法区和堆是由所有线程共享的,例如使用ThreadPoolExecutor创建多个线程时,堆与方法区都可以被多个线程读取。
程序计数器 学过计算机组成原理的人都会知道在CPU的寄存器中有一个PC寄存器,存放下一条指令地址,这里,虚拟机不使用CPU的程序计数器,自己在内存中设立一片区域来模拟CPU的程序计数器。只有一个程序计数器是不够的,当多个线程切换执行时,那就单个程序计数器就没办法了,虚拟机规范中指出,每一条线程都有一个独立的程序计数器。注意,Java虚拟机中的程序计数器指向正在执行的字节码地址,而不是下一条。
虚拟机栈 是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的时候都会创建一个栈帧(我觉得可以把它看作是一个快照,记录下进入方法前的一些参数,实际上是方法运行时的基础数据结构),用于存放局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直到执行完成的过程都对应着一个栈帧在虚拟机中的入栈到出栈的过程。我们平时把内存分为堆内存和栈内存,其中的栈内存就指的是虚拟机栈的局部变量表部分。局部变量表存放了编译期可以知道的基本数据类型,对象引用,和返回后所指向的字节码的地址。
本地方法区 与 虚拟机栈 所发挥的作用很类似,但是要注意一下,虚拟机规范中没有对本地方法区中的方法作强制规定,虚拟机可以自由实现,即可以不是字节码。但是也可以是字节码,这样虚拟机栈和本地方法区就可以合二为一,事实上,OpenJDK和SunJDK所自带的HotSpot虚拟机就直接将虚拟机栈和本地方法区合二为一。
堆 这个概念应该很多人都很熟悉,例如初学C语言的时候,老师就会讲malloc方法会在堆中分配空间,这里也一样。这个区域是用来存放对象实例的,几乎所有对象实例都会在这里分配内存,虚拟机规范中讲:所有对象的实例以及数组都要在堆上分配。但是随着JIT(Just-in-time) 编译期的发展,有些时候也有可能在栈上分配(这里我也不是很明白其中的道理)。堆是java垃圾收集器管理的主要区域(很多时候会称为GC堆,不叫垃圾堆),垃圾收集器实现了对象的自动销毁。
方法区 也是各个线程共享的区域,它用于存储已经被虚拟机加载过的类信息,常量,静态变量,及时编译期编译后的代码(类方法)等数据。这里要讲一下运行时常量池,它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用(其实就是八大基本类型的包装类型和String类型数据)。
最后还有一个直接内存,在JDK1.4版本中加入了NIO类,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,也就是说通过这种方式,不会在运行时数据区域分配内存,这样就避免了在运行时数据区域来回复制数据,直接调用外部内存。
二. 对象的创建对于面向对象的一门语言,我们无时不在通过new关键字创建对象,那么这个过程又是怎样的呢?
当虚拟机遇到一条new指令的时候,首先会去检查所new的类是否已经被加载,在哪里检查?当然在方法区,方法区存放了加载过的类信息。如果没有加载,那么先执行类的加载。
通过类加载检查后,虚拟机开始为新生对象分配内存,对象所需要的内存大小在类加载完成后已经可以确定,这时候只要在堆中分配空间即可。分配内存有两种方式,第一种,我们假设内存绝对规整,那么只要在用过的内存和没用过的内存间放置一个指针即可,每次分配空间的时候只要把指针向空闲空间移动相应距离即可。第二种,我们假设空闲内存和非空闲内存夹杂在一起,实际上就是这种情况,那么就需要一个列表,去记录堆内存的使用情况,操作系统对内存的管理就是这样的。