Java的技术体系包括
- 支持Java程序运行的虚拟机(JVM)
- 提供接口支持的Java API
- Java 编程语言
- 第三方Java框架(如Spring等)
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
对于Java开发者而言,由于 Java虚拟机规范 提供了一套自动内存管理的机制,使得开发者无需去关注每一个对象的分配和回收,同时在绝大多数情况下JVM保证了内存泄露和内存溢出的问题不容易出现。但也带来了新的问题,因为Java开发者把内存控制的权利交给了JVM,这就导致很多的开发者不关心Java对象的生命周期和在内存中的分配回收原理,从而写出了并不能使JVM内存管理机制高效运行的代码,进而增加了内存泄露和内存溢出的风险,而当这些问题一旦出现,去解决这样的问题又显得无从下手。
因此我们需要去了解在Java虚拟机中,是如何定义内存空间的,程序运行过程中,又是如何分配和回收内存空间的。
JVM内存区域的划分
Java虚拟机在程序运行时会把它所管理的内存划分为若干个不同的数据区域。每个区域都有不同的用途、创建和销毁的时间。有的区域伴随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束来进行创建和销毁。因此,按照Java虚拟机规范,java运行时数据区域可以分为一下几类
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
程序计数器
流程控制
程序计数器是一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器。JVM需要通过该计数器的值来获取下一条需要执行的字节码指令,因此程序中的分支,循环,跳转,异常处理,线程恢复等基础功能都需要通过程序计数器来完成。在多线程的环境下,在线程切换后再恢复时需要继续回到刚才的位置执行,这就需要每条线程都拥有独立的程序计数器,且各线程之间计数器互不影响,相互独立,因此程序计数器所在的内存区域是线程私有的。
另外当正在执行的是一个Java方法时,计数器记录的是正在执行的虚拟机字节码指令地址,如果是一个Native方法,计数器的值为空。此区域也是Java虚拟机规范中唯一一个没有规定OOM的区域。
总结如下:
- 用途:标志当前线程执行到了哪一条指令
- 生命周期:跟随线程的创建销毁同步,因此也是线程私有的
- 特征:唯一一个无OOM的区域
Java虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的时候,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行完成,对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期间可知的基本数据类型,对象引用和returnAdress类型。其所需的内存空间,在编译期间完成分配,也就是说,当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
此区域规定了两种异常,StackOverflowError,即线程请求的栈深度大于虚拟机所允许的栈深度。OutOfMemoryError,虚拟机栈动态扩展时无法申请到足够的内存。
总结如下:
- 用途:用于存放方法执行过程中的数据
- 生命周期:与线程生命周期同步
- 特征:一般最为关注的区域是局部变量表,也被通常称为“栈内存”。大小在编译期间完全确定
本地方法栈
为虚拟机使用到的Native方法服务。同样也会抛出StackOverflowError和OutofMemoryError异常
Java堆
Java堆是虚拟机所管理的最大的一块内存区域,此区域唯一目的就是用来存放对象实例,几乎所有的对象实例都在这里分配内存。因此是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
Java堆也是垃圾回收期管理的主要区域,因此也被称为“GC堆”。Java堆从内存回收和内存分配的角度来看又可继续细分,但无论如何划分存储的都是对象实例,划分的目的是为了更好地回收和更快地分配。在堆中没有内存完成实例分配并且堆也无法再进行扩展的时候,会抛出OutOfMemory。
总结:
- 用途:存放对象实例和数组
- 生命周期:与虚拟机启动而创建,进程结束销毁
- 特征:最大的一块内存区,垃圾回收的主要区域,开发重点关注区域
方法区
用于存储已经被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据。同样也是各个线程共享的区域。Java虚拟机规范对方法区的限制非常宽松,不需要连续内存空间,可以选择固定或者可扩展大小,可以不实现垃圾回收。所以,相对而言,垃圾收集行为在这个区域是比较少出现的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。
总结如下:
- 用途:存放虚拟机加载的类信息,常量,静态变量等
- 生命周期:虚拟机启动时创建
- 特征:垃圾回收行为不太关注
运行时常量池
属于方法区的一部分,Class文件有一项信息是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分将在类加载后进入方法区的运行时常量池中存放。
直接内存
直接内存不是Java虚拟机规范定义的内存区域,在JDK1.4中新加入了一种通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存。这样能在一些场景下显著提高性能,避免了在Java堆和Native堆中来回复制数据。