JVM架构及内存模型详解
JVM内存架构
JVM架构主要分成了三个部分:
- JVM内存模型,主要包括了方法区、堆、虚拟机栈、程序计数器、本地方法栈。在下面进行展开。
- 执行引擎,包括最核心的解释器和GC垃圾回收器,还包括了JIT编译器。
- 本地方法接口和库
JVM内存模型
这里我们将结合着第二张图一起来看。
方法区
在JVM的规范里是存储一些常量的区域,主要是类常量。因为运行时也可以产生常量,比如我们最常用的String,所还包括了Runtime Constant Pool。最后它还包括了Code Cache一部分,这部分是存储JIT优化编译的内容。
存储在哪? 这个地方来说是存储在堆上,因为非栈即堆嘛。但是因为GC并不经常需要管理这块区域,所以才有了一个逻辑的区分,好让GC能能够识别。这也就是为什么要称之为永久代的原因。
那为什么永久代最后也取消了呢?
这个是在JDK7的时候与JRockit融合时发现,JRockit没有永久代,所以它也取消了,使用了新的元空间来替代,不继续在堆上,而是放入到直接内存里。主要带来的好处是:分配内存更灵活。这里有一个要注意的点是-XX:MetaspaceSize的作用,从Oralce文档我们可以看到这是什么时候触发一次Full GC,而不是最大值。根据平台的不同,默认值是12~20MB。
而真正用来限制元空间大小的参数是-XX:MaxMetaspaceSize。
堆
这个很都熟悉,是用来存储我们对象的地方。也是垃圾回收主要的区域,分成年轻代和老年代。
这里要注意的有两个点:
- 由于优化编译器的进步,现在对象在堆上分配已经不是那么绝对。
- TLAB(Thread Local Allocation Buffer)这个概念,指的是在Heap每个线程可以拥有自己独立的线程分配缓存,可以更高效的分配内存。
虚拟机栈、本地方法栈、程序计数器
这两个都是线程私有的。
在第二张图中可以看到,程序计数器比较小,它主要负责的是当前线程所执行的字节码的行号指示器。如果熟悉操作系统的CPU指令暂存对这个理解起来会比较容易,主要还是CPU的分片时间机制,所以每一个并发需要保存自己的指令和数据。程序计数器指向的是下一条指令的内存地址。
虚拟机栈和本地方法栈都是栈,区别是本地方法栈是Native的方法。
虚拟机方法栈描述的是Java方法执行的内存模型,每一个方法被执行的时候,都会创建一个桢(Frame)。桢的信息主要包括了返回值,局部变量表,元信息引用,操作数栈。
局部变量表中的对象会指向堆,也会指向运行时常量池(Runtime Constant Pool)。
运行时常量池
其实目的是很简单,就是对一些常量做缓存,避免重复生成,在JVM的规范我们可以看到是放在方法区的,但是Sting字符串是比较特殊的。我这里对它的理解是String类型的长度不固定,所以在正常GC过程中价值会比较大。
此外,CodeCache 这里主要是JIT编译器的结果缓存,避免重复编译指令,可以提高性能。
一些问题
- 基本类型存储在哪? 是线程私有的吗?
基本类型要分成是函数执行过程的还是对象的字段,前者会存储在栈上,后者会存储在Heap上。 - 为什么要有基本类型呢?主要基本类型用的比较多,分配对象动作比较重,所以为了性能考虑。
我们常说变量会共享是为什么呢? 这里是指栈上可以有多个变量指向同一个基本类型