前言
我们写代码时,通常会关注代码与对象之间的流转。但实际上,我们有没有认真去关注过 java 程序运行时,类、对象、局部变量与方法调用链是存放在哪里的呢?
JVM 内存
众所周知,java 程序是执行在 jvm 虚拟机中的,那在众多的 jvm 虚拟机中,HotSpot VM 是当下最热门的或者说使用得最多的 jvm 版本。在 HotSpot VM 中,整个虚拟机内存被划分为几大模块:
- 堆(Heap)
- 方法区(Method Area)
- 程序计数器(Program Counter Register)
- 虚拟机栈(JVM Stacks)
- 本地方法栈(Native Method Stacks)
堆(Heap)
该区域就是我们平时正常 new 出来的对象待的地方,该区域也是我们平时接触得最多的区域,这里细分为:Young Generation(新生代) 和 Old Generation(老生代) ,也就是我们平时经常关注的 YGC 和 FGC 的地方,而 Young Generation 中间又再细分为三个区域:Eden、From Survivor、To Survivor,YGC 会有各种的算法来对这三块区域做针对性的垃圾回收算法,这里就先不展开讨论,有兴趣的可以参考 面试官,不要再问我“JAVA GC垃圾回收机制”了 。
该区域是所有线程所共享的,而存放的又是主要的业务对象,所以空间相对来说会是比较大的。一般可以使用 -Xms设置堆的最小空间大小 和 -Xmx设置堆的最大空间大小。
方法区(Method Area)
方法区和刚说的栈区域有很多相似的地方:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。而方法区通常也被称为非堆区域(non-heap),注意这要与堆外内存区分开!
方法区它存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。就是一些与运行时生成的对象区别开,是一个固定存放物资的区域。
方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。
程序计数器(Program Counter Register)
程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令,必须要有一个地方储存每个线程具体处理到哪一步了,该区域就是做这件事了。
虚拟机栈(JVM Stacks)
学习过汇编语言的同学也许会比较容易理解它的作用。它是一个记录线程调用方法模型的栈,每一条线程私有一个虚拟机栈,所以它也与对应线程生死与共。
其每个方法模型被称为栈帧,栈帧会存放 4 个属性:局部变量表、操作栈、动态链接、方法返回地址。
ps:对于其中的局部变量,如果是变量为基础类型,栈帧会直接存储对应的值,但如果是高级类型的话只会存放值的引用。
本地方法栈(Native Method Stacks)
与虚拟机栈作用相似,也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务。
对于 jvm 的内存,我们可以用这样的一张图来归纳起来以帮助对比理解。
堆外内存(Off-Heap Memory)
区别于 JVM 内存,java 常用的还有堆外内存。虽然 JVM 内存拥有非常完善的垃圾管理机制,可以让开发人员无需关注内存资源回收随心所欲地进行开发,但也正因为存在一连串非常复杂精妙的垃圾管理算法,导致在高并发情况(特别是写内存多)下可能会出现频繁的 YGC 和 FGC (每次 GC 都会引起程序卡顿),反而会成为性能瓶颈的帮凶!
针对这一类的业务情况,我们通常会使用堆外内存来提升我们的性能水平。堆外内存其实就是游离于 JVM 管理之外的物理机内存。不属于 JVM 的管理,自然就没有了 JVM 的那套 GC 算法,这样能使我们有更加好的扩展性和 IO 速度。
在JAVA中,可以通过Unsafe和NIO包下的ByteBuffer来操作堆外内存,也可以使用第三方堆外缓存管理包例如 ohc(off-heap-cache) 来操作。