当前位置:   article > 正文

Java虚拟机之内存结构_java虚拟机的内存结构

java虚拟机的内存结构

JVM内存结构(运行时数据区)

JVM 内存结构,也就是运行时数据区,它主要包含5个部分:

  • 线程私有:程序计数器、栈、本地方法栈
  • 线程共享:堆、方法区

程序计数器 (线程私有)

Program Counter Register

用来存储执行下一条要执行的指令的地址,也就是将要执行的指令代码, 由执行引擎读取下一条指令。

它是一块很小的内存空间,小到可以忽略不计。

任何时间,一个线程只有一个方法在执行,叫做当前方法。程序计数器会存储当前线程执行的方法的JVM指令地址;或者如果执行的是native方法,则是未指定值(undefined)

使用PC寄存器存储字节码指令地址有什么用?

CPU会不停地切换各个线程,再切换回来的时候,需要通过它来确定此线程接下来执行哪条指令

本地方法栈 (线程私有)

本地方法栈用于管理本地方法(native)的调用,本地方法时C语言实现的,本地方法中用到的变量存放在本地方法栈对于的内存空间中。

一般情况下,不需要关注。

栈 (线程私有)

  • 先进后出 FILO
  • Java 所有的指令都是根据【栈】来设计的
  • 栈是运行时的单位,而堆是存储单位
  • 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧。方法的调用和对出,对应着栈帧的入栈出栈
  • 操作类型:入栈和出栈
  • 作用:主管 Java 程序的运行,它保存方法的局部变量(8中基本类型,以及对象的引用地址)、部分结果,并参与方法的调用和返回。
  • 特点:速度快,仅次于程序计数器(PC寄存器)
  • 对于栈来说,不存在垃圾回收问题
  • 可能存在OOM,但是没有GC

栈帧

  • 每个线程有自己的栈,栈中的数据是以【栈帧】的格式存在
  • 每个方法对应一个栈帧,一对一关系
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
  • 每一个时间点,只有一个栈帧是正在执行的,叫做【当前栈帧】
  • 执行引擎运行的所有字节码指令,只针对当前栈帧操作
  • 如果方法中调用了其他方法,则新的栈帧会被创建,放在栈的顶端,成为当前栈帧
  • 如果方法调用过了其他方法,方法返回时,当前栈帧会传回执行结果给前一个栈帧,虚拟机会丢弃当前栈帧,使前一个栈帧成为当前栈帧
  • Java方法有两种返回方式:1)正常执行结束;2)遇到未处理的异常。都会导致栈帧被弹出

栈帧的内部结构

局部变量表

  • 定义为数字数组,主要存储【方法参数】和方法内部定义的【局部变量】
  • 数据类型包含:8种基本数据类型,对象引用,以及 returnAddress 类型
  • 局部变量表是线程私有的,不会有线程安全问题
  • 所需要的容量大小是在【编译期】就确定好的
  • 方法嵌套的次数由栈的大小决定,当然,也跟栈帧中局部变量表的大小有关
  • 参数和局部变量越多,栈帧就越大,方法能够调用的次数也就越少
  • 方法调用结束后,随着栈帧的销毁,局部变量表也会销毁
  • 在栈帧中,与性能调优最为密切的就是局部变量表
  • 在方法执行时,虚拟机使用局部变量表完成方法的传递
  • 局部变量表中的变量,是垃圾回收中的根节点(GC Roots),只要是局部变量表中直接或间接引用的对象,都不会被回收

slot(槽)

  • 局部变量表的基本存储单元是 slot
  • 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress),64位的占两个slot(long、fload、double)
  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余参数按照参数表顺序排列
  • 静态方法不会有【this对象】
  • slot 是【可回收】的,如果已经出了变量的作用域,它所占的槽位会回收,后面新创建变量时,可以使用回收的槽位

