前言

很高兴遇见你~ 欢迎阅读我的文章。

在上一篇文章JVM基础(一):认识虚拟机中,介绍了什么是虚拟机,以及字节码class文件。这一篇文章主要讲关于JVM的运行时数据区结构。

我们知道,c/c++在内存上划分为了栈和堆区,栈区存放函数的局部变量等,堆区为用户自主开辟的空间。当开发者需要新建一个对象时,首先需要在堆区中开辟一个空间,再初始化,使用。最后需要自己手动去释放该内存区域。而JVM把这些工作都做好了,当我们需要新建一个对象的时候,只需要一个关键字new即可创建一个对象,当我们不再使用这个对象的时候,垃圾回收机制会帮我们自动回收该内存。两种方式各有优势,c/c++对内存有至高无上的权利,同时也意味着自己有更重的责任去管理对象的生命周期;JVM自动管理内存,向开发者屏蔽了直接的内存操作,但是如果对JVM的运行时数据区认识不够,就很容易发生内存泄露、内存溢出等问题。这也是我们需要学习JVM运行时数据区的原因之一。

JVM运行时数据区结构和c/c++有点相似,同样分配了栈区与堆区。具体的结构如下图:

我们的class文件通过类加载系统,被加载到虚拟机中,在虚拟机进行处理。执行引擎负责执行程序,调用方法并执行。本科库接口和本地方法库则为平台相关的一些列方法,例如android的拍照、通知等等。我们的重点在:运行时数据区。其中方法区和堆是进程共享,而虚拟机栈、本地方法栈、程序计数器是线程共享。下面对各个内存区域进行讲解。

堆区

堆区主要存放对象实例,是垃圾回收的主要目标。堆区在物理上可以不连续,但在逻辑上一定是连续的。堆区为线程共享。

堆区是我们需要关注的一个重点。我们创建的一切对象以及数组(其实数组也是一种对象),都分配在堆区,同时Class对象(注意,这个class对象指的是类本身,而不是由类创建出来的对象)也是存储在堆区。堆区是线程共享,所有线程的创建的对象都存放在堆区中。堆区是垃圾回收的主要目标,也是内存泄露以及内存溢出异常的高发地。堆区并不是一整块内存,而是会进行划分实现不同的特性,如分代设计、线程缓冲区等,用以解决垃圾回收、线程安全等问题,后面系列文章会详细讲解。

堆区大小可以是可拓展的,也可以是固定的。目前主流的虚拟机均设计为可拓展型。当堆申请内存失败,无法拓展时,会抛出OutOfMemoryError异常。

方法区

方法区主要存放类信息、常量、静态变量、即时编译后的代码缓存,以及类、方法、字段的符号引用等。方法区为线程共享。

我们的class文件中的类被加载后存放在方法区中。常量,也就是final类型的变量且在编译阶段就能确定数值,以及静态变量均放在这个地方。字节码被解释器解释后的机器码,也会缓存在这个区域中。同时,未加载的类、方法调用位置等等,这些需要有一地址去调用。如类的加载,首先虚拟机要知道有这样一个类和这个类的位置,然后才能判断类是否存在和去哪里加载这个类,这个就是类的符号引用,其他类似。

方法区存放的数据的生命周期看似都是伴随整个进程的,但事实上也是需要进行垃圾回收,只是回收的效果很难令人满意,例如类的卸载对时机的把握非常苛刻。HotSpot在初期,直接把方法区和堆区合在一起,把方法区当为永生代,而堆区分为新生代和老年代,直接对方法区和堆区统一进行垃圾回收,省去了专门给方法区写一套垃圾回收算法。这种设计会很容易导致内存溢出问题,jdk8之后HotSpot就放弃了永生代,改用本地内存实现的元空间。

当无法满足新的内存分配需求的时候,会抛出OutMemoryError异常。

运行时常量池

常量池属于方法区的一部分,主要存储数据常量以及符号引用。

存储常量这个一般没有什么疑问,就是final修饰的常量。符号引用有:包名、类的全限定名称、方法名称描述符等等。JVM与c/c++不同,他并不是在编译阶段就确定好代码的布局,而是需要用到某个类才去加载使用,也就是动态加载。那么整个程序所有的类的符号引用就都存在常量池中,当需要加载一个类的时候,就需要从常量池中拿到该类的符号引用进行类加载,如果找不到,则抛出ClassNotFoundError异常。

常量池并非只有在编译阶段才能创建数据,在运行阶段也可以在常量池中添加数据,如String的intern方法。所以当常量池无法拓展内存时,会抛出OutOfMemoryError异常。

程序计数器

程序计数器是当前线程所执行的字节码的行号指示器,标记程序下一条代码运行位置。程序计数器为线程私有。

解释器通过程序计数器来指引代码的执行。分支、循环、异常、跳转、线程恢复等都需要程序计数器来控制。Java的多线程实现方式是线程轮换来分配处理器的执行时间。当线程被挂起时,程序计数器会记录当前线程代码的执行位置,当线程恢复时可以恢复到正确的代码执行位置。

正常情况下程序计数器指示的是字节码指令的地址,如果当前运行的是本地方法,那么程序计数器的值为0。这一块内存不会抛出OOM异常,毕竟就一个地址存储,不需要拓展多少的内存。

虚拟机栈&本地方法栈

虚拟机栈是JVM方法执行的的线程内存模型:每个方法执行的时候,会往虚拟机栈中压入一个栈帧(用于存储局部变量、操作数栈、动态链接、方法出口、对象引用等信息)。

本地方法栈:和虚拟机栈类似,但执行的方法类型为本地方法。

虚拟机栈和c/c++的栈有一些相似。在jvm中每个方法被执行时都会压入一个栈帧,当方法结束时会把栈帧出栈。栈帧中包含了方法中各种数据类型,但要注意的是栈帧中的保存的是对象引用而不是对象本身,具体的对象是保存在堆中。栈帧中保存数据的最小单位是变量槽。变量槽根据不同虚拟机会有不同,如32位、64位等。如果使用32位变量槽,那么一个int数据就占据一个变量槽,而一个double数据则要占用两个变量槽。

本地方法栈Java虚拟机规范中没有明确规定实现方式。HotSpot直接把本地方法栈和虚拟机栈合二为一,统一管理。

注意,方法栈会抛出两种异常:StackOverFlowError和OutOfMemoryError。前者当栈的深度达到限制会抛出,后者当无法申请更多的空间来拓展栈大小时会抛出。

总结

文章讲解了JVM的整体内存结构,解析了运行时数据区的各种区域的作用特点等。

了解运行时数据区,是为了清楚JVM对于内存区域的划分,知道哪些地方会发生内存泄露以及内存溢出,让我们可以写出更加健壮的代码。同时也是继续了解JVM其他知识的基础,如堆区的GC机制以及对象创建流程、类加载子系统、线程内存模型等等,都是建立在这个JVM整体模型的基础上。

全文到此,原创不易,觉得有帮助可以点赞收藏评论转发关注。
笔者才疏学浅,欢迎评论区交流指正。
如需转载请私信交流。

另外欢迎光临笔者的个人博客:传送门