内容为之前学习笔记整理,本文示意图为自己学习所画。如果有问题欢迎指正。

1 内存布局总体结构

根据 JVM 规范,JVM 内存共分为虚拟机栈(Virtual Machine Stacks)、堆(Heap)、方法区(Method Area)、程序计数器(Program Counter Registers)、本地方法栈(Native Method Stacks)五个部分。

JVM Runtime Data Area

  • Java 8 之前在堆(Heap)中除了年轻代(YongGen)、老年代(OldGen)之外还存在一个永久代(PremGen)

    • 永久代存放:类的元数据、静态变量和常量

    • 方法区(Method Area)存在于永久代之中

    • 运行时常量池(Runtime Constant Pool)存在于方法区(Method Area)中

  • Java 8 及之后的版本,彻底移除了持久代(PermGen),而使用 元空间(Metaspace) 来进行替代

    • 永久代中的 类元信息(class metadata) 转移到了 本地内存(Native Memory) 而不是虚拟机

    • 永久代中的 字符串常量池(interned Strings)类静态变量(class static variables) 转移到了堆( Heap)中

    • 永久代参数(PermSize 与 MaxPermSize)失效,替换为元空间参数(MetaspaceSize 与 MaxMetaspaceSize)

  • Java 8 为什么要将永久代替换成Metaspace?

    • 字符串存在永久代中,容易出现性能问题和内存溢出。

    • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

    • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

    • Oracle 可能会将 HotSpot 与 JRockit 合二为一,JRockit 没有所谓的永久代。

  • 废除永久代的好处

    • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间

    • 将运行时常量池从 PermGen 分离出来,与类的元数据分开,提升类元数据的独立性。

    • 将元数据从 PermGen 剥离出来到 Metaspace,可以提升对元数据的管理同时提升GC效率。

2 程序计数器(Program Counter Register)

用于执行引擎在线程切换。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址

  • 如果线程正在执行的是一个Native方法,这个计数器值则为 Undefined

  • 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个

3 Java 虚拟机栈(Java Virtual MachineStacks)

Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期和线程相同。

Java虚拟机栈和线程同时创建,用于存储栈帧(Stack Frame)。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

栈帧

栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。每个栈帧,都包含四个区域:

  • 局部变量表(Local Variable Table):用于存放方法参数和方法内定义的局部变量。 包括8种基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

  • 操作数栈(Operand Stack):是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

  • 动态连接(Dynamic Linking):将符号引用转换成直接引用。

  • 返回地址(Return Address):方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

在Java虚拟机规范中,对这个区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;

  • 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

4 本地方法栈(Native Method Stack)

基本功能与虚拟机栈非常相似,服务的对象是 native 方法。本地方法栈也是线程私有的,它的生命周期与线程相同,每个线程都有一个。

在 HotSpot 虚拟机中直接就把本地方法栈和虚拟机栈合二为一。

会和 Java 虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

5 堆(Heap)与 元空间(Metaspace)

堆是什么:

  • 在虚拟机启动的时候创建。

  • 堆中的数据是线程所共享的,目的就是存放对象实例。

  • 堆是 垃圾收集器管理 的主要区域。

  • 堆是虚拟机所管理的内存中最大的一块,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代(JDK 1.7以及之前还存在永久代);新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间,默认占比 8:1:1。

  • 堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(可以通过 -Xms-Xmx 控制)。

  • 方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。

  • 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

另外:从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local AllocationBuffer,TLAB)。

JVM-HEAP-1.7-1.8

  • Yong Gen:1个Eden Space和2个Suvivor Space(from 和to)。主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。

  • Old Gen(Tenured Gen): 主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。

  • 默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3

  • 默认 -XX:SurvivorRatio=6,标识Eden 空间:From Survivor空间:To Survivor空间 = 8:1:1

对象分配内存的工作流程图

对象分配内存

GC相关概念

  • form survivor 又称 s0

  • to survivor 又称 s1

  • 部分收集:Partial GC

    • 新生代收集:Minor GC / Young GC

      • 年轻代空间不足触发, 这里年轻代指的是Eden满。Survivor满不会引发GC
    • 老年代收集:Major GC / Old GC

      • 老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC,如果Major GC , 内存仍然不足,则报错OOM
  • 混合收集:Mixed GC

    • G1垃圾回收器会混合回收, region 区域回收
  • 整堆收集:Full GC

    • 用System.gc() , 系统会执行Full GC ,不是立即执行

    • 老年代空间不足时触发

    • 方法区空间不足时触发

关于元空间(Metaspace)的单独说明

  • 在 JDK1.7 之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收

  • 从 JDK1.8 开始,HotSpot 虚拟机移除永久代,并把方法区移至元空间

  • 永久代与元空间的区别

    • 永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存

    • 在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中。

6 方法区(Method Area)与 运行时常量池(Runtime Constant Pool)

在 HotSpot 虚拟机上 GC 分代收集扩展至方法区,使用了永久代来实现方法区(JDK1.7及以前,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间)。

元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的。方法区是一个规范,只不过取代永久代的是元空间(Metaspace)。

  • 与堆类似,方法区是被各个线程共享的内存区域

  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

类加载器将Class文件加载到内存之后,将类的信息存储到方法区中

  • 类信息:类全名、直接父类的全名、修饰符、实现的接口列表

  • 类的属性信息:名称、类型、修饰符

  • 类的方法信息:返回类型、参数数量和类型、修饰符、字节码bytecodes、操作数栈、局部变量表及大小(abstract和native方法除外)、异常表

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  • 静态常量池:存放编译期间生成的各种字面量与符号引用。在字节码文件中即 .class 文件。

  • 运行时常量池:常量池表在运行时的表现形式。在方法区。

  • 编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。

7 直接内存(Direct Memory)

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分。

  • 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显

  • 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区 (Buffer)的 I/O 方法,它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。避免了 在Java堆和Native堆中来回复制数据。

直接内存的大小并不受到 JVM 堆大小的限制,甚至不受到 JVM 进程内存大小的限制。它只受限于本机总内存(RAM 及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。

DirectBuffer并没有真正向OS申请分配内存,其最终还是通过调用 Unsafe 的 allocateMemory() 来进行内存分配。不过 JVM 对 Direct Memory 可申请的大小也有限制,可用 -XX:MaxDirectMemorySize=1M 设置,这部分内存不受JVM垃圾回收管理。

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/linweiwang/p/15416676.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!