操作数栈

  • 操作数栈【先进后出】,通过数组实现
  • 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
  • 用于保存【计算过程】的【中间结果】,同时作为计算过程中变量的临时存储空间
  • 操作数栈是JVM执行引擎的一个工作区,当栈帧被创建时,操作数栈是空的
  • 操作数栈有一个明确的栈深度用于存储数值,最大深度在编译期就定好了,保存在方法的code属性中,为max_stack值
  • 在操作数栈中,32bit 类型占用一个栈单位深度,64bit 占用两个
  • 如果调用的方法有返回值,则其返回的结果会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的指令
  • 操作数栈中的元素数据类型必须与字节码指令的序列严格匹配(32bit占1个单位,64bit占2个单位)
  • 我们说Java虚拟机的【解析引擎是基于栈的执行引擎】,这里的栈指的就是操作数栈

栈顶缓存技术:

基于栈式架构的虚拟机所使用的【零地址指令】更加紧凑,但是每个操作都有入栈和出栈,就意味着需要更多的指令分派次数和内存读写次数,会影响执行速度。

为了解决上面的问题,HotSpot JVM 设计了栈顶缓存技术,将栈顶元素全部缓存到物理CPU的寄存器(寄存器特点:指令更少,执行速度快)中,以此降低对内存的读写次数。

动态链接

每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用(上图中橘黄色和绿色部分)

在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为【符号引用】保存在class文件常量池中。比如描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。那么动态链接的作用就是:将这些符号引用转换为调用方法的直接引用

常量池作用:为了提供一些符号和常量,便于指令的识别

方法出口

  • 方法正常退出时,调用者的pc计数器的值作为返回地址(程序需要知道这个方法调用完后,接下来从哪个地方开始继续运行),即调用该方法的指令的下一条指令的地址(返回指令中包含return)
  • 方法异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息(没有对异常进行处理)
  • 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用着方法继续执行下去
  • 正常结束和异常结束的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

堆 (线程共享)

Java 中的堆是 JVM 所管理的最大的一块内存空间,也是最重要的内存空间之一。主要用于存放各种类的实例对象。 

  • 《Java虚拟机规范》规定,堆空间在物理上可以处于不连续的空间上,但是在逻辑上它应该被看成是连续的
  • 《Java虚拟机规范》对Java堆的描述是:所有对象实例以及数组都应当在运行时分配在堆上。”几乎“所有对象实例都在堆上分配,一切从实际出发。也有可能在栈上分配(对象逃逸)
  • 堆,是GC执行垃圾回收的重点区域
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在【垃圾收集】时才会被移除

java 堆分为新生代(Young)和老年代(old)。

新生代

生命周期较短,创建和消亡都非常迅速

新生代又分为三个区域:Eden、From Survivor ( s0 )、To Survivor ( s1 )。

  • 默认新生代占堆内存空间的 1/3,老年代占 2/3;
  • 几乎所有对象都是在 Eden 区域被 new 出来;
  • Eden 区满的时候会触发 Minor GC,同时也会对 s0、s1 进行回收;
  • s0 和 s1 的其中一个存放每次 GC 后,新生代中剩余的不需要挪到老年代的对象,另一个空着(比如一开始 s1空着,GC 后,将 Eden 和 s0 中剩余的对象挪到 s1 中,再下一次则挪到 s0中)
  • 由于 Eden 中对象存活时间都非常短,绝大部分都在 GC 后留不下来,所以 s0 和 s1 不需要很大,可以远远比 Eden 小。JVM 默认 Eden:From:To = 8:1:1,但是 jvm 也会自动调整比例分配,所以不一定是8:1:1。

老年代

生命周期非常长

  • 对象在 Survivor 中每熬过一次 Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中
  • 大对象直接进入老年代。通过 PretenureSizeThreshold 参数设置对象多大可以算作大对象
  • Minor gc 后存活的对象 Survivor 区放不下的话,也会进入老年代
  • 老年代空间不足时,会触发 Full GC。当然,其他情况也可能导致 Full GC,比如老年代空间担保机制。

