精 灵 王


  • 首页

  • 文章归档

  • 所有分类

  • 关于我

  • 搜索
设计模式之美 分布式 Redis 并发编程 个人成长 周志明的软件架构课 架构 单元测试 LeetCode 工具 位运算 读书笔记 操作系统 MySQL 异步编程 技术方案设计 集合 设计模式 三亚 游玩 转载 Linux 观察者模式 事件 Spring SpringCloud 实战 实战,SpringCloud 源码分析 线程池 同步 锁 线程 线程模型 动态代理 字节码 类加载 垃圾收集器 垃圾回收算法 对象创建 虚拟机内存 内存结构 Java

Java 虚拟机内存的各个区域

发表于 2020-10-25 | 分类于 Java虚拟机 | 0 | 阅读次数 234

JAVA 虚拟机管理的内存会包括以下几个内存区域:

  1. 程序计数器
  2. JAVA虚拟机栈
  3. 本地方法栈
  4. JAVA 堆
  5. 方法区
  6. 运行时常量池
  7. 直接内存

内存结构图:

程序计数器(Program Counter Register)

程序计数寄存器(Program Counter Register),Register 的命名源于 CPU 的寄存器,寄存器存储指令相关的线程信息,CPU 只有把数据装载到寄存器才能够运行。

这里,并非是广义上所指的物理寄存器,叫程序计数器(或PC计数器或指令计数器)会更加贴切,并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。

程序计数器是一块比较小的内存空间,可以被看做是当前线程执行的字节码的行号指示器。

为了线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的计数器,每条线程之间的计数器需要互不影响,独立存储,所以我们称程序计数器是**"线程私有"**的内存区域。

字节码解释器是工作时是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来执行

存储

主要用来存储指向下一条指令的地址,即将要执行的指令代码。

如果线程正在执行的是一个JAVA方法, 那么程序计数器记录的就是正在执行的字节码指令地址。

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

特点

  • 线程私有
  • 唯一不会OOM异常的区域

JAVA虚拟机栈(Java Virtual Machine Stacks)

Java 虚拟机栈(Java Virtual Machine Stacks),早期也叫 Java 栈。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。

存储

JAVA虚拟机栈描述的是JAVA方法执行的内存模型:主要用于存储局部变量表、操作数栈、动态连接、方法出口信息等。

局部变量表存放了编译期可知的8种基本数量类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型, 不等同于对象本身)和returnAddress(指向了一条字节码指令的地址)类型。

局部变量表所需内存空间在编译器就完成了分配。

方法运行期不会改变局部变量表的大小。

64位长度的long和 duoble会占用2个局部变量空间(Slot),其余的数据类型都只占用一个。

特点

  • 线程私有,生命周期与线程一致。

  • 访问速度仅次于程序计数器

  • JAVA虚拟机栈可能会出现两种异常:

    1. StackOverFlowError异常

      线程请求的栈深度大于虚拟机允许的深度,会抛出这个异常

    2. OutOfMemoryError异常

      虚拟机栈动态扩展时无法申请到足够的内存时,会抛出这个异常

    可以通过参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

栈运行原理

  • JVM 直接对 Java 栈的操作只有两个,对栈帧的压栈和出栈,遵循“先进后出/后进先出”原则
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。
    即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧
  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
  • Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常,不管用哪种方式,都会导致栈帧被弹出
  • IDEA 在 debug 时候,可以在 debug 窗口看到 Frames 中各种方法的压栈和出栈情况

1

局部变量表

局部变量表也被称为局部变量数组或者本地变量表。

主要用于存储方法参数和定义在方法体内的局部变量,包括编译器可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址,已被异常表取代)

