本系列是周志明老师《深入理解Java虚拟机》第三版的学习笔记。
Java应用运行时内存交给Java虚拟机自动管理,但出现内存泄漏和溢出问题时,如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。
下面从概念上介绍Java虚拟机内存的各个区域,讲解这些区域的作用、服务对象以及可能产生的问题,这也是学习内存管理的第一步。
Java虚拟机内存区域图示:
一、 程序计数器
多线程是通过线程轮流切换,分配处理器执行时间的方式来实现。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
-
属于线程私有的内存区域。
-
Java虚拟机唯一不会产生OutOfMemoryError异常的区域。
二、Java虚拟机栈
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
-
属于线程私有的内存区域。
-
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
-
如果申请内存时失败,抛出OutOfMemoryError异常。
-
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。(HotSpot的虚拟机栈不能动态扩展)。
三、本地方法栈
与虚拟机栈的作用类似,区别:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。本地方法栈则为虚拟机使用到的本地(native)方法服务。
-
属于线程私有的内存区域。
-
《Java虚拟机规范》未对本地方法栈中使用的语言、使用方式与数据结构未做任何强制规定。HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一。
-
本地方法栈抛出的异常与虚拟机栈一致。
四、Java堆
Java堆(Java Heap)是在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例。
-
所有线程共享的一块内存区域。
-
基于分代设计的垃圾收集器,会将堆内存分为新生代、老年代、永生代等等。
-
Java堆可扩展,通过参数-Xmx和-Xms设定。
-
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出OutOfMemoryError异常。
五、方法区
方法区(Method Area)用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
-
与Java堆一样,是各个线程共享的内存区域。
-
JDK8中,方法区放弃永久代的概念实现,用本地内存中实现的元空间(Metaspace)来代替。
-
方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
六、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
-
动态性,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,比如String类的intern()方法。
-
受方法区内存的限制,无法申请到内存时抛出OutOfMemoryError异常。
七、直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中 定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。
-
JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
-
本机直接内存的分配不会受到Java堆大小的限制,但受到本机总内存限制,设置-Xmx等参数信息时,容易忽略掉直接内存,使得出现OutOfMemoryError异常。