JVM系列文章(一):Java内存区域分析

作为一个程序员,仅仅知道怎么用是远远不够的。起码,你需要知道为什么可以这么用,即我们所谓底层的东西。

那到底什么是底层呢?我觉得这不能一概而论。以我现在的知识水平而言:对于Web开发者,TCP/IP、HTTP等等协议可能就是底层;对于C、C++程序员,内存、指针等等可能就是底层的东西。那对于Java开发者,你的Java代码运行所在的JVM可能就是你所需要去了解、理解的东西。

我会在接下来的一段时间,和读者您一起去学习JVM,所有内容均参考自《深入理解Java虚拟机:JVM高级特性与最佳实践》(第二版),感谢作者。

本文是系列文章第一篇,讲述的是Java内存区域,即在虚拟机上,数据是怎么存储的。

一、运行时数据区域

运行时数据区分为两个部分,一部分由所有线程共享,一部分是各个线程私有的。

线程共享的数据区包括方法区和堆,线程私有的数据区包括虚拟机栈、本地方法栈和程序计数器。如下图所示:

(图片来自网上图片库)

下面我们分别对这些区域进行介绍:

1.程序计数器

一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

如果线程正在执行的是一个JAVA方法,计数器值为当前执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,计数器值为空。

这个内存区域是唯一一块绝对不会出现OutOfMemoryError的区域。

2.虚拟机栈

线程私有,它的生命周期与线程相同。描述的是Java方法执行的内存模型:

每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表:

局部变量表存放了编译时可知的基本数据类型(8种,boolean等)、对象引用(指向对象起始地址的引用指针,或者是指向一个代表对象的句柄,或者是其他与此对象相关的位置)、returnAddress类型(指向字节码指令的地址)。局部变量表所需要的内存空间在编译期完成分配,在进入方法时,需要在帧中为这个方法分配多大的局部变量空间是完全确定的,运行时不改变。

局部变量表可能有两种异常状况: 如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverFlowError异常;如果虚拟机栈可以动态扩展(大多数虚拟机都可以),如果扩展时无法申请到足够的内存,就会抛出OutOfMemory异常。

3. 本地方法栈

作用与虚拟机栈类似,它们之间的区别是: 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。也会抛出StackOverFlowError和OutOfMemoryError异常。

4.Java堆

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆被所有线程共享,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。

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

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,,既可以是固定大小的,也可以是可拓展的。如果在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemoryError异常。

5.方法区

所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码等数据。虽然这个区域有“永久代”之称,然而这个区域仍然存在内存回收,主要是针对常量池的回收和对类型的卸载。方法区也会抛出OutOfMemoryError异常。

运行时常量池:

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

6.直接内存

直接内存并不是虚拟机运行时数据区的一部分,但是这部分内存也被频繁使用,也可能导致OutOfMemoryError异常,所以放到这里一起讲。

JDK1.4中加入了NIO(New Input/Out )类,引入了一种基于通道和缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中提高性能,避免了在Java堆和Native堆中来回复制数据。

受本机总内存和处理器寻址空间(比如处理器是32位的,那你能够通过地址访问到的内容就是2^32,即4G,所以你能搭配的最大内存就是4G)的限制,也会抛出OutOfMemoryError异常。

二、对象的创建、布局与访问

知道了内存中都存放了什么之后,我们自然想进一步了解虚拟机内存中的其他细节。比如是怎么创建、布局以及如何访问的。

我们以最流行的HotSpot虚拟机以及常用的内存区域Java堆为例,探讨一下对象分配、布局与访问的全过程。

1.对象的创建

我们创建对象,当然是用new指令。

虚拟机遇到new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个引用代表的类是否已经被加载、解析和初始化过。即第一步,先去检查虚拟机加载了你要new的这个类没,如果没加载,必须先执行相应的类加载过程。(在以后的文章中会详细介绍)

然后是为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。

分配内存有两种方式:

指针碰撞:如果Java堆中内存绝对规整,在使用的内存放在一边,空闲内存放在另一边,中间一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相同的距离。

空闲列表:如果并不是规整的,虚拟机就需要维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

除了如何划分可用空间之外,还需要考虑修改指针时的线程安全问题。可能出现正在给对象A分配内存,指针还未修改,对象B又同时使用原来的指针分配内存的情况。

解决这个问题有两种方案:

从起点,到尽头,也许快乐,或有时孤独,

JVM系列文章(一):Java内存区域分析

相关文章:

你感兴趣的文章:

标签云: