终于明白了:JVM内存模型

运行时数据区

如下图所示,Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。终于明白了:JVM内存模型
注:图片来源于网络

线程私有

程序计数器

  程序计数器是一块较小的内存空间,用作程序执行字节码的行号指示。为了线程的切换能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程间互不影响,所以该内存区域是线程私有的。
  此内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域。

Java虚拟机栈

  虚拟机栈也是线程私有的,它描述的是方法执行的内存模型,即每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、方法出口等信息。方法的执行开始到结束对应着一个栈帧在虚拟机栈中的入栈到出栈。所以栈帧的生命周期与线程相同。
  根据Java虚拟机规范中的规定,该区域规定了两种异常:如果线程请求的栈深度大于虚拟机所允许的栈深度(即申请的栈内存大于了设置的线程栈大小),将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时申请不到足够的内存空间(即没有足够的内存空间分配),就会抛出OOM异常。

本地方法栈

  本地方法栈与虚拟机栈类似,它们的区别无非是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。
  根据Java虚拟机规范中的规定,该区域也规定了两种OOM异常,与虚拟机栈相同。

线程共享

Java堆

  大多数情况下,Java堆是虚拟机管理的内存中最大的一块内存,几乎所有的对象实例都在此区域分配内存(jdk1.7后根据逃逸分析技术可栈上分配)。
  Java堆也是垃圾回收器管理的主要区域。所以Java堆可以分为新生代和老年代;更细分的话可以分为Eden区和两个Survivor区(分别互为From Survivor和To Survivor),这几个区域都是存储对象实例的,这样划分只是为了更好地进行回收和分配内存。
  根据Java虚拟机规范中的规定,如果堆中没有足够内存完成实例的分配,并且无法再扩展时,则抛出OOM异常。

方法区

  方法区与Java堆一样,是各个线程共享的内存区域,用于存储以加载的内信息、常量、静态变量等数据。方法区也属于堆的一个逻辑部分,它有个别名叫非堆(Non-Heap),目的就是与堆区分开。
  方法区是永久代的实现,对于其他虚拟机来说是不存在永久代的概念的,所以这种实现方式并不是很好。随着我们项目的扩展、需求的扩展,永久代的数据将可能越来越大,而永久代的大小是不便维护的,就很容易发生内存溢出的问题。所以Java 8以后由元空间(Metaspace)替换了永久代。
  相对堆而言,GC的行为在该区域是比较少出现的,但并非数据进入了方法区就会永久存在了,这个区域的内存回收主要针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果并不是很好。
  根据Java虚拟机规范中的规定,当方法区不能满足内存分配的需求时,将抛出OOM异常。

运行时常量池

  运行时常量池是方法区的一部分。Class文件中有类的版本、字段、方法、接口、常量池等信息的描述,其中常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池。当然,被翻译出来的直接引用也存在该区域中。
  Java中并没有要求常量一定要在编译期才能产生,也就是并不一定是预先置于Class文件中的常量池中的内容才能进入该区域,运行期间也可能将新的常量放入池中,例如String类中的intern()方法。

直接内存

  直接内存并不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OOM的出现。
  直接内存的分配不会受到Java堆大小的限制,但既然时内存,就会受到本机总内存大小以及处理其寻址空间的限制。在配置虚拟机参数时,如果忽略直接内存,使得各个内存区域总和大于物理内存,就会导致动态扩展时发生OOM异常。


  文章中提到的GC相关的内容会在后续的理论篇中介绍,OOM、String的intern()方法会在实战篇进行介绍。

个人博客地址:https://dummyprodigal.github.io/blog/2019/11/30/JVM%E5%9F%BA%E7%A1%80/JVM%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/