JVM内存管理
程序要运行,就需要有数据,有了数据,自然要存储,而不同的数据有不同的存储方式,存储空间,以及管理方式。Java程序自然也不例外,因此有了JVM的内存管理。只有可以高效的分配和释放内存,程序才能更加高效的运行。
常见的数据:
- 静态变量
- 常量
- 局部变量
- 超大对象
- 小对象
- …等等
1、JVM内存区域划分图
2、程序计数器
程序计数器,记录当前正在执行的字节码的地址,通俗而言就是记录当前执行到的的字节码的行号,如果当前正在执行的是一个本地方法的话,该值为空(Undifined)。程序计数器也成为PC指针。
程序计数器的作用
Java虚拟机的多线程是通过切换CPU时间片的方式来实现的,任意时刻,一个单核处理器只能执行一个线程中的指令。为了在发生线程切换后,CPU能从正确的位置继续执行,因此需要保护现场,另外对于程序中的异常,循环,分支跳转等都需要程序计数器。程序计数器只记录所在线程的当前执行的字节码的行号,每个线程都有属于自己的程序计数器,程序计数器属于线程私有的。
可能的异常或者错误
因为程序计数器只记录字节码的行号,因此所占内存空间特别小,不会发生OOM和StackOverflow。(也是唯一一块不会抛出OOM的内存区域)。
2、Java虚拟机栈
Java虚拟机栈,描述Java方法执行的内存模型,方法调用时,都会创建一个栈帧,栈帧中存储方法的局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。可以通过-Xss来设置虚拟机栈的大小。
主要包含以下信息:
2.1、局部变量表
局部变量表存储编译器可知的数据类型(8大基本数据类型和对象引用),因为Java是一门跨平台的语言,因此这些数据类型的大小在编译器间就是已知的,因此局部变量表的内存空间在编译期间就已经分配完毕,且在运行期间不会再改变。
这些数据类型在局部便量表中的存储空间以局部变量槽来表示,局部变量槽的大小以32bit和64bit居多,由虚拟机自身决定。
局部变量表类似于操作系统的内存,用于存储CPU执行所需要的数据。
2.2、操作数栈
操作数栈相当于一层缓存,每次执行引擎在读取数据时,都预先将局部变量表中的数据拷贝到操作数栈中。满足先进后出的原则。
2.3、可能的异常或者错误
当方法调用的栈深度超过虚拟机运行的栈深度时(没有出口的方法递归调用),就会抛出StackOverflow错误。一般而言栈的深度是可以扩展的,当扩展时无法再申请到内存时就会抛出OutOfMemroy错误。那么到底抛出哪个错误呢?
虚拟机栈中其实存着着两种栈,第一种是栈帧,对应着方法,每一个方法的调用都会形成栈帧,压入虚拟机栈中,遵循先进后出的原则。另一个是操作数栈,对应着操作数。
3、本地方法栈
本地方法栈的作用与Java虚拟机栈的作用基本是一致的只是,本地方法栈上执行的是Native方法,而虚拟机栈上执行的是Java方法而言。而且堆本地方法栈的使用并没有严格规定,因此有的虚拟机直接将两部分合二为一。
可能的异常或者错误
与虚拟机栈一样该区域也可能抛出StackOverflow和OOM,且原因和本地方法栈是一样的。
4、Java堆
Java堆在虚拟机启动时创建,是Java虚拟机管理的内存的最大的一块内存。在Java中几乎所有的对象和数据都在Java堆上进行内存分配。Java堆是垃圾回收器管理的主要区域,因此也成为GC堆。Java堆上存放对象实例数据,和数组,但并非所有的对象实例和数组都在堆上分配。Java堆在逻辑上是连续的,在物理地址上一般是不连续的。
由于对象生命周期不同及垃圾收集器的原因,又将堆空间分为新生代和老年代,其中新生代又分为Eden区和两个survive区,至于为何要这样划分详见JVM垃圾回收算法。
可能的异常或者错误
当堆中的对象所占内存达到最大值,这时候再创建新对象就会抛出OOM。堆可以通过-Xmx和-Xms来控制相应的大小。
虽然Java堆是线程共享的,但从内存分配的角度而言,也有内存私有的部分,在分配内存时,为了解决并发问题,为每个线程独自分配一块小空间(TLAB) 分配缓冲区,这样每个线程有自己独立的空间进行内存就不会出现并发问题。
5、元空间
在JDK8及其以后,引入了元空间的概念,取代了之前的永久代,通过-XX:MaxMetaspaceSize设置其大小,这块空间是在堆外内存上的。因为之前的永久代在堆上,这个区域会存储类信息静态变量常量等,甚至string.intern等都会进入这个区域,容易造成OOM。
而直接使用直接内存,会减少OOM的概率。