JVM基础系列:Java 虚拟机内存结构(运行时数据区)

  大家都知道运行JAVA程序,会先将源文件变成字节码文件,然后JVM会加载字节码文件,将其存入内存空间中,之后进行一系列的初始化动作,最后运行程序得出结果。

  那么字节码数据在JVM内存中是如何存放?JVM在为类实例或者成员变量分配内存是如何分配的?这些都需要我们了解以下JVM的内存结构。

  内存结构其实不是官方的说话,在《Java虚拟机规范》中用的时“运行时数据区”这个术语。但是这个名称不太形象,所以习惯性的用JVM内存结构这个说法。

  根据《规范》中的说话,JVM的内存结构可以分为公有和私有两部分。公有指的是所有线程共享的部分,指的是Java堆,方法区,常量池。私有指的是每个线程的私有数据,包括:PC寄存器,Java虚拟机栈,本地方法栈。

  公有部分:java堆、方法区、常量池

  在Java虚拟机中,线程共享部分包括Java堆,方法区以及常量池

  Java堆指的是JVM划分出来的一块区域,这块区域专门用户Java实例化对象的内存分配,几乎所有实例都会在这里进行内存分配。之所以说几乎,有些时候小对象会直接在线程栈上进行分配,这种称为“栈上分配”。这里就不深入介绍了。

  方法区指的是存储Java字节码数据的一块区域,他存储了每个类的结构信息,例如运行时常量池、字段和方法数据、构造方法等。这里可以看出来常量池也是存放在方法区,只是在《规范》里把这两类放在同一等级上。了解即可。

  方法区在不同版本的JVM是不一样的。例如在1.7版本中,方法区称为永久代(Permanent Space),而在JDK1.8中则被称之为(元空间)MetaSpace。

  各个部分大致作用了解后,现在深入了解下Java堆。

  

  如上图所示,Java会根据对象存活时间的不同,把Java堆分为年轻代、老年代两个区域,年轻代还被进一步分为Eden区、From Survivor 0区、To Survivor 1区。

  当需要对象需要分配时,一个对象永远优先被分配在年轻代的Eden区,等到Eden区域内存不够时,JVM找机会启动垃圾回收。此时Eden区中没有被引用的对象的内存会被回收,而一些存活时间较长的对象就会进入到老年代。在JVM中有一个名为-XX:MaxTenuringThreshold的参数专门来设置晋升到老年代所需要经历的GC次数,如果一个对象经历设置的GC次数,下一次GC就会将对象进入老年代。

  为什么Java堆要用这种区域划分的方案呢?

  根据我们实际经验,虚拟机中的对象必定会有存活时间长的对象,也有存活时间短的对象,而时间短的对象占大部分,这个规律是符合正态分布的,假设我们将他们混合在一起存储,那么存活短的对象有很多,那么势必会导致频繁的垃圾回收。而垃圾回收时,不得不对所有内存进行扫描,但是其中有些对象,他存活时间很长,对他们扫描简直就是浪费时间。因此为了提高垃圾回收效率,分区存储、分代管理还是有必要。

  还有一个值得我们思考的问题,为是什么默认JVM配置,Eden: from : to = 8:1:1呢?

  其实这也是经验之谈,IBM公司根据大量的项目统计得出的结果,根据IBM公司对对象存活时间的统计,发现80%的对象存活的时间很短。于是他们将Eden区设置为年轻代的80%,这样可以减少空间的浪费,提高内存空间的利用率。

  私有部分:PC寄存器、Java虚拟机栈、本地方法栈

  Java堆以及方法区的数据是共享的,但是有一些数据则是线程私用的。线程私有部分可以分为:PC寄存器、Java虚拟机栈、本地方法栈三大部分。

  PC寄存器,也可以叫做程序计数器,指的是保存当前正在执行的方法。如果这个方法不是native方法,那PC保存JVM正在执行的字节码指令地址。如果是native方法,那么PC保存的值是undefined。任意时刻,Java虚拟机里的一个线程只会执行一个方法的代码,而这个被线程执行的方法为该线程的当前方法,其地址被存在程序计数器中。

  Java虚拟机栈,这个栈与线程同时创建,用来存储栈帧,即存储局部变量与一些过程结果的地方。栈帧存储的数据包括:局部变量表,操作数栈。

  当虚拟机使用其他语言(例如C语言)来实现指令,也会使用本地方法栈。

  总结

  JVM的内存结构,即运行时数据区是学习虚拟机所必须掌握的地方,其中以Java堆的内存模型最为重要,因为很多时候问都是出现在Java堆中。因此Java堆的划分以及常用参数调整还是比较重要的。

posted @ 2022-08-31 08:47  梅晓煜  阅读(97)  评论(0)    收藏  举报