槽 Slot

  • 局部变量表最基本的存储单元是 Slot(变量槽)
  • 32 位以内的类型只占用一个 Slot(包括returnAddress类型),64 位的类型(long和double)占用两个连续的 Slot
  • byte、short、char 在存储前被转换为int,boolean也被转换为int,0 表示 false,非 0 表示 true
  • JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,索引值的范围从 0 开始到局部变量表最大的 Slot 数量
  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 Slot 上
  • 如果需要访问局部变量表中一个 64bit 的局部变量值时,只需要使用前一个索引即可。(比如:访问 long 或 double 类型变量,不允许采用任何方式单独访问其中的某一个 Slot)
  • 如果当前帧是由构造方法或实例方法创建的,那么该对象引用 this 将会存放在 index 为 0 的 Slot 处,其余的参数按照参数表顺序继续排列(这里就引出一个问题:静态方法中为什么不可以引用 this,就是因为this 变量不存在于当前方法的局部变量表中)
  • 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

局部变量表特点:

  • 是线程的私有数据,因此不存在数据安全问题
  • 局部变量表所需要的容量大小是编译期确定下来的
  • 表中的变量只在当前方法调用中有效
  • 随着方法栈帧的销毁,局部变量表也会随之销毁

操作数栈

每个独立的栈帧中除了包含局部变量表之外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称为表达式栈(Expression Stack)。

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

操作数栈,在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈(push)、出栈(pop)。

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如,执行复制、交换、求和等操作。

栈顶缓存

基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。由于操作数是存储在内存中的,因此频繁的执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM 设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

动态链接(指向运行时常量池的方法引用)

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。

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

2

Class 文件的编译过程中不包括传统编译器中的连接步骤,一切方法调用在 Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。

在 JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关

  • 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
  • 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接

对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

  • 早期绑定:早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  • 晚期绑定:如果被调用的方法在编译器无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式就被称为晚期绑定。

虚方法和非虚方法

  • 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法,比如静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法
  • 其他方法称为虚方法

方法返回地址(return address)

用来存放调用该方法的 PC 寄存器的值。

一个方法的结束,有两种方式

  • 正常执行完成
  • 出现未处理的异常,非正常退出

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。

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

当一个方法开始执行后,只有两种方式可以退出这个方法:

  1. 执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,简称正常完成出口

    一个方法的正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定

    在字节码指令中,返回指令包含 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 以及 areturn,另外还有一个 return 指令供声明为 void 的方法、实例初始化方法、类和接口的初始化方法使用。

  2. 在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口

    方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

本地方法栈(Natice Method Stack)

本地方法栈与虚拟机栈的作用非常的相似,区别在于虚拟机栈为虚拟机执行JAVA 方法(字节码)服务, 而本地方法栈则为虚拟机使用到的Native 方法服务。

本地方法栈是"线程私有"的内存区域

与虚拟机栈一样,本地方法栈也会抛出StackOverFlowError 和 OutOfMemoryError 异常。

在 Hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一。

栈是运行时的单位,而堆是存储的单位。

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。

JAVA 堆(Java Heap)

存储

JAVA 堆内存区域的唯一目的就是**存放JAVA 对象实例,**几乎所有的对象实例以及数组都会在这里分配内存。

💡随着JIT编译器的发展与逃逸分析技术,栈上分配、标量替换等优化技术让所有对象都在堆上分配不再那么绝对了

从内存回收的角度看,AVA 堆可以分为新生代和老年代;

再细致一点有Eden 区、From Survivor区、To Survivor 区等,堆分代结构图:

h

Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError 异常。

JAVA堆是"线程共享"的内存区域。

年轻代

年轻代是所有新对象创建的地方。

当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。

年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1

  • 大多数新创建的对象都位于 Eden 内存空间中
  • 当 Eden 空间被对象填充时,执行**Minor GC**,并将所有幸存者对象移动到一个幸存者空间中
  • Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的。
  • 经过多次 GC(默认15 次) 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代。

老年代

旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。

大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝。

元空间

不管是 JDK8 之前的永久代,还是 JDK8 及以后的元空间,都可以看作是 Java 虚拟机规范中方法区的实现。

虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。

堆大小设置

可以通过 -Xmx 和 -Xms 来设定堆内存大小

  • Xms 用来表示堆的起始内存,等价于 XX:InitialHeapSize
  • Xmx 用来表示堆的最大内存,等价于 XX:MaxHeapSize

