前言
很久没有写博客了,不是自己停止了思考,很多自己对架构、计算原理的领悟比较零散,并且很多都记录在电脑上没有发出来。最近刚好有点时间,就把之前的理解整理出来。今天两年前写的,关于对JVM基础的理解。
JVM内存模型
内存模型整体上分为程序计数器、JAVA虚拟机栈、本地方法栈、JAVA堆、方法区,其中JAVA堆和方法区是线程共享的,其余的是线程私有无线程安全问题。
程序计数器
程序计数器是JVM内占用内存比较少的一块区域,线程私有。当执行的是JAVA代码时,记录的值是JAVA字节码指令地址,可以看成是当前线程代码执行的记录器,当执行的Native代码时,程序计数器记录的值是undefined,由于程序计数器内存占用不会随着程序运行而改变,所以程序计数器是唯一一片没有规定任何outOfMemoryError的区域。
JAVA虚拟机栈
JAVA虚拟机栈也是线程私有的,JAVA虚拟栈可以看成是JAVA方法执行的内存模型,JAVA虚拟机栈对每个方法都有对应的栈帧,栈帧中存着JAVA方法内定义的基本数据类型、引用类型、局部变量表,操作数栈,动态链接,返回地址等数据。一个JAVA方法从执行到结束对应了JAVA虚拟机栈中对应的栈帧中数据入栈和出栈。
如果请求栈深度过大会抛出StackOverflowError,如果请求扩展栈空间失败会抛出outOfMemoryError。
本地方法栈
本地方法栈和JAVA虚拟机栈基本一样,只是本地方法栈执行的是c代码写的本地方法而已,在hotspot虚拟机中干脆把本地方法栈和JAVA虚拟机栈合并了。
JAVA堆
JAVA堆是线程共享的,所有线程都可以共享这片内存,线程安全问题就会出现在这片内存里。几乎所有的java对象都是在这块内存分配内存的,并且JVM的GC也是主要在这片区域内,JAVA堆还可以细分为年轻代和年老代两片区域,这两块区域都是一大块物理连续的内存空间。其中年轻代因为复制算法还可以细分为Eden区域和两个survivor区域。如果堆内存空间满了,GC过后也无法分配更多空间会抛出outOfMemoryError错误。
方法区
方法区与JAVA堆一样也是线程共享的,方法区存储的是被JVM加载的类信息,常量,静态变量和即时编译器编译后的代码数据。需要多提一句的是在方法区内保存常量是通过运行时常量池。运行时常量池用于存放编译期生成的各种字面量和符号引用,同时在运行期间也可以把常量放入常量池,用的比较多的就是string.intern()方法。
直接内存
直接内存并不是JVM虚拟机运行时数据区域的一部分,也不是JVM规范中定义的内存区域,但是从JDK1.4之后也会频繁使用,因为JAVA NIO。因为之前BIO在读写数据时内存是在JVM堆内分配,所以在操作读写时需要将数据从JVM堆内拷贝到另一块与JVM运行时无关的一块缓冲区再进行用户态内核态切换把数据从用户空间复制到内核空间缓冲区,为什么要从JVM堆内存copy到用户空间的缓冲区是因为JVM会进行GC,可能会把对象移动导致内存地址变化。这样凭空多一次copy操作导致性能底下,所以出现了直接内存,每次进行IO操作时直接在JVM堆区域创建一个DirectBuffer对象指向直接内存地址,数据读写直接在直接内存中,减少了copy操作。但是直接内存分配慢所以一般的数据操作最好还是使用对内存,IO操作才使用直接内存。
基本上上述内存划分就是JVM内存模型,符合JAVA虚拟机规范,但是在我们接触最多的Hotspot虚拟机里面很模块并不是完全像上述一样,在JDK7之前基本符合上述描述,但是在JDK8之后已经把方法区去掉了,换成了元空间区域,
元空间
metaspace 在JDK8之后用户替代perm永久代,我们都知道JDK8以前perm这块内存区域保存的是JVM加载的类信息Klass数据,可以通过XX:permSize和XX:maxPermSize来设定永久代内存区域大小,而JDK8之后的metaspace变成了由klass Metaspace和NoKlass Metaspace空间组成。其中Klass Metaspace就是用来存储klass数据的而NoKlass Metaspace则是保存类中的方法定义信息和常量池的,并且如果compressedClassSpaceSize关闭的话,klass Metaspace空间可以没有,klass数据也可以存储在NoKlass Metaspace中。
从JVM内存模型谈谈反射
大家都知道java的动态特性就是因为反射的存在,反射是java可以在任意运行时期拿到任意一个类和获取这个类的所有属性,可以在任意时刻生成类的实例和调用实例的任意方法。这个反射的原理就是当类加载时,元空间创建KlassInstance后会同步在堆中再创建一个Class对象实例,就是方便在运行期编码层可以拿到类定义的所有属性,换句话说,这个Class对象是KlassInstance的实例映射。
创建实例对象在JVM中内存分布
当我们写下代码创建一个对象的时候如Object obj = new Object()
时会在三个内存区域存储,其中如果该object类没有被加载会被类加载器加载进jvm,然后在方法区生成一个描述该类的KlassInstance实例,然后同步在堆创建这个类的class实例,接着obj压入java虚拟栈的栈帧中,最后创建具体对象实例存在堆中。