JVM 内存分布(资料整理)

参考:JVM内存模型看这个就够了
参考:JVM学习(三)JVM内存模型
参考:深入理解JVM-内存模型(jmm)和GC
参考:JDK1.8 JVM内存划分
参考:《深入理解JVM虚拟机(周志明)》

1.程序计数器

1.基本概念

      Register 的命名源于CPU的寄存器,CPU只有把数据装载到寄存器才能够运行
      寄存器存储指令相关的现场信息,由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?
      每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常。

2.定义

程序计数器是一块较小的内存空间,可看作当前线程正在执行的字节码的行号指示器

  • 执行Java方法
    计数器记录的就是当前线程正在执行的字节码指令的地址
  • 执行本地方法
    那么程序计数器值为undefined

3.特点

  • 一块较小的内存空间
  • 线程私有。每条线程都有一个独立的程序计数器。
  • 是唯一一个不会出现OOM的内存区域。
  • 生命周期随着线程的创建而创建,随着线程的结束而死亡。

4.作用

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

2.虚拟机栈

1.概念

      相对于基于寄存器的运行环境来说,JVM是基于栈结构的运行环境
      栈结构移植性更好,可控性更强,是描述Java方法执行的内存区域,它是线程私有的,栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程
      在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧,正在执行的方法称为当前方法,栈帧是方法运行的基本结构,在执行引擎运行时,所有指令都只能针对当前栈帧进行操作
在这里插入图片描述
虚拟机栈通过压/出栈的方式,对每个方法对应的活动栈帧进行运算处理。

  • 方法正常执行结束,肯定会跳转到另一个栈帧上
  • 如果出现异常,会进行异常回溯,返回地址通过异常处理表确定

2.局部变量表

      存放方法参数和局部变量,相对于类属性变量的准备阶段和初始化阶段来说,局部变量没有准备阶段,必须显式初始化
      如果是非静态方法,则在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是参数和局部变量
      字节码指令中的STORE指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内

3.操作栈

      操作栈是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和提取信息。JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的stack属性中

4.动态连接

      每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接

5.方法返回地址

方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  • 返回值压入,上层调用栈帧
  • 异常信息抛给能够处理的栈帧
  • PC计数器指向方法调用后的下一条指令

6.特点

      局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且表的大小在编译期就确定,在创建的时候只需分配事先规定好的大小即可。在方法运行过程中,表的大小不会改变
      Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡.

7.Java虚拟机栈会出现两种异常

  • StackOverFlowError
          若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求的栈深度大于虚拟机允许的最大深度时(但内存空间可能还有很多),就抛出此异常
  • OutOfMemoryError
          若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常

3.本地方法栈

      本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。本地方法栈也是```线程私有``的。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

      与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常。

4.方法区

1.定义

      JVM规范说:
      Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it.
      “方法区逻辑上是堆的一部分”。

      实际上方法区是JVM定义的一种规范,不同的虚拟机实现也不一样,hotspot对方法区的实现是永生代,而在jdk1.8之后,又换到了直接使用机器内存的元数据区,都可以看成是对方法区的不同实现。
      Java虚拟机规范中定义方法区是堆的一个逻辑部分,但是别名Non-Heap(非堆),以与Java堆区分。简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化,而装载后的结果就是由.class文件转变为方法区中的一段特定的数据结构。

2.数据组成

  • 类型信息
    全限定名、直接超类的全限定名、类的类型还是接口类型、访问修饰符、直接超接口的全限定名的有序列表
  • 字段信息
    字段名、字段类型、字段的修饰符
  • 方法信息
    方法名、方法返回类型、方法参数的数量和类型(按照顺序)、方法的修饰符
  • 其他信息
    除了常量以外的所有类(静态)变量、一个指向ClassLoader的指针、一个指向Class对象的指针、常量池(常量数据以及对其他类型的符号引用)

3.特点

  • 线程共享
    方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的.整个虚拟机中只有一个方法区.
  • 永久代(jdk1.7)
    方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为永久代.
  • 内存回收效率低
          Java虚拟机规范对方法区的要求比较宽松,可以不实现垃圾收集。方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载
    和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。当方法区内存空间无法满足内存分配需求时,将抛出OutOfMemoryError异常.

5. 直接内存(Direct Memory)

      直接内存不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但在JVM的实际运行过程中会频繁地使用这块区域.而且也会抛OOM.

      在JDK 1.4中加入了NIO(New Input/Output)类,引入了一种基于管道和缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆里的DirectByteBuffer对象作为这块内存的引用来操作堆外内存中的数据。

      这样能在一些场景中显著提升性能,因为避免了在Java堆和Native堆中来回复制数据.

6.java堆

1.概念

      对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
      根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。

2.新生代

      新生代= 1个Eden区+ 2个Survivor区

      绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young GC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。
      Survivor 区分为S0和S1两块内存空间,送到哪块空间呢?每次Young GC的时候,将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。
      如果YGC要移送的对象大于Survivor区容量上限,则直接移交给老年代

3.老年代

      用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:

  • 大对象
    可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
  • 大的数组对象且数组中无引用外部对象。

      老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
      如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

4.堆上分配过程

在这里插入图片描述

7.JDK1.7与1.8差异

1.元数据区

      在JDK1.8中元空间区取代了永久代,永久代原本主要存放Class和Meta的信息。而元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

2.引入原因

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

3.对比图示

JDK1.7
在这里插入图片描述

JDK1.8
在这里插入图片描述

8.命令配置

在这里插入图片描述

发布了82 篇原创文章 · 获赞 15 · 访问量 3128

猜你喜欢

转载自blog.csdn.net/qq_34326321/article/details/103525143