通常会将 -Xmx 和 -Xms 两个参数配置为相同的值,其目的是为了能够在垃圾回收机制清理完堆区后不再需要重新分隔计算堆的大小,从而提高性能。

默认情况下,初始堆内存大小为:电脑内存大小/64,最大堆内存大小为:电脑内存大小/4;

默认情况下新生代和老年代的比例是 1:2,可以通过 –XX:NewRatio 来配置;新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1,可以通过 -XX:SurvivorRatio 来配置。

若在 JDK 7 中开启了 -XX:+UseAdaptiveSizePolicy,JVM 会动态调整 JVM 堆中各个区域的大小以及进入老年代的年龄。

此时 –XX:NewRatio 和 -XX:SurvivorRatio 将会失效,而 JDK 8 是默认开启-XX:+UseAdaptiveSizePolicy。在 JDK 8中,不要随意关闭-XX:+UseAdaptiveSizePolicy,除非对堆内存的划分有明确的规划。每次 GC 后都会重新计算 Eden、From Survivor、To Survivor 的大小,计算依据是GC过程中统计的GC时间、吞吐量、内存占用量。

内存分配过程

为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。

  1. new 的对象先放在伊甸园区(Eden),此区有大小限制
  2. 当伊甸园(Eden)的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
  3. 然后将伊甸园中的剩余对象移动到幸存者From区
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 From区,如果没有回收,就会放到幸存者 To区
  5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
  6. 什么时候才会去养老区(Old)呢? 默认是 15 次回收标记
  7. 在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理
  8. 若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常

TLAB (Thread Local Allocation Buffer)

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。

从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略 。OpenJDK 衍生出来的 JVM 大都提供了 TLAB 设计

方法区(Method Area)

存储

方法区主要用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

它还有一个别名Non-Heap(非堆),目的就是与JAVA 堆进行区分开来。

运行时常量池(Runtime Constant Pool)是方法区的一部分。

方法区内部结构

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

类型信息:

对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区中存储以下类型信息

  • 这个类型的完整有效名称(全名=包名.类名)
  • 这个类型直接父类的完整有效名(对于 interface或是 java.lang.Object,都没有父类)
  • 这个类型的修饰符(public,abstract,final 的某个子集)
  • 这个类型直接接口的一个有序列表

域(Field)信息:

JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集)

方法(Method)信息:

JVM 必须保存所有方法的

  • 方法名称
  • 方法的返回类型
  • 方法参数的数量和类型
  • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
  • 方法的字符码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
  • 异常表(abstract 和 native 方法除外)
    • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

设置大小

Java7 中我们通过-XX:PermSize 和 -xx:MaxPermSize 来设置永久代参数

JDK8 及以后:

  • 元数据区大小可以使用参数 XX:MetaspaceSize 和 XX:MaxMetaspaceSize 指定,替代上述原有的两个参数

  • 默认值依赖于平台。Windows 下,XX:MetaspaceSize 是 21M,XX:MaxMetaspacaSize 的值是 -1,即没有限制

  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据发生溢出,虚拟机一样会抛出异常 OutOfMemoryError:Metaspace

  • XX:MetaspaceSize :设置初始的元空间大小。对于一个 64 位的服务器端 JVM 来说,其默认的 XX:MetaspaceSize 的值为20.75MB,这就是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置,新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足,那么在不超过 MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。

  • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收的日志可观察到 Full GC 多次调用。为了避免频繁 GC,建议将 XX:MetaspaceSize 设置为一个相对较高的值。

    所以对于方法区,Java8 之后的变化:

    • 移除了永久代(PermGen),替换为元空间(Metaspace);
    • 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
    • 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
    • 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
    • 移除了永久代(PermGen),替换为元空间(Metaspace);
    • 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
    • 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
    • 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

特点

  • 方法区和堆一样也是"线程共享"的内存区域
  • 会抛出OutOfMemoryError 异常

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分, 用于存放编译器生成的各种字面量和符号引用。

解运行时常量池的话,我们先来说说字节码文件(Class 文件)中的常量池(常量池表)

常量池

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包含各种字面量和对类型、域和方法的符号引用。

