Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一
Java内存区域
对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出、内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有助于帮我们理解Java虚拟机到底是如何解决内存问题,如果出现了内存泄露或内存溢出等方面的问题,我们也可以找到问题的解决方案。
Java内存区域划分
Java虚拟机在执行Java程序的过程中,会把它管理的内存划分成若干个数据区域。有的区域随着虚拟机进程启动而存在,有的区域则是依赖用户线程的启动和结束而建立和销毁,Java运行时数据区的划分如下图,方法区和堆属于所有线程共享的数据区,而虚拟机栈、本地方法栈、程序计数器则是每个线程有一个。而执行引擎将会在后面的章节介绍。
程序计数器
程序计数器,可以看做是当前线程执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转等基础功能都依赖于此计数器完成。
关于字节码:字节码是我们写好java源代码以后,经过编译生成的可以使JVM读懂的语言。比如我们编译后得到的.class文件。
我们写好的java源码最终变成可执行文件的过程是:java源码-->经编译器生成的可执行java字节码(比如虚拟指令或者.class文件)-->经过JVM解释器变成机器可以执行的二进制文件-->程序运行。
由于Java虚拟机的多线程是通过线程轮流切换并且分配处理器执行时间的方式来实现的,因此同一时间对于一个处理器而言,只有一个线程中的一条指令会被执行,因此,为了切换线程后,不同线程可以恢复到正常的位置,程序计数器是每个线程含有一个。他们之间互不影响,独立存储。
如果执行的是Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,那么计数器为空,此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。
Java虚拟机栈
Java虚拟机栈的生命周期与线程相同,每个方法执行的时候会创建一个栈帧,用于存放局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolen、byte、char、short、int、float、long、double)、对象引用(reference)和returnAddress类型(指向了一条字节码指令的地址).局部变量表所需的内存空间在编译期间完成非配,方法运行期间不会改变局部变量表的大小。
我的理解是,对于在方法执行中,New新的对象、增加数组等,其本身是在堆上申请空间,然后返回一个引用存储在对应方法的栈中,因此局部变量表其大小在编译器就可以确定。
本地方法栈
本地方法栈与虚拟机栈作用基本一致,只不过虚拟机栈为执行Java方法服务,而本地方法栈是为调用的Native服务。
Java堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的就是存放对象实例,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。
方法区
方法区也是各个线程共享的内存区域,用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
运行时常量池
运行时常量池,是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池,用于存放编译期生成的各种字面量和符号引用。
对象访问
关于对象访问的问题,我们来看一个很典型的例子:
Object obj = new Object();
这一个简单的对象创建,实际涉及到了Java栈、Java堆、方法区三个区域的关联关系。
首先对于Object obj而言,这在每个线程单独的虚拟机栈里创建了一个reference类型变量,指向Java堆中的一个地址,在Java堆中,存放的是new Object()新创建出来的实例数据值的结构化内存,而在方法区,则会创建该对象的类型、父类、接口、方法等地址信息。
而实现reference类型指向对象的引用,有两种方法进行实现:句柄方法和直接指针
如果使用句柄方法,在Java堆中将会划分出一块内存作为句柄池。java栈中的reference中存放的是的对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息。
如果使用指针访问方法,Java堆对象就必须考虑如何放置访问类型数据的相关信息,reference中存放的就是对象地址。
使用句柄的好处是,reference存储的地址是稳定的句柄地址,就算对象因为垃圾回收机制而被移动(是很正常的事情),也只需要Java堆里的实例数据地址进行修改即可,在栈里的reference地址不需要改变。
而使用直接指针访问的方式,就是速度快,reference存储的地址就是对象所在地址,节省了一次指针定位的开销。但缺点就是,如果对象在堆里发生了改变,那么在栈中的reference指向内容也要改变。
实际问题处理
堆溢出
堆溢出的问题,重点是确认内存中对象是否是必要的,也就是检查是否有内存泄露问题。是否有对象已经不再需要使用,但仍然无法被垃圾回收机制收集。如果没有这样的对象,那么就应当检查虚拟机的堆参数是否合适、能否再扩大等问题。
栈溢出
在多线程中,可能是产生的线程过多,从而导致每个线程可支配的内存容量变小,导致OutOfMemory的问题。也可能是同一个线程中执行内容栈帧过大,导致StackOverflowError的问题。