JVM-内存模型

Scroll Down

JVM内存模型

使用java有些时间了,对于jvm还一直没有很好的了解过,一句老话说得好如果你不去了解jvm,那么你怎么去做性能调优,不会调优那么就无法成为一个高手了。最近刚好了看了jvm相关的资料,然后记录下来,填补下这部分的知识空洞。

Jvm也就是俗称的java虚拟机,至于说java虚拟机是什么,我们就不在这里多说了,可以百度下资料很多,我们主要说说jvm内部是怎么组成的。通常来说在java程序在运行时程序内部的数据都是保存在jvm的内存中,那么jvm是怎么保存运行中的java程序的数据的。我们可以看看jvm运行时的数据区都有哪些。

JVM运行时数据区域: 程序计数器,栈,本地方法栈,堆,方法区/元数据区,运行时常量池,直接内存。
内存模型分析
JDK1.8之前
JVM运行时数据区域
Jvm_01
JVM中的线程
Jvm_02
JDK1.8
JVM运行时数据区域
Jvm_03

程序计数器

程序计数器可以看作是当前线程所执行的字节码的行号指示器。jvm是把class文件当作字节码来处理的,字节码解释器工作的时候通过改变程序计数器的值来获取下一条需要执行的字节码指令。字节码解释器需要依赖程序计数器来完成指令分支,循环,跳转,异常处理,线程恢复等基础功能
jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时间点,一个处理器(多核处理器就是其中一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都有一个自己独立的程序计数器,各个线程之间的计数器互不影响,独立存储,一般我们把这部分内存区域叫做"线程私有"的内存,也就是说java多线程之所以能确保各个线程都可以按照自己调用顺序运行,就是因为有程序计数器来记录各自线程的执行位置。
如果线程执行的是java的方法,那么计数器记录的是正在执行的虚拟机字节码执行地址;如果执行的是native方法,那么计数器值为空(undefined)。这部分内存区域是唯一一个在jvm中没有规定任何outOfMemoryError的情况的区域,也就是说如果outOfMemory不会发生在程序计数器。

jvm栈和程序计数器一样也是线程私有的,栈的的生命周期也线程一样,也就是说每个线程都有一个栈,虚拟机栈描述的是java方法的内存模型。jvm会对每个执行的的方法创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用至执行完成的过程,对应着一个栈帧在jvm栈中如栈和出栈的过程。

本地方法栈

类似于java虚拟机栈,区别于jvm栈的是,jvm栈服务与jvm的java方法,本地方法栈服务与jvm用到的native方法

jvm堆是虚拟机中最大的一块内存,在jvm启动的时候创建,是所有线程共享的一块内存区域,几乎所有的对象实例和数组都在堆上面分配的。

方法区

永久代 or 元数据区
方法区:是所有线程共享的一块内存区域,用来存储已经被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据。方法区也有一个别名叫做‘非堆’(Non-Heap)和堆区分开来。
对于Hotspot虚拟机来说,在JDK1.7的时候很多人更愿意把方法区叫做‘永久代’,就以HotSpot 虚拟机来说,在 JDK1.8 之前,方法区也被称作为永久代,这个方法区会发生我们常见的 java.lang.OutOfMemoryError: PermGen space 异常,注意是永久代异常信息,我们也可以通过启动参数来控制方法区的大小:

-XX:PermSize 设置方法区最小空间 -XX:MaxPermSize 设置方法区最大空间

但是本质上两者并不等价,仅仅因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集管理器可以像管理Java堆一样来管理这部分内存,可以省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机是不存在永久代的概念的。

元数据区:在JDK1.8之后元数据区取代了永久代。但是方法区的概念还是存在的,也就是说JDK1.8中时使用元空间来实现方法区的,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于元数据空间并不在虚拟机中,而是使用本地内存。也就是说在JDK1.8之后不存在永久代,使用元数据区替代。

JDK8之后就没有永久代这一说法变成叫做元空间(meta space),而且将老年代与元空间剥离。元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而不会再出现像永久代的内存溢出错误了,也不会出现泄漏的数据移到交换区这样的事情。用户可以为元空间设置一个可用空间最大值,不设置默认根据类的元数据大小动态增加元空间的容量。对于一个 64 位的服务器端 JVM 来说,其默认的–XX:MetaspaceSize 值为 21MB。也就是说默认的元空间大小是21MB

JDK1.8 之后的方法区为何变化如此之大?
做这个改变呢也许主要是基于以下两点原因:
1、由于 永久代(PermGen)内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM错误。
2、移除 永久代(PermGen)可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。

还有需要注意一点的是虽然用元数据替代永久代并不是说完全完全,因此,你还必须监控你的内存消耗情况,因为一旦发生泄漏,会占用你的大量本地内存,有可能会出现更加糟糕情况

运行时常量池

运行时常量池是方法区的一部分,class信息除了类的版本,字段,方法,接口等描述信息,还有一部分是常量池,用于存储在编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。 jdk1.8后运行时常量池搬到了元数据区

直接内存

这部分内存其实不是jvm的运行时数据区的一部分,而是机器的物理内存,主要的一个应用场景是NIO中的DirectByteBuffer来操作这一部分,例如我们读取一些大文件的时候,可以用直接内存,这样可以避免直接使用jvm内存而造成的内存不足产生outOfMemory,但是这部分内存大小受制于物理机本身内存大小限制,如果机器本身内存不足,也会报outOfMemoryError