常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

运行时常量池

  • 在加载类和结构到虚拟机后,就会创建对应的运行时常量池
  • 常量池表(Constant Pool Table)是 Class 文件的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
  • JVM 为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的
  • 运行时常量池中包含各种不同的常量,包括编译器就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或字段引用。此时不再是常量池中的符号地址了,这里换为真实地址
    • 运行时常量池,相对于 Class 文件常量池的另一个重要特征是:动态性,Java 语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String 类的 intern() 方法就是这样的
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则 JVM 会抛出 OutOfMemoryError 异常。

移除永久代原因

  • 为永久代设置空间大小是很难确定的。
    在某些场景下,如果动态加载类过多,容易产生 Perm区的 OOM。如果某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现 OOM。而元空间和永久代最大的区别在于,元空间不在虚拟机中,而是使用本地内存,所以默认情况下,元空间的大小仅受本地内存限制。
  • 对永久代进行调优较困难

直接内存(Direct Memory)

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范定义的内存区域。

在JDK1.4中新加入了NIO(new Input/Output) 类,引入了一种基于通道(Channel) 与 缓冲区(buffer) 的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

这样操作能避免来回在Java堆和Native堆复制数据, 从而显著提高性能。

这部分内存也会被频繁的使用,也会抛出OutOfMemoryError 异常。

总结

  1. 程序计数器
    1. 保存的是线程正在执行的字节码指令地址
    2. 线程私有的区域
    3. 唯一不会抛出OutOfMemoryError 异常的区域
  2. JAVA虚拟机栈
    1. 用于存储局部变量表、操作数栈、动态连接、方法出口信息等。
    2. 线程私有的区域
    3. 会抛出StackOverFlowError 和 OutOfMemoryError 异常
  3. 本地方法栈
    1. 功能与虚拟机栈相似,主要是为虚拟机使用到的Native 方法服务
    2. 线程私有的区域
    3. 会抛出StackOverFlowError 和 OutOfMemoryError 异常
  4. JAVA 堆
    1. 存放JAVA 对象实例和数组实例
    2. 线程共享的区域
    3. 会抛出OutOfMemoryError 异常
  5. 方法区
    1. 主要存放类的结构信息、常量信息、字段信息、静态变量、方法数据、构造函数、普通方法的字节码信息、即时编译器编译后的代码等信息
    2. 线程共享的区域
    3. 会抛出OutOfMemoryError 异常
  6. 运行时常量池
    1. 用于存放编译器生成的各种字面量和符号引用
    2. 是方法区的一部分,是线程共享的区域
    3. 会抛出OutOfMemoryError 异常
  7. 直接内存
    1. 不是虚拟机规范定义的内存区域, 使用Native函数库直接分配堆外内存
    2. 会抛出OutOfMemoryError 异常
精 灵 王 wechat
👆🏼欢迎扫码关注微信公众号👆🏼
  • 本文作者: 精 灵 王
  • 本文链接: https://jinglingwang.cn/archives/memoryarea
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 设计模式之美 # 分布式 # Redis # 并发编程 # 个人成长 # 周志明的软件架构课 # 架构 # 单元测试 # LeetCode # 工具 # 位运算 # 读书笔记 # 操作系统 # MySQL # 异步编程 # 技术方案设计 # 集合 # 设计模式 # 三亚 # 游玩 # 转载 # Linux # 观察者模式 # 事件 # Spring # SpringCloud # 实战 # 实战,SpringCloud # 源码分析 # 线程池 # 同步 # 锁 # 线程 # 线程模型 # 动态代理 # 字节码 # 类加载 # 垃圾收集器 # 垃圾回收算法 # 对象创建 # 虚拟机内存 # 内存结构 # Java
Java 线程池生命周期
Java 中类加载的机制总结
  • 文章目录
  • 站点概览
精 灵 王

精 灵 王

青春岁月,以此为伴

85 日志
14 分类
43 标签
RSS
E-mail
Creative Commons
Links
  • 添加友链说明
© 2022 精 灵 王
渝ICP备2020013371号
0%