前言
Java虚拟机的内存区域主要分为:程序计数器,虚拟机栈,本地方法栈,Java堆,方法区🖊
其中,栈描述方法执行,及方法的局部变量和引用;堆描述对象;方法区描述类和常量
其中,栈结构线程私有,堆结构线程共享(方法区描述类,也是堆结构)
Java虚拟机运行时数据区域
上图理解:
堆结构线程共享,描述所有的对象(类在方法区中,方法区也是堆);
栈和计数器线程私有,描述方法执行,和存放引用
Java虚拟机在运行时会把它所管理的内存划分为若干个不同的数据区域。大致分为以下:
程序计数器:在Java中,每个线程都有一个独立的程序计数器,独立存储,该段内存是线程私有的
Java虚拟机栈:描述方法执行的内存模型,以及存放局部变量,对象引用:
每个方法在执行的同时会创建栈帧用于存储局部变量表、动态链接等信息。
其中,局部变量表存放基本类型、对象引用、returnAddress类型。其内存空间大小分配在编译期间就可以完全确定,之后也不会改变。
- 线程私有的,生命周期与线程相同。
- 当线程请求的栈深度大于虚拟机允许的深度,抛出StackOverFlowError异常
- 可动态扩展,可能会产生OutOfMemoryError异常(OOM)
本地方法栈:描述Native方法的内存模型,其他与虚拟机栈一样。
Java堆:存放对象实例,几乎所有的对象实例都在这里分配内存。
- 被所有线程共享这一块内存区域,是Java虚拟机所管理的最大一块内存
- 堆是GC垃圾收集器管理的主要区域(在普遍使用的分代手机算法中,堆可分为:新生代和老年代)
- 对线程来说,还划分出多个线程私有的本地线程分配缓冲区(TLAB)
- 以上无论如何划分,堆存放的都是对象实例,划分只是为了更好地回收和分配内存
- 可动态扩展,可能会产生OutOfMemoryError异常(OOM)
方法区:存储Class文件的相关信息:已被加载的类信息、常量、(类变量)静态变量、编译后的代码等
- 所有线程共享
- 运行时常量池:Class文件的常量池的信息,在类加载后进入方法区的运行时常量池
- Java不要求常量一定要编译时产生,可以运行期间产生放入方法区常量池,例如String.intern()
- (此外Class文件中还有类版本、字段、方法、接口等描述信息)
- 这部分的GC较少,在此区域GC主要为了针对常量池回收和类型卸载。
- 其实它也是堆结构,但是要与Java堆区分,称为“Non-Heap”非堆
- 可动态拓展,可能会产生OutOfMemoryError异常(OOM)
直接内存:
- 可动态拓展,可能会产生OutOfMemoryError异常(OOM)
OutOfMemoryError异常(内存溢出)
申请的内存空间超过了系统实际分配的空间(系统无法满足内存申请) ,就会发生OutOfMemory异常(以下简称OOM)
除了程序计数器外,其他几个运行时区域都有可能发生OOM异常
Java堆溢出
堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径以避免对象被垃圾回收,那么对象数量在到达堆最大容量限制是抛出内存溢出OOM异常。
分清楚是内存泄漏还是内存溢出
内存泄漏:申请内存后,无法释放已申请的内存空间 。
比如,你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
内存溢出:申请内存时,没有足够的内存供申请者使用。
比如:给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,内存不够用,就会报OOM,此时内存溢出。
又比如:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。
虚拟机栈和本地方法栈溢出
(HotSpot虚拟机不区分这两个栈)
StackOverFlowError:线程请求的栈深度大于虚拟机允许的深度
OutOfMemoryError:扩展栈时无法申请到足够的内存
单线程测试下,无论是栈容量少,还是栈帧太大,都抛出了StackOverFlow异常
而在多线程下,通过不断建立内存用量大的线程,迅速耗尽内存空间,会抛出OOM
方法区溢出
通过不断产生动态类(如反射和动态代理、JSP等会加载Class的操作),在运行时产生大量的类去填满方法区,直到溢出,抛出OOM。