Java内存区域划分

阅读了《深入理解Java虚拟机》之后的学习记录。在理解了Java的内存区域划分的情况下对于出现OOM问题等一些列内存问题以及调优方面将会有很大的帮助,也可以让自己在编写代码的时候能够预防某些可能出现内存泄漏或者内存溢出的情况,编写高质量的代码。


运行时数据区域

Java虚拟机在运行的过程中会将虚拟机所管理的内存划分为若干个不同的区域,每个区域存放不同的数据担负着不同的职责。下面是运行时数据区域的图示:


其中绿色部分表示各个线程都可以访问到的线程共享的数据区域,不乏有熟知的堆(Heap)内存等区域。而灰色部分表示线程隔离的数据区域。


程序计数器

虽然图中看起来程序计数器(Program Counter Register)占用了很大的一部分(主要是因为名字很长吧),实际上程序计数器只占用了比较小的一块空间,与汇编语言中的程序计数器比较类似(但是汇编语言是直接指的是物理意义上的计数器,这里是针对JVM的), JVM的程序计数器可以看作是 当前线程所执行的字节码的行号的指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能。
在多线程环境中,为了线程切换后能够恢复到正确的执行位置,所以每一个线程都需要有一个独立的程序计数器,各个线程之间的程序计数器独立存储,互不影响,所以称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java的方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器的值则是空的(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。

Java虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtural Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直至执行完成(或者抛出异常)对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表(Local Variable Table):存放了 编译期可知的各种基本数据类型(int, byte, short, char, long, float, double,boolean),和对象引用(不是对象本身,JVM可以通过这对象引用直接或间接的查找到对象在Java堆中的数据存放的起始地址索引,并且可以直接或者间接的查找到对象所属的数据类型在方法区中的存储类型信息)。局部变量表的空间在 编译期间完成分配,当进入一个方法的时候,这个栈帧中所分配的局部变量表的空间是确定的,并且在运行期间不会改变它的大小。在方法执行的时候,虚拟机使用局部变量表来完成参数值到参数变量列表的传递,如果执行的是实例方法(不是static方法),局部变量表中第0个索引位置默认用于传递方法所属的对象的引用,这也是为什么在实例方法中我们能够使用this关键字来访问到所属的对象(因为所属对象作为隐含的参数被传递)。
操作数栈(Operation Stack):是一个后进先出的结构,用于完成方法中如加减乘除等操作的,具体原理可以百度。
动态链接:在Java的Class文件在加载完成之后在常量池中存有大量的符号引用,通过这些符号引用可以知道方法调用的时候到底应该调用哪个方法(在Java的重载和覆盖中表现出来)。如果是static的方法,那么这个符号引用就会在类加载阶段或者第一次使用的时候转化为直接引用,而另一部分则是在运行期间才能知道调用哪一个方法(对应Java的多态性)。

异常情况:对于虚拟机栈存在两种异常1)纵向,如果线程请求的栈的深度大于虚拟机所允许的深度,那么抛出StackOverflowError。比如递归调用的时候递归的次数过多或者无限递归。2)横向,虚拟机栈扩展时如果没有申请到足够的内存那么抛出OOM(一般虚拟机栈允许动态扩展)。可以理解为不断的创建线程时导致内存溢出。

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈类似,只不过本地方法栈作用于本地方法(Native 方法)。

堆(Heap)是Java程序员最熟悉的一个区域,也是Java程序中虚拟机所管理的内存最大的一块。同时堆也是各个线程共享的一块内存区域。所有的对象实例和数组都要在堆上分配,但是也不是那么的绝对,比如在HotSpot的虚拟机中java.lang.Class对象实例就是存放在方法区中的,作为程序访问方法区中这些类型数据的外部接口。
异常情况:如果在堆中没有内存完成实例的内存分配,并且堆也没办法扩展时,将会抛出OOM。当使用-Xmx(最大堆内存)和-Xms(最小堆内存)控制堆大小的时候,如果-Xmx等于-Xms那么就无法扩展。反之则可以动态扩展。
OOM:
public class Demo {
	
	public static void main(String[] args) {
		List<Test> list = new ArrayList<>();
		while (true) {
			list.add(new Test());
		}
	}
	
	static class Test {
		//1M的内存
		private byte[] memoryHolder = new byte[1024 * 1024];
	}

}


方法区

方法区(Method Area)是各个线程共享的数据区域,它用于存储已经被虚拟机加载的类信息(类的版本,字段,方法,接口等信息),常量,静态变量,即时编译器编译后的代码等数据。方法区中也包含了平时经常提到的运行时常量池,用于存放各种字面量和符号引用。如经常说的字符串常量池就是存放的这里。例如String的intern方法()就是对运行时常量池的应用。
异常情况:当方法去无法满足内存分配需求时则抛出OOM。


*直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据的一部分,也不是Java虚拟机规范中定义的内存区域,可以理解为是除了上述五种内存区域以外,虚拟机没有获取到的操作系统中的其他内存区域。这或许听起来很玄幻,实际上这部分也很可能会发生OOM问题。比如Java中的NIO操作就是使用的本地方法直接分配的堆外内存,既然是内存那么就要受到机器实际内存的限制,当内存不够的时候就会抛出OOM,比如操作系统内存最大为2G,那么一般堆内存一般最多划分个1.5,1.6个G左右就差不多了,然而剩余的一部分除了操作系统本身会占用一部分之外就只剩下很少的部分了,这个时候如果有大量的NIO操作,那么很有可能发生OOM。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值