Fork me on GitHub

走进JVM【二】理解JVM内存区域

引言

对于C++程序员,内存分配与回收的处理一直是令人头疼的问题。Java由于自身的自动内存管理机制,使得管理内存变得非常轻松,不容易出现内存泄漏,溢出的问题。

不容易不代表不会出现问题,一旦内存泄漏或溢出的情况发生,调试起来会变得非常困难。这就要求我们对虚拟机的内存区域有深入的理解。最终能够判断内存方面的异常发生时,具体在JVM中的位置。

内存区域

image

JVM运行时,首先需要类加载器(ClassLoader) 加载所需类的字节码,加载完毕交由执行引擎执行,执行过程中需要一段空间来存储数据(类比CPU与主存)。这段内存空间的分配和释放过程正是我们所关心的,称为运行时数据区

对于CS相关从业者,深入理解操作系统的内存的层次结构,分配与垃圾收集过程都是大有裨益的。同理,欲定位内存问题的出现区域,必须剖析运行时数据区

运行时数据区

如上图所示,运行时数据区包括:程序计数器(即PC寄存器),Java 虚拟机栈(VM Stack),Java 堆(Heap),方法区(Method Area),本地方法栈(Native Method Stack)。下面带领大家深入理解各个数据区域

JVM实际上就是一台虚拟的计算机,目的是为了实现"一次编译,处处执行"。所以,在理解运行时数据区时,完全可以与操作系统系统 内存,寄存器类比学习。

程序计数器

每条虚拟机中的线程都有自己的寄存器,称之为程序计数器(PC)。为了保证线程之间的独立性,因而PC内的空间是线程私有的。

  • 线程私有只能本线程访问的区域,其他线程无权访问。

程序计数器的作用

虚拟机中的多线程通过线程轮转调度,为每条线程分配时间片来实现并发执行。同一时刻,处理机只能执行一条线程。当切换到另外一条线程时,若不保存当前未执行完线程的执行位置,下次处理机再执行这条线程时,又要重新开始执行。这种情况显然是不能容忍的。

引入程序计数器的目的,就是为了记录线程的执行情况,便于下次切换后进行线程恢复

程序计数器的机制

如何记录线程的执行情况? 其实也并不复杂,只需要记录正在执行的虚拟机字节码指令的地址。如果运行的是Native(本地)方法,计数器的值为Undefined

程序计数器是唯一没有OutOfMemoryError异常的区域。

Java 虚拟机栈

每个Java方法执行时,需要分配内存空间来存储局部变量表操作数栈动态链接方法出口等信息。将这部分内存称之为栈帧(Stack Frame)。虚拟机栈用于存储栈帧,是Java方法执行的内存模型

显然我们需要为每个执行的方法分配栈空间,因此Java虚拟机栈也是线程私有的。

虚拟机栈的作用

虚拟机栈记录Java方法执行的过程。每个方法开始执行时,为之创建一个栈帧记录信息;方法执行到完成的过程,对应栈帧在虚拟机栈中入栈到出栈的过程

image

局部变量表

局部变量表是栈帧中的重要部分。存放编译期定义的基本数据类型, 对象引用(相当于对象地址),及returnAddress类型(字节码指令地址)。

局部变量表空间在编译期间分配,执行方法的过程中不会改变其大小。

异常

  1. 当线程请求的栈深度大于所允许的深度,抛出StackOverflowError异常。
  2. 长度不够时,虚拟机栈可进行动态扩展,申请内存。若无法申请到足够的内存,抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机栈类似,区别是虚拟机栈记录执行的Java方法,本地方法栈则记录Native方法。

本地方法栈同样会抛出StackOverflowErrorOutOfMemoryError异常。

Java 堆

Java堆用于存储对象实例,为所有对象分配内存空间

所有对象实例都要在堆上分配空间,因此Java堆是所有线程共享区域。对象的生命周期结束后,Java堆还要负责内存回收,因此Java堆也常被称之为GC堆(Garbage Collected Heap)。

内存模型

从内存回收的角度,Java堆可以分为新生代(Young Generation)与老生代(Old Generation)。这种划分的方式,是为了更好的回收内存(老生代内存会被优先回收)。

image

如图,新生代还可以分为Eden空间From Survivor空间To Survivor空间

永久代(Permanent Generation)用于存储静态类型数据,与垃圾收集器关系不大。

注意:本图展示的是JVM堆的内存模型,JVM堆内存包括Java堆区域永久代区域。因此,永久代不属于Java堆

异常

Java堆同样可扩展(-Xmx与-Xms参数)。若堆中内存已无法为对象实例分配且无法再扩展,抛出OutOfMemoryError异常。

方法区

方法区存储类信息常量静态变量等数据,是线程共享的区域。为与Java堆区分,方法区还有一个别名Non-Heap(非堆)。

方法区≠永久代

方法区就是永久代?并非如此。

HotSpot虚拟机选择用永久代来实现方法区,从而省去了为方法区编写内存管理代码的工作。这只是一种实现方式,其他虚拟机(BEA JRockit,IBM J9)都不存在永久代这一概念。

通过永久代来实现方法区容易造成内存溢出,未来也可能会被替代。

在虚拟机规范中,方法区的实现没有明确的规定,因此不能将方法区等同于永久代。

异常

当方法区无法满足内存分配的需要时,抛出OutOfMemoryError异常。

运行时常量池

运行时常量池(Runtime Constant Pool)用于存放编译期生成的各种字面量和符号引用。

运行时常量池具备动态性,使得运行期间也可将新的常量放入池中。例如String类的intern() 方法。

package intern;

public class Main1 {
	public static void main(String[] args) {
		String s0= "I'm coding";   
		String s1=new String("I'm coding");   
		String s2=new String("I'm coding");   
		System.out.println( s0==s1 );  
		System.out.println( s0==s1.intern());   
		s2=s2.intern();  
		System.out.println( s0==s2 );   
		  
	}
}

输出结果

false
true
true

本例中,s0直接保存在常量池,s1与s2的对象实例存储在Java堆中。==直接比较对象的hashCode,因此第一行输出false。s1.intern()方法返回s1在常量池中的引用,没有则创建。
s1存放的字符串已经在常量池中存在,直接返回s0的引用,第二行输出true
同理,s2接收了s2.intern()的返回值,字符串值与s0相同,第三行输出true

运行时常量池是方法区的一部分,因此受方法区内存的限制。当无法申请到内存时,抛出OutOfMemoryError异常。

总结

对于JVM的内存管理, 最重要的还是与OS内存管理知识进行类比以及结合实践来学习。理解JVM内存区域的目的也是为了在工程中出现内存相关异常时能够准确的定位所在区域,及时处理。

后续我们将在本文的基础上来理解对象的创建过程以及OutOfMemoryError异常


作者: I'm coding

链接ACFLOOD

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如果您觉得本文对您有所帮助,就给俺点个赞吧!

posted @ 2016-06-22 19:30  I'm coding  阅读(2471)  评论(0编辑  收藏  举报