天天看点

深入理解JVM虚拟机笔记——第2章 JAVA内存区域与内存溢出异常一、JVM运行时数据区域二、对象访问

一、JVM运行时数据区域

JVM在执行JAVA程序的时候会将它管理的内存划分为几个数据区域,如下图所示:

深入理解JVM虚拟机笔记——第2章 JAVA内存区域与内存溢出异常一、JVM运行时数据区域二、对象访问

1、程序计数器

程序计数器是一块较小的内存空间,其作用是作为当前线程所执行字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等功能都是依靠这个计数器来实现的。

Java虚拟机实现多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。一个处理器(多核中的一个核)在一个时刻是能执行一个线程的一条指令,所以为了线程切换后能回到原来的位置,每个线程应该有单独的程序计数器。所以程序计数器是线程私有的内存区域。

如果正在执行的是native方法,则程序计数器为空。native方法是使用非java语言实现的方法,它的作用是扩充jvm,有了它,java可以做任何层次的应用,但是使用了native方法的程序可移植性会下降。

程序计数器区域不会与OutOfMemoryError。也是jvm内存区域中唯一不会发生该错误的区域。

2、Java虚拟机栈

线程私有。它描述的是java方法执行时的内存模型:每个方法被执行的时候会创建一个栈帧(Stack Frame)(方法运行时期的基础数据结构,即java虚拟机栈的栈元素),用来存储方法的局部变量表、操作栈、动态链接、方法出入口等。每一个方法被调用到执行完成的过程,对应着一个栈帧在java虚拟机栈中入栈到出栈的过程。

我们常说的栈内存和堆内存中,栈内存就是指java虚拟机栈的局部变量表。布局变量表的大小是在编译期间分配的,其空间大小在进入一个方法是就可以确定,运行过程中不会改变。

Java虚拟机栈可能出现两种异常:

(1)线程请求的栈深度超出虚拟机运行的最大深度,抛出StackOverflowError。

(2)扩展虚拟机栈时无法申请到足够的内存,抛出OutOfMemoryError

这两种异常实际是对一件事情额不同描述。一般在单线程环境下抛出StackOverflowError异常,多线程环境下抛出OutOfMemoryError异常。需要注意的是:多线程环境下,每个线程分配到的栈容量越大,可以建立的线程数就越少。因此,在多线程环境下发生内存溢出时,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。

java虚拟机栈容量由-Xss参数设置。

3、本地方法栈

与java虚拟机栈功能相似,为native方法服务,java虚拟机栈是为java方法(字节码)服务。有的虚拟机将这两个区域合二为一。本地方法栈也是线程私有的。本地方法栈大小是由-Xoss参数设置的,这个参数实际上无效,栈容量只由-Xss参数设置。

4、Java堆

Java堆是线程共享的内存区域,在虚拟机启动的时候创,用来存放对象实例。几乎所有的对象都在堆上分配,JIT编译器的发展和逃逸技术的发展产生了栈上分配、标量替换等优化技术。堆是垃圾回收管理的主要区域,也称“GC堆”(Garbage Collection Heap)。堆在物理上可以不连续,但是逻辑上必须连续。堆的大小可以固定,也可以动态扩展(通过-Xms和-Xmx设置,两个值设置为相同时堆大小固定)。

当堆出现OutOfMemoryError异常的时候,可以通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的对存储快照进行分析。分析时,首先分清楚是出现了内存泄漏还是内存溢出。如果是内存泄漏,进一步通过工具查看泄漏对象的GC Root引用链。如果不存在泄漏,就应该检查堆参数(-Xms和-Xmx),对比物理机内存看其是否可以调大。还要从代码上检查是否存在某些对象生命周期过长、持有状态时间过长,尝试减少程序运行时的内存消耗。

5、方法区

方法区也是线程共享的,用来存储已经被虚拟机加载的类的信息,常量、静态变量、及时编译器编译后的代码等数据。垃圾回收在这个区域进行比较少,主要是常量池的回收和类型的卸载。我们可以通过-XX:PermSize和-XX:MaxPermSize设置方法区的大小。在经常动态生成大量class的应用中,容易产生方法区内存溢出,要特别注意类的回收状况。

为了把堆中的垃圾分代回收机制扩展到方法区中,常常将方法区称为“永久代”。

6、运行时常量池

运行时常量池是方法区的一部分,用来存放编译期间生成的各种字面量和符号引用(类加载后存入)。允许期间也可能将新的常量放入常量池,比如String类的intern()方法。我们可以通过-XX:PermSize和-XX:MaxPermSize设置方法区的大小,从而间接设置常量池的大小。

7、直接内存

直接内存不是虚拟机运行时数据区域的一部分。但是虚拟机运行的时候,直接内存也会被频繁地使用。jdk1.4之后加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用native函数库直接分配到堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆和native堆中来回复制数据。

在进行虚拟机内存设置(-Xmx)时,直接内存容易被服务器管理员忽略,使得各个区域内存的总和大于物理内存的限制(包括物理级别和操作系统级别的限制),从而导致动态扩展内存是出现OutOfMemoryError。

直接内存容量可以通过 -XX:MaxDirectMemorySize指定,默认情况下跟java堆的最大值一样。DirectByteBuffer分配内存时抛出的内存溢出异常是通过计算得知的,并没有真正向操作系统申请。

二、对象访问

在java中,最简单的对象访问都涉及Java栈、Java堆、方法区三个内存区域。比如

Object obj=new Object();

这条语句出现在方法体中时,“Object obj”将反映到java栈的本地变量表中,作为一个引用类型的数据出现。“new Object()”会反映到Java堆中,形成一块存储了Object类型所有实例数据的结构化内存,这块内存的长度是不固定的。另外,java堆中还要保存能查找到此对象类型数据(如对象类型、父类、实现接口、方法等)的地址信息,而这些类型信息本身是存储在方法区中。

通过引用类型访问对象的方式有两种:使用句柄和直接指针。

使用句柄访问:在java堆中分配出一块内存作为句柄池,引用中存储的是对象的句柄地址,而句柄中包含对象实例数据和类型数据的具体地址。如下图所示:

深入理解JVM虚拟机笔记——第2章 JAVA内存区域与内存溢出异常一、JVM运行时数据区域二、对象访问

使用句柄访问的好处:在对象被移动(在垃圾回收时很常见)时无需改变引用变量的值,只需要改变句柄中指向对象数据的地址信息。

直接指针访问:引用中直接存储的是对象地址,需要额外考虑对象的类型信息的放置。使用直接地址访问的好处是节省了一次指针定位的开销,速度更快,在频繁访问对象时优势明显。

参考博客:

https://www.cnblogs.com/fengbs/p/7029013.html

https://www.cnblogs.com/parryyang/p/5726077.html