堆空间相关参数:

  • -XX:+PrintFlagsInitial:查看所有的参数的默认初始值
  • -XX:+PrintFlagsFinal:查看所有参数的最终值
  • -Xms:初始堆空间内存(默认物理内存的1/64)
  • -Xmx:最大对空间内存(默认物理内存的1/4)
  • -Xmn:设置新生代的大小(初始值及最大值)
  • -XX:NewRatio:新生代和老年代的比例。-XX:NewRatio=2,表示1:2
  • -XX:SurvivorRatio:Eden和s0 s1的比例。默认 8:1:1。实际使用的时候是6:1:1。
  • -XX:MaxTenuringThreshold:对象经历多少次垃圾回收后放到老年区,默认15次(对象有可能直接就被放在老年代,也可能不到阈值就被放到老年代)
  • -XX:HandlePromotionFailure:是否设置空间分配担保

对象逃逸分析:

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识,但是有一种特殊情况。那就是如果经过逃逸分析,后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无须再堆上分配了,也不用进行垃圾回收了。这也是最常见的堆外存储技术。

解释对象逃逸分析前,先看看JVM的三种运行模式:

  1. 解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码
  2. 编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式),先将所有JVM字节码一次编译为机器码,然后一次性执行所有机器码
  3. 混合模式(Mixed Mode):依然使用解释模式执行代码,但是对于一些 "热点" 代码采用编译模式执行,JVM一般采用混合模式执行代码

混合模式是JVM默认采用的执行代码方式,一开始还是解释执行,但是对于少部分“热点”代码会采用编译模式执行,这些热点代码对应的机器码会被缓存起来,下次再执行无需再编译,这就是我们常见的JIT(Just In Time Compiler)即时编译技术。 在即时编译过程中JVM可能会对我们的代码做一些优化,比如对象逃逸分析等。

public void test() { 
      User user = new User(); 
      user.setId(1);
      user.setName("zhuge");
      // TODO 保存到数据库 
}

对象逃逸分析:上面test方法中的user对象,我们可以确定当方法结束这个对象就可以认为是无效对象了。对于这样的对象我们其实可以将其分配的栈内存里,让其在方法结束时跟随栈内存一起被回收掉,不过这个也只是可能在栈上分配,而不是绝对!

如果一个对象被发现只能从一个线程访问,那么对这个对象的操作可以不考虑同步。

JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,jdk7 之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)

方法区 (线程共享)

方法区是逻辑上的东西,是JVM的规范,所有虚拟机必须遵守。

方法区是 JVM 所有线程共享的、用于存储类的信息、常量池、方法数据、方法代码等。

  • 永久代:即 PermGen space,是 jdk8 之前,是 HotSpot 虚拟机基于 JVM 规范,对方法区的一个具体实现。而且只有 HotSpot 虚拟机有永久代,其他虚拟机实现并没有永久代。
  • 元空间:即 Metaspace。jdk7开始,移除永久代的工作就已经开始了,直到 jdk8,永久代被彻底移除,方法区的实现变成了元空间。
jdk版本jdk6jdk7jdk8
方法区实现

永久代 

永久代元空间
运行时常量池位置永久代永久代元空间

字符串常量池位置

StringTable

永久代堆 (Heap)堆 (Heap)
静态变量位置永久代堆 (Heap)堆 (Heap)
类的类型,方法,域信息永久代永久代元空间

jdk8 方法区:

元空间与永久代之间最大的区别

元空间并不在虚拟机中,而是使用本地内存

  • 方法区的大小由 PermSize 和 MaxPermSize 参数指定。
  • 元空间默认大小是没有限制的,不过也可以通过 MetaspaceSize 和 MaxMetaspaceSize 参数指定

为什么要移除永久代?

方法区主要存储类的相关信息,类及方法的信息等比较难确定其大小,而且有的业务功能点比较多,在运行过程中,要不断加载很多类,可能就OOM了。而元空间并不在虚拟机内存中,而是使用本地内存,因此默认情况下,元空间大小仅收本地内存限制。

JVM内存参数设置

以 jdk8 为例:

 java -jar 启动时指定 jvm 参数:

java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss1024K ‐XX:MetaspaceSize=512M ‐XX:MaxMetaspaceSize=512M ‐jar xxxxxx.jar

tomcat 中指定 jvm 参数:catalina.sh 的 JAVA_OPTS 中指定。

-Xss 设置越小,一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。

当然,还有很多其他参数,需要根据应用的实际情况考虑来考虑如何设置。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/1002371
推荐阅读
相关标签
  

闽ICP备14008679号