背景:听说Java运行时环境的内存划分是挺进BAT的必经之路。
内存划分:
Java程序内存的划分是交由JVM执行的,而不像C语言那样需要程序员自己买单(C语言需要程序员为每一个new操作去配对delete/free代码),放权给JVM虚拟机处理有利也有弊,好处是不容易出现内存泄漏和内存溢出问题,坏处就是自己的屁股不能自己擦,万一有一天JVM罢工不释放了,还是自个忘了释放,So了解虚拟机容易引起内存泄漏和溢出的场景对Java程序员来说还是必不可少的。【内存泄漏:Out Of Memmory,系统已经不能再分配空间了,好比你需要50M的空间,系统就只剩下40M;内存溢出:Memmory Leak,开辟了资源空间但用完后忘记释放,内存还在被占用,多次内存泄漏就会导致内存溢出;】了解JVM内存划分要端到端,先从Java程序执行的具体过程来看:
从图1中可以清楚看到Java程序的执行过程,大致就是Java源代码(后缀为.java)首先被Java编译器编译成字节码文件(后缀为.class),然后交由JVM中的类加载器加载各个类的字节码文件,加载好字节码文件后再交由JVM引擎执行。在整个程序执行过程中,上图中运行时区域会用一段空间来存储程序执行期间需要用到的数据和相关信息,也就是我们弄懂内存划分要深度研究的区域,即JVM。
运行时数据(内存)区:
图2中,梯形形状的部分是所有线程之间共享的区域,长方形形状的部分则是线程运行时独有的数据区域;《Java虚拟机规范》固定了运行时区域包括:程序计数器、Java栈(虚拟机线)、本地方法栈、方法区和堆五大部分。
1、程序计算器(Program Counter Register)
程序计算器又称为PC寄存器,这块内存区域相当小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计算器的值来选取下一条需要执行的字节码命令;在JVM中多线程是通过线程轮流切换来获得CPU执行时间的,So无论何种情况下一个CPU的内核只会执行一条线程中的指令,而为了保证每个线程都在线程切换后能够恢复到切换之前的程序执行位置,每个线程就必须要有自己独立的程序计数器,因此程序计数器是每个线程私有的;在JVM中,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址,而线程若是执行native方法则程序计数器中的值是undifined;因为程序计数器中存储的数据所占内存空间的多少不会随这程序的执行而改变,于是程序计数器不可能发生内存溢出OOM现象;
特性:
a. 是当前线程所执行的字节码的行号指示器;
b. 是当前线程私有的;
c. 不会发生OOMError的错误;
2、Java栈(虚拟机线Java stack)
简单的说Java栈就是Java方法执行的内存模型。其内部存放的是一个个的栈帧,每个栈帧对应着一个被调用的方法;栈帧中包括局部变量表(Local Variables)、操作数栈(Operation Stack)、志向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息;由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的互不干扰的Java栈;当当前线程执行了一个方法,随之就会创建一个与之对应的栈帧,并将建立的栈帧进行压栈操作,当方法执行完毕后便会将栈帧出栈;So线程当前执行的方法所对应的栈帧必定位于Java栈的顶部,即如队列的先进后出。下图表示了一个Java栈的模型:
局部变量:
局部变量表就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参);若变量是基本数据类型则直接存储它的值,若变量是引用类型则存储的是指向对象的引用;而局部变量表的大小在编译的时候就已经确定了,So程序执行期间其大小亦是不会改变的。
操作数栈:
操作数栈就是用于对表达式求值计算的,当个线程执行过程实际上就是不断执行语句的过程,也就是不断计算的过程,So程序中的所有计算过程都是通过操作数栈来完成的。
指向运行时常量池的引用:
指向运行时常量池的引用是由于在方法的执行过程中有肯需要用到类中的常量,因此必须要有一个引用指向运行时常量。
方法返回地址:
一个方法执行完毕后,要返回之前调用它的位置,于是在栈中就必须保存一个方法返回的地址。