【JVM】之运行时数据区 Runtime Data Areas

news/2025/2/26 7:27:10

Runtime Data Areas

官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

概述

官方解释

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

在程序的执行过程中,Java虚拟机定义了各种运行时数据区。其中一些运行时数据区创建于Java虚拟机启动,并且仅当虚拟机退出时销毁。而其他则是每个线程独有的,线程独有的数据区在线程创建时创建。线程退出时销毁。

注:

线程共享数据区:堆区Heap、方法区Method Area[堆、堆外内存(永久代或元空间、代码缓存)]

线程独占数据区:Java虚拟机栈(JVM Stack)、本地方法栈(Native Method Stack)、程序计数器/PC寄存器(Program Counter Register).

图示

在这里插入图片描述

详细图示

在这里插入图片描述

PC寄存器

The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine’s pc register is undefined. The Java Virtual Machine’s pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.

Java虚拟机能够支持多个线程同时运行。每一个Java虚拟机线程都有它自己的PC寄存器。任何时候,一个Java虚拟机线程只会在一个方法代码中执行。这个方法称之为这个线程的当前方法。如果这个方法不是本地方法,则PC寄存器中存储的是当前执行的Java虚拟机指令的地址。如果正在执行的方法时一个本地方法,则在PC寄存器中的地址为undefined。Java虚拟机的PC寄存器有足够大的容量去容纳一个返回地址或者在特殊平台的一个本地指针。

总结:

  • PC寄存器是每个线程独有的。
  • PC寄存器记录的是当前线程正在执行的Java虚拟机指令的地址(非本地方法)。或者是undefined(本地方法)。
  • PC寄存器的生命周期和线程的生命周期一致。
  • 任何时间一个线程只有一个方法在执行,称之为当前方法
  • 字节码解释器工作时就是通过改变这计数器的值来选取下一条需要执行的字节码指令。它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复基础功能都需要依赖这个计数器来完成。
  • 它是《Java虚拟机规范》中唯一一个没有规定任何OutOfMemoryError情况的区域。

PC寄存器为什么要设置为私有的?

-> 我们都知道所谓的多线程在一个特定的时间段内只会执行某一个线程的方法,CPU会不停的切换任务,这样必然导致经常中断和恢复,如何保证分毫不差呢?

为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的方法就是每个线程都有自己的PC寄存器。这样,各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

由于 CPU 时间片轮限制,多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

CPU 时间片

CPU 时间片即 CPU 分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。

在宏观上:俄们可以同时打开多个应用程序,每个程序并行不悖,同时运行。

但在微观上:由于只有一个 CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

在这里插入图片描述

虚拟机栈

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.

每一个虚拟机线程都有一个私有的虚拟机栈。当线程创建的时候而创建。一个Java虚拟机栈存储着多个Java虚拟机栈帧。Java虚拟机栈类似于C中的栈。它有本地变量表(Local variables)和部分结果。是方法调用和返回的一部分。因为Java虚拟机栈出了入栈和出栈以外,不能进行其他操作。Java虚拟机栈内存空间可以是不连续的。

In the First Edition of The Java® Virtual Machine Specification, the Java Virtual Machine stack was known as the Java stack.

This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created.

Java虚拟机规范中运行Java虚拟机栈可以是一个固定内存大小,也可以是动态扩展和收缩的。如果Java虚拟机栈是一个固定大小的内存,这个大小是可以在Java虚拟机栈创建时进行选择的。

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of Java Virtual Machine stacks, as well as, in the case of dynamically expanding or contracting Java Virtual Machine stacks, control over the maximum and minimum sizes.

Java虚拟机提供给程序员或用户去控制虚拟机栈的初始化大小,同时,如果是动态扩容或收缩的场景下,对最大最小容量的控制。

The following exceptional conditions are associated with Java Virtual Machine stacks:异常:

  • If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.

如果虚拟机栈申请的内存超过了被允许的大小,虚拟机会抛出一个StackOverflowError;

  • If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

如果java虚拟机可以动态扩容,并且尝试了扩容但是还是没有足够的内存可以扩容,或者在创建线程没有足够的空间进行栈的初始化,此时Java虚拟机会抛出OutOfMemoryError

总结:

  • Java虚拟机栈是每个线程独有的。
  • Java虚拟机栈的生命周期和线程一样。
  • Java虚拟机栈只能进行栈帧的入栈和出栈,不能进行其他操作。
  • Java虚拟机可以是固定大小或者是动态扩展和收缩的。如果是固定大小,则可以进行指定的。
  • 固定大小。如果虚拟机栈申请的内存超过了被允许的大小,虚拟机会抛出一个StackOverflowError;
  • 动态扩容。在扩容时无法申请到足够内存,或者新创建线程没有足够空间初始化栈,则会抛出OutOfMemoryError。

基本内容

Java虚拟机栈是什么?

Java虚拟机栈(Java Virtual Machine Stack), 每一个线程都会创建一个自己的Java虚拟机栈,其内部是由一个一个的栈帧组成的(Stack Frame),对应着方法的一次一次调用,线程私有的。

一个栈帧的创建意味着一个方法的调用开始,而一个栈帧的销毁意味着一个方法的结束(包括正常结束&异常退出)。

生命周期

和线程的生命周期一样。

作用

Java虚拟机栈主管程序的运行,保存着局部变量和部分返回结果,是方法调用和返回的一部分。

栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器。

JVM直接对Java栈的操作只有两个:

  • 每一个方法的执行,伴随着栈帧的入栈
  • 执行结束后栈帧的出栈

对于栈来说不存在垃圾回收的情况。(栈存在异常的情况 StackOverflowError)

在这里插入图片描述

栈中可能出现的异常

Java 虚拟机规范允许Java 栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个StackOverflowError 异常。

  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

**注意:**但是HotSpot虚拟机的栈内存是不可以动态扩展的,所以在Hotspot虚拟机中不会出现栈的OOM,只会出现StackOverflowError。

public void test() {
    System.out.println("AAA");
    test();
}
// 上述代码就会造成栈的StackOverflowError
Exception in thread "main" java.lang.StackOverflowError
	at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)

我们可以通过-Xss10m来设置栈的大小为10m;

栈的存储单位

栈中存储的是什么?

每一个线程都有自己的栈,栈中的数据是以栈帧的形式存在的。在这个线程上正在执行的每个方法都有对应自己的栈帧(Stack Frame)。

栈帧(Stack Frame)是一个内存块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈运行原理

JVM直接对java虚拟机栈的操作有两个,一个是入栈,一个是出栈,遵循栈的特点先进后出,后进先出原则。

在一条活动线程上中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame), 与当前栈帧对应的方法称为当前方法(Current Method). 定义这个方法的类称为当前类(Current Class)

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

如果当前方法中调用了其他方法,对应的新的栈帧会被创建出来,然后压入栈顶,称为新的当前栈帧。

在这里插入图片描述

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另一个栈帧。

如果当前方法调用了其他方法,在方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机栈会丢弃当前栈帧,使得前一个栈帧称为新的当前栈帧。

每一个方法从调用开始到执行结束的过程,都对应着一个栈帧的入栈和出栈的过程。

Java方法返回方式

Java方法有两种返回函数的方式,一种是正常的方法执行结束返回,使用return指令,另一种就是出现异常。不管是哪种方式,都会导致栈帧被弹出。

栈帧的内部结构

每个栈帧都存储着下列信息:

  • 局部变量表(Local Variables [Table])
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
  • 返回地址(Return Address)(或方法正常退出或者异常退出的定义)
  • 附加信息

图示:

在这里插入图片描述

并行情况下,每一个线程的栈都是私有的,一次每一线程都有各自的栈,并且每个栈中都有很多栈帧。

在编译Java源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来了,并且写入到方法表的code属性之中,换言之,一个栈帧需要分配多少内存,并不会收到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。

局部变量表(LV)

局部变量表【Local Variables】也称为局部变量数组或本地变量表。

  • 定义为一个数字数组。主要用于存储方法参数和定义在方法体内的局部变量.这些数据类型包括各种基本数据类型、对象引用(reference),以及returnAddress类型。
  • 由于局部变量表是建立在线程的虚拟机栈上,是线程私有的,所以不存在数据安全的问题
  • 局部变量表所需的容量在编译期间就确定下来了。并保存在方法表的code属性的maximum local variables数据项中。在方法运行期间不会改变局部变量表的大小。
  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更大的栈空间,使得嵌套调用的次数减少。
  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递。当方法调用结束后,随着方法的栈帧的销毁,局部变量表也会随之销毁。

关于slot的理解

  • 局部变量表,最基础的存储单元Slot(变量槽)
  • 参数值的存放总是在局部变量数组index0开始,到数组长度-1的索引结束。
  • 局部变量表中存放编译期可知的各种基础数据(8种),引用数据(reference),returnAddress类型的变量。
  • 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占两个slot。
  • byte,short,char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
  • JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表的每一个slot上。
  • 如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long和double类型的变量)。
  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象的this引用将会存放在局部变量表下标为0的位置上。其余参数按照顺序依次存放。

在这里插入图片描述

Slot的重复利用

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量的作用域已经结束,那么在其作用域外之后声明的变量就很有可能会重复利用其过期的槽位。从而达到节省资源的目的。

public class SlotReuseTest {
    public static void main(String[] args) {
        {
            int a = 1;
        }
        int b = 2;
    }
}

上面代码int类型的b会重用a的槽位,因为在b声明的地方,a的作用域已经结束了。

在这里插入图片描述

静态变量和局部变量的对比

参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。

我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次在“初始化阶段”,为对象设置显示初始化值。

和类变量不同的是,局部变量表不存在系统初始化过程,这意味着一旦定义了局部变量则必须手动初始化,否则无法使用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YkbjaTnE-1637070186718)(images/image-20211023103255926.png)]

上述图片可以看到,编译期即出错。

补充说明

在栈帧中。与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根节点(GC Roots),只要被局部变量表中直接或间接引用的对象都不会被回收

==>【可达性分析,以GC Roots根节点,GC Roots直接可达或间接可达的对象都不是垃圾】。

操作数栈(OS)

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

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

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

在这里插入图片描述

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

操作数栈就是 JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之创建出来,这个方法的操作数栈是空的。

每一个操作数栈都有一个明确的栈深度由于存储数值,其所需的最大深度在编译期间就已经确定好了,保存在放的code的属性中,为 maximum stack size的值。

在这里插入图片描述

栈中的任何一个元素都是可以任意的Java数据类型。

  • 32bit的类型占用一个栈单位的深度。
  • 64bit的类型占用两个栈单位的深度。

操作数栈并非采用索引的方式进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。

如果被调用的方法带有返回值的话,其返回值将会压入到当前栈帧的操作数栈顶中,并更新PC寄存器下一条需要执行的指令。

操作数栈中元素的数据类型必须和字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,这里的栈指的就是操作数栈。

动态链接(DL)

动态链接(Dynamic Linking),方法返回地址,附加信息:有些地方称之为帧数据区。

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking). 比如:invokedynamic指令。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mVkKrY8M-1637070186723)(images/image-20211023115930674.png)]

为什么需要运行时常量池呢?

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

方法的调用:解析与分配

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

静态链接

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转为直接引用。这种情况下将调用方法的符号引用转为直接引用的过程称之为静态链接

动态链接

如果被调用的方法在编译器无法被确定下来,只能够在程序运行期将调用的方法的符号引用转为直接引用,由于这种引用转换过程具有动态性,因此称之为动态链接。

静态链接和动态链接不是名词,而是动词,这是理解的关键。

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

早期绑定

早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转为直接引用。

晚期绑定

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

虚方法和非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称之为非虚方法。

静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称之为虚方法。

普通调用指令

  • invokestatic:调用静态方法,解析阶段确定唯一方法版本
  • invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法

动态调用指令

  • invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic 指令则支持由用户确定方法版本。其中 invokestatic 指令和 invokespecial 指令调用的方法称为非虚方法,其余的(fina1 修饰的除外)称为虚方法。

关于 invokedynamic 指令

  • JVM 字节码指令集一直比较稳定,一直到 Java7 中才增加了一个 invokedynamic 指令,这是Java 为了实现「动态类型语言」支持而做的一种改进。

  • 但是在 Java7 中并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。直到 Java8 的 Lambda 表达式的出现,invokedynamic 指令的生成,在 Java 中才有了直接的生成方式。

  • Java7 中增加的动态语言类型支持的本质是对 Java 虚拟机规范的修改,而不是对 Java 语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在 Java 平台的动态语言的编译器。

动态类型语言和静态类型语言

动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。

说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

方法重写的本质

Java 语言中方法重写的本质:

  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。
  2. 如果在类型 C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常。
  3. 否则,按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出 java.1ang.AbstractMethodsrror 异常。

IllegalAccessError 介绍

程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

方法返回地址RA

存放调用该方法的PC寄存器的值。一个方法的结束,有两种情况:

  • 正常执行退出。即return指令。
  • 出现未处理的异常,非正常退出。

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

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

  1. 执行引擎遇到任意一个方法返回的字节码指令(return).会有返回值传递给上层的方法调用者。简称正常调用完成
    1. 一个方法在正常调用完成之后,究竟需要使用哪一个退出指令,还需要根据方法的返回值的实际类型而定。
    2. 在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short,int类型时使用)。lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。另外还有一个return指令表名为void的方法,实例初始化方法,类和接口的初始化方法使用。
  2. 在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有找到对应的异常处理器,就会导致方法的退出,简称异常调用完成

代码:

public static void main(String[] args) {
    try {
        int i = 0;
        int a = 2 / i;
    } catch (Exception e) {
        e.printStackTrace();
    }
}

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

在这里插入图片描述

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

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

附加信息

《java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧中。

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。而本地方法栈则是为虚拟机是用到的本地方法服务。

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryErrory异常。

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

概述

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads.

The heap is the run-time data area from which memory for all class instances and arrays is allocated.

Java虚拟机的堆区被所有Java线程所共享。堆是运行时数据区用来分配所有的类实例和数组的。

The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated.

堆在虚拟机启动时创建,堆存储的对象通过自动的存储管理系统来进行释放内存。(如垃圾收集器)。对象不需要明确的回收,即不需要手动回收。

The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor’s system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.

Java虚拟机不采用特定的某种自动存储管理系统,而取决于其实现系统选择的存储技术。堆可以是一个固定大小的,也可以是一个可以动态扩容和收缩的。堆内存不需要是连续的一块空间。

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size.

一个java虚拟机实现可以提供给程序员或者用户自己控制堆区的初始化大小。同时,如果堆区支持动态扩容和压缩,也可以进行最大和最小的堆区设置。

The following exceptional condition is associated with the heap:

  • If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError.

堆内存相关的异常:

​ 当计算所需要的堆区大小比自动内存管理系统能够提供的要大,Java虚拟机则会抛出OOM。

总结:

  1. 堆是线程共享的区域。用来分配所有的对象实例和数组。

    The heap is the run-time data area from which memory for all class instances and arrays is allocated.

  2. 堆在虚拟机启动时创建。无需手动进行内存释放。方法结束后,堆中的对象不会马上被移除,需要垃圾收集的时候才会回收移除。

  3. 如果是固定大小的堆,则可以通过设置其初始化大小,如果可动态扩展和压缩的,则可以设置其最大和最小的大小。

    • 通过-Xms20m来设置堆区的最小内存为20m,-Xmx20m设置堆最大内存为20m,-Xmn10m设置新生代内存大小为10m。
    • 通过-XX:SurvivorRatio=8来设置Eden区:s0:s1=8:1:1;
    • 通过-XX:NewRatio=2来设置新生代和老年代的比例。新生代为1,老年代占2;
  4. 当对象所需堆区不够时,虚拟机会抛出OOM。

  5. 堆区可以是不连续的区域。【堆可以在物理上不是连续的,但是逻辑上应该视为其连续的】。

  6. 数组和对象不会存储在栈中,因为栈帧保存引用。这个引用指向对象或数组在堆中的位置。【Hotspot虚拟机启用了逃逸分析,但是不会进行栈上分配,只会进行标量替换】

  7. 堆是GC(Garbage Collection 垃圾收集器)执行垃圾收集回收的重点区域。

  8. 如果从分配内存的角度看,所有的线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer),以提高对象分配的效率。

栈堆方法区的关系图

在这里插入图片描述

堆内存细分

Java7及之前堆内存逻辑上分为三部分:新生代+老年代+永久代

  • Young Generation Space 新生代。新生代又分为 Eden区和Survivor区。
  • Tenure Generation Space 老年代。Old/Tenure.
  • Permanent Space 永久代。

Java8及之后的堆内存逻辑上分为三部分:新生代+老年代+元空间

  • Young Generation Space 新生代。同样分为Eden区和Survivor区。
  • Tenure Generation Space 老年代。Old/Tenure.
  • Meta Space. 元空间

设置堆内存大小与OOM

堆空间大小设置

Java堆区用于存储对象实例和数组。堆的大小在JVM启动的时候就设置好了。可以通过-Xms-Xmx来设置。

  • -Xms:用于设置堆的初始内存。等价于-XX:InitialHeapSize;
  • -Xmx:用于设置堆的最大内存。等价于-XX:MaxHeapSize;

一旦堆区中的内存大小超过-Xmx所指定的内存,将会抛出OOM[OutOfMemoryError]异常。

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

默认情况下

  • 初始内存大小:电脑物理内存大小/64;
  • 最大内存大小:电脑物理内存大小/4;
long totalMemory = Runtime.getRuntime().totalMemory(); //初始内存
long maxMemory = Runtime.getRuntime().maxMemory(); //最大内存
System.out.println(totalMemory / 1024.0 / 1024 / 1024 + "G"); // 0.23779296875G
System.out.println(maxMemory / 1024.0 / 1024 / 1024 + "G");// 3.51220703125G

OutOfMemoryError 举例

-Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8

-Xms20m -Xmx20m 设置堆的初始化内存和最大内存都为10M

-XX:SurvivorRatio=8 设置Eden:Survivor0:Survivor1=8:1:1

public class HeapOutOfMemory {
	// -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 
	public static void main(String[] args) throws InterruptedException {
		byte[] bytes = new byte[1024 * 1024 * 10];//10M
		TimeUnit.SECONDS.sleep(1000000);
	}
}

运行上述代码:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.example.demo.jvm.heap.HeapOutOfMemory.main(HeapOutOfMemory.java:8)

新生代与老年代

存储在JVM的Java对象可以被划分为两类:

  • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常的迅速。
  • 另一类对象的生命周期却非常长,在某些极端情况下还能够与JVM生命周期保持一致。

Java堆区进一步划分的话,可以分为新生代(YoungGen)和老年代(OldGen);

其中新生代又分为 Eden区、Survivor0 和 Survivor1区。有时也称之为 From区 和 To区。

在这里插入图片描述

配置新生代和老年代占比

  • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占堆区的1/3;
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占堆区的1/5;

在HotSpot中,Eden空间和两个Survivor区的比例是 8:1:1; 可以使用-XX:SurvivorRatio=5来进行手动设置。

==几乎所有的Java对象都是在Eden区被new出来的。==绝大部分的Java对象的销毁都在新生代进行了。

IBM公司的专门研究表明:新生代的80%的对象都是朝生夕死的。

可以使用-Xmn来设置新生代最大内存大小,这个参数一般是使用默认值即可。

图解对象分配过程

在这里插入图片描述

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

  1. new的对象先放在Eden区。此区有大小限制。
  2. 当Eden区的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对新生代进行垃圾回收(MinorGC/YoungGC).将伊甸园区Eden的不再被其他对象所引用的对象进行销毁。再加载新的对象到伊甸园区。
  3. 然后将伊甸园区的剩余对象移动到幸存者0区。对象年龄计数器加1。【存储在对象头中】
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到了幸存者0区,如果没有回收,就会放到幸存者1区。
  5. 如果再次经历垃圾回收,此时会重新放回到幸存者0区,接着再去幸存者1区。
  6. 啥时候能去老年区呢?可以设置次数。默认是15次。
    • 可以通过参数:-XX:MaxTenuringThreshold=N来进行设置。
  7. 在老年区相对安全。当老年代区域内存不足时,再次触发GC[MajorGC],对养老区进行内存清理。【FullGC。对整堆进行清理】
  8. 若养老区执行了MajorGC之后,发现依然无法进行对象内存分配,则会抛出OOM异常。【java.lang.OutOfMemoryError: Java Heap Space

对象分配和晋升流程图

在这里插入图片描述

总结:

  • 针对幸存者s0.s1区的总结:复制之后有交换,谁空谁是To。
  • 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不在永久代和方法区收集。

Minor GC,MajorGC、Full GC

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

很对Hotspot虚拟机的实现,他里面的GC按照回收区域又分为两种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC).

  • 部分收集:不是完整的收集整个Java堆的垃圾收集。其中又分为:
    • 新生代收集(MinorGC/ YoungGC):只是新生代的垃圾收集。
    • 老年代收集(MajorGC/OldGC):只是老年代的垃圾收集。
      • 目前,只有CMS会有单独的老年代收集行为
      • 注意,很多时候,MajorGC和FullGC都是混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • 混合回收(MixedGC):收集整个新生代部分老年代的垃圾收集。
      • 目前,只有G1会有这种行为。【region】
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

最简单的分代式GC策略的触发条件

年轻代GC(MinorGC)的触发机制

  • 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden区满,Survivor区满不会触发GC。(每次MinorGC会清理年轻代的内存)。
  • 因为Java对象大多都具备朝生夕死的特点,所以MinorGC非常频繁,一般回收速度也比较快。
  • MinorGC会引发STW,暂停其他用户线程。等垃圾回收结束,用户线程才会恢复运行。

老年代GC(MajorGC/FullGC)触发机制

  • 指发生在老年代的GC,对象从老年代消失时,我们说“MajorGC”或”FullGC“发生了。
  • 出现了MajorGC,经常会伴随着至少一次“MinorGC”(但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
    • 也就是在老年代空间不足时,会先尝试触发MinorGC,如果之后空间还不足,则会触发MajorGC。
  • MajorGC的速度一般比MinorGC慢10倍以上,STW的时间更长。
  • 如果MajorGC后,内存还是不足,则会报OOM。

FullGC触发机制

触发Full GC执行的情况有如下五种:

  1. 调用了System.gc(); 系统建议执行FullGC,但不是必然执行。
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过MinorGC后进入老年代的平均大小大于老年代的可用内存。
  5. 由Eden区、S0区向S1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代可用内存小于该对象大小。

说明:FullGC是开发或调优中尽量避免的。这样STW世家你会短一些。

堆空间分代思想

为什么要把Java堆分代?不分代就不能正常工作吗?

经研究:不同对象的生命周期是不同的。70%-99%的对象是临时对象。

  • 新生代:由Eden,两个大小相同的survivor(又称From/To,S0/S1)构成,to总是空。
  • 老年代:存放新生代中经历多次GC仍然存活的对象。

其实不分代完全可以,分代的唯一理由就是优化GC的性能。如果没有分代,那么所有的对象都在一块。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块对象的“朝生夕死”对象的区域进行回收。这样就会腾出比较大的空间。

在这里插入图片描述

内存分配策略

如果对象在Eden出生并经过第一次MinorGC存活下来,并且能被Survivor容纳的话,将被移动到Survivor区,并将对象的年龄设置为1. 对象在Survivor区中没熬过一次MinorGC,年龄就会增加1,当他的年代到达一定程度(默认为15岁,其实每个JVM,每个GC有所不同)时。就会被晋升到老年代。

对象的晋升老年代年龄阈值。可以通过-XX:MaxTenuringThreshlod来设置。

针对不同年龄段的对象分配原则如下:

  • 优先分配到Eden。
  • 大对象直接分配到老年代。(尽量避免程序中出现过多的大对象)
  • 长期存活的对象分配到老年代。
  • 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和超过了Survivor区的一半。年龄大于或等于该年龄的对象将会直接晋升到老年代。无需等到-XX:MaxTenuringThreshlod要求达到的年龄值。
  • 空间分配担保:-XX:HandlePromotionFailure.

为对象分配内存TLAB

为什么有TLAB(Thread Local Allocation Buffer)?

  • 堆区是线程共享区域,任何线程都可以访问到堆区的共享数据。
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

什么是TLAB?

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

TLAB 的再说明

  • 尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但JVM 确实是将 TLAB 作为内存分配的首选

  • 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启 TLAB 空间。

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

  • 一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。

在这里插入图片描述

堆空间的参数设置

官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

-XX:+PrintFlagsInitial 查看所有的参数的默认初始值。

-XX:+PrintFlagsFinal 查看所有的参数的最终值(可能会存在修改,不再是初始值)

-Xms: 堆区的初始化值。(默认为物理内存的1/64)

-Xmx: 堆区的最大值。(默认为物理内存的1/4)

-Xmn: 新生代大小。(初始值及最大值)

-XX:NewRatio=2 设置新生代和老年代的比例。2表示新生代:老年代=1:2;新生代占堆区的1/3;

-XX:SurvivorRatio=8 设置Eden区与Survivor区的比例。8表示Eden:S0:S1=8:1:1;

-XX:MaxTenuringThreshold=15 设置对象晋升成老年代的年龄阈值。默认是15;

-XX:+PrintGCDetails 打印GC的详细信息。 打印GC简要信息:1. -XX:+PrintGC. 2.、-verbose:gc

-XX:HandlePromotionFailure 是否开启空间担保策略。

-XX:HandlePromotionFailure

在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间

  • 如果大于,则此次MinorGC是安全的。
  • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
    • 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
      • 如果大于,则尝试进行一次MinorGC,但这次MinorGC依然是有风险的。
      • 如果小于,则改为进行一次FullGC。
    • 如果HandlePromotionFailure=false,则改为进行一次FullGC;

在 JDK6 Update24 之后,HandlePromotionFailure 参数不会再影响到虚拟机的空间分配担保策略,观察 openJDK 中的源码变化,虽然源码中还定义了 HandlePromotionFailure 参数,但是在代码中已经不会再使用它。

JDK6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 FullGC。

堆是分配对象的唯一选择么?

在《深入理解 Java 虚拟机》中关于 Java 堆内存有这样一段描述:

随着 JIT 编译期的发展与逃逸分析技术逐渐成熟,栈上分配标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

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

逃逸分析概述

如何将堆上的对象分配到栈,需要使用逃逸分析手段。

这是一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

举例 1

public void my_method() {
    V v = new V();
    // use v
    // ....
    v = null;
}

没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除,每个栈里面包含了很多栈帧

public static StringBuffer createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

上述方法如果想要StringBuffer sb不发生逃逸,可以这样写

public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

举例 2

public class EscapeAnalysis {
    public EscapeAnalysis obj;

  	// 方法返回EscapeAnalysis对象,发生逃逸
    public EscapeAnalysis getInstance() {
        return obj == null ? new EscapeAnalysis() : obj;
    }

    // 为成员属性赋值,发生逃逸
    public void setObj() {
        this.obj = new EscapeAnalysis();
    }

    // 对象的作用于仅在当前方法中有效,没有发生逃逸
    public void useEscapeAnalysis() {
        EscapeAnalysis e = new EscapeAnalysis();
    }

    // 引用成员变量的值,发生逃逸
    public void useEscapeAnalysis2() {
        EscapeAnalysis e = getInstance();
    }
}

参数设置

在 JDK 6u23 版本之后,HotSpot 中默认就已经开启了逃逸分析

如果使用的是较早的版本,开发人员则可以通过:

  • 选项“-XX:+DoEscapeAnalysis"显式开启逃逸分析
  • 通过选项“-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果

结论开发中能使用局部变量的,就不要使用在方法外定义。

代码优化

使用逃逸分析,编译器可以对代码做如下优化:

一、栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配

二、同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

三、分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。

栈上分配

JIT 编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

常见的栈上分配的场景

在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。

同步省略

线程同步的代价是相当高的,同步的后果是降低并发性和性能。

在动态编译同步块的时候,JIT 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么 JIT 编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除

举例

public void f() {
    Object hellis = new Object();
    synchronized(hellis) {
        System.out.println(hellis);
    }
}

代码中对 hellis 这个对象加锁,但是 hellis 对象的生命周期只在 f()方法中,并不会被其他线程所访问到,所以在 JIT 编译阶段就会被优化掉,优化成:

public void f() {
    Object hellis = new Object();
	System.out.println(hellis);
}

标量替换

标量(scalar)是指一个无法再分解成更小的数据的数据。Java 中的原始数据类型就是标量。

相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java 中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在 JIT 阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过 JIT 优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

举例

public static void main(String args[]) {
    alloc();
}
private static void alloc() {
    Point point = new Point(1,2);
    System.out.println("point.x" + point.x + ";point.y" + point.y);
}
class Point {
    private int x;
    private int y;
}

以上代码,经过标量替换后,就会变成

private static void alloc() {
    int x = 1;
    int y = 2;
    System.out.println("point.x = " + x + "; point.y=" + y);
}

可以看到,Point 这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。 标量替换为栈上分配提供了很好的基础。

标量替换参数设置

参数-XX:EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配到栈上。

上述代码在主函数中进行了 1 亿次 alloc。调用进行对象创建,由于 User 对象实例需要占据约 16 字节的空间,因此累计分配空间达到将近 1.5GB。如果堆空间小于这个值,就必然会发生 GC。使用如下参数运行上述代码:

-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

这里设置参数如下:

  • 参数-server:启动 Server 模式,因为在 server 模式下,才可以启用逃逸分析。
  • 参数-XX:+DoEscapeAnalysis:启用逃逸分析
  • 参数-Xmx10m:指定了堆空间最大为 10MB
  • 参数-XX:+PrintGC:将打印 Gc 日志
  • 参数-XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有 id 和 name 两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配

注意:【Hotspot栈上分配是使用标量替换完成的】

Oracle Hotspot JVM 中并未使用栈上分配,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上

总结

年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。

老年代放置长生命周期的对象,通常都是从Survivor区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上。如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接将其放置到老年代。

当GC只发生在年轻代中,回收年轻代对象的行为称之为MinorGC。

当GC发生在老年代时,则被称为MajorGC或者FullGC。一般的,MinorGC的频率要比MajorGC高得多,即老年代中垃圾回收发生的频率将大大低于年轻代。

方法区

官方文档:Chapter 2. The Structure of the Java Virtual Machine (oracle.com)

栈、堆、方法区的交互关系图

在这里插入图片描述

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads.

Java虚拟机有一个方法区是被所有Java虚拟机线程所共享的。

The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process.

方法区类似于常规语言编译代码的存储区域,或类似于操作系统进程中的文本段。

It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

方法区存每个类的结构信息,如 运行时常量池,字段和方法数据。方法和构造器的字节码信息。包括特殊方法用于在类或对象实例初始化或者接口初始化。

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

方法区在虚拟机启动时创建,虽然方法区在逻辑上是属于堆区的一部分,简单的实现可以选择不进行垃圾收集和压缩。这个规范不强制要求方法区的位置和管理已编译代码的策略。方法区可以是一个固定大小的,也可以是可动态扩展和压缩的。方法区的内存不需要连续。

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

Java虚拟机实现需要提供程序员或者用户控制方法区的初始化大小。同时,在可变大小的方法区,则需控制方法区的最大最小值。

The following exceptional condition is associated with the method area:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

异常:如果方法区不能获取到能够提供的内存,Java虚拟机则会抛出OOM;

总结:

  • 方法区是线程共享的。生命周期和Java虚拟机一样。并且实际的物理内存空间可以和Java堆区一样都是可以不连续的。
  • 方法区的大小和堆空间一样,可以选择固定大小或者可扩展的。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。java.lang.OutOfMemoryError:PermGen space或者 java.lang.OutOfMemoryError:Metaspace.
    • 加载大量的第三方jar包;Tomcat部署的工程过多(30-50个)。大量动态的生成反射类
  • 关闭虚拟机就会释放这个区域的内存。

HotSpot方法区的演进

在JDK7及以前,习惯上把方法区,称为永久代。JDK8开始,使用元空间取代了永久代。本质上,方法区和永久代并不等价,仅是对Hotspot而言的。

现在看来,当前使用永久代,不是好的IDEA。导致Java程序更容易OOM。(超过-XX:MaxPermSize上限)。

而到了JDK8,完全废弃了永久代的概念。改用使用本地内存的元空间(Meta Space)来代替。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存

永久代、元空间二者不只是名字变了,内部结构也变了。

根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。

设置方法区大小与OOM

设置方法区内存的大小

jdk7及之前

  • 通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M。
  • 通过-XX:MaxPermSize来设置永久代最大可分配空间。32位机器默认是64M,64位机器默认是82M。
  • 当JVM加载的类信息超过这个值,会抛出异常OutOfMemoryError:PermGen space;

jdk8及之后

  • 元空间区大小可以使用参数-XX:MetaspaceSize-XX:MaxMetaspaceSize来指定。
  • 默认依赖于平台。Windows下,-XX:MetaspaceSize=21M -XX:MaxMetaspaceSize=-1//没有限制.
  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError: Metaspace
  • -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务端JVM来说,其默认的-XX:MetaspaceSize值为21M。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)。然后这个高水位线将会重置。新的高水位线的值取决于GC释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值.如果释放空间过多,则适当降低该值。
  • 如果初始化高水位线设置的过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁的GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

OOM代码演示

public class MetaspaceOOM extends ClassLoader{
	// -XX:MetaspaceSize=10m-XX:MaxMetaspaceSize=10m
	public static void main(String[] args) {
		int j = 0;
		try{
			MetaspaceOOM test = new MetaspaceOOM();
			for (int i=0;i<10000;i++){
				//创建Classwriter对象,用于生成类的二进制字节码
				ClassWriter classWriter = new ClassWriter(0);
				//指明版本号,public,类名,包名,父类,接口
				classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
				//返回byte[]
				byte[] code = classWriter.toByteArray();
				//类的加载
				test.defineClass("Class" + i, code, 0, code.length); //CLass对象
				j++;
			}
		} finally{
			System.out.println(j);
		}
	}
}

结果:

[GC (Metadata GC Threshold) [PSYoungGen: 26219K->2224K(76288K)] 26219K->2232K(251392K), 0.0027270 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Metadata GC Threshold) [PSYoungGen: 2224K->0K(76288K)] [ParOldGen: 8K->2065K(122368K)] 2232K->2065K(198656K), [Metaspace: 8977K->8977K(1056768K)], 0.0096671 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Last ditch collection) [PSYoungGen: 0K->0K(76288K)] 2065K->2065K(198656K), 0.0010878 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[Full GC (Last ditch collection) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 2065K->2047K(232448K)] 2065K->2047K(308736K), [Metaspace: 8977K->8977K(1056768K)], 0.0106394 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
8531
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b100000, 0x0000000773080000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b100000,0x000000076b2eb9e0,0x000000076f100000)
  from space 10752K, 0% used [0x000000076fb80000,0x000000076fb80000,0x0000000770600000)
  to   space 10752K, 0% used [0x000000076f100000,0x000000076f100000,0x000000076fb80000)
 ParOldGen       total 232448K, used 2047K [0x00000006c1200000, 0x00000006cf500000, 0x000000076b100000)
  object space 232448K, 0% used [0x00000006c1200000,0x00000006c13ffd70,0x00000006cf500000)
 Metaspace       used 9009K, capacity 10120K, committed 10240K, reserved 1056768K
  class space    used 4565K, capacity 4588K, committed 4608K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at com.example.demo.jvm.metaspace.MetaspaceOOM.main(MetaspaceOOM.java:20)

如何解决OOM

  • 要解决OOM异常或者Heap space的异常,一般的手段是首先通过内存映射分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是要确认内存中的对象是否是必要的。也就是要先分清楚到底是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
  • 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径和GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确的定位出泄漏代码的位置。
  • 如果不存在内存泄漏,换句话说就是内存中的对象确实都需要活着,那就应当检查虚拟机的堆参数-Xms和-Xmx,与机器物理内存对比是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

方法区内部结构

在这里插入图片描述

方法区存储什么

《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:

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

在这里插入图片描述

方法区的内部结构

类型信息

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

  1. 这个类型的完整有效名称(全限定名=包名+类名)。
  2. 这个类型直接父类的完整的有效名(对于interface或是 java.lang.Object,都没有父类)。
  3. 这个类型的修饰符(public, abstract, final 的某个子集)。
  4. 这个类型直接接口的一个有序列表。
域(Field)信息

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

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

方法(Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序。

  • 方法名称。
  • 方法的返回类型(或void)。
  • 方法参数的数量和类型(按顺序)。
  • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)。
  • 方法的字节码(bytecode)、操作数栈深度,局部变量表大小(abstract和native方法除外)。
  • 异常表(abstract和native方法除外)。
    • 每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移位置、被捕获的异常类的常量池索引。
non-final的类变量
  • 静态变量和类关联在一起,随着类的加载而加载,它们称为类数据在逻辑上的一部分。
  • 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它。
public class NonFinalField {
	public static void main(String[] args) {
//		Order order = new Order(); //方式1
		Order order = null; //方式2
		order.hello();
		System.out.println(order.count);
	}
}

class Order {
	public static int count = 1;
	public static void hello() {
		System.out.println("hello!");
	}
}

方式一和方式二均可。

补充说明:全局常量(static final)

被声明为final的类变量的处理方法则不同,每一个全局常量在编译时候就被分配了。

运行时常量池 VS 常量池
  • 方法区,内部包含了运行时常量池。
  • 字节码文件,内部包含了常量池。
  • 要弄清楚方法区,需要理解清除ClassFile,因为加载类的信息都在方法区。
  • 要弄清楚方法区的运行时常量池,需要理解清除ClassFile中的常量池。

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

为什么需要常量池

一个Java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候,会用到运行时常量池。

比如:如下的代码:

public class SimpleClass {
    public void sayHello() {
        System.out.println("hello");
    }
}

虽然只有 194 字节,但是里面却使用了 String、System、PrintStream 及 Object 等结构。这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。

常量池中有什么?

击中常量池内存储的数据类型包含:

  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用

例如下面这段代码:

public class MethodAreaTest2 {
    public static void main(String args[]) {
        Object obj = new Object();
    }
}

Object obj = new Object();将会被翻译成如下字节码:

0: new #2  // Class java/lang/Object
1: dup
2: invokespecial // Method java/lang/Object "<init>"() V
小结

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

运行时常量池

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

方法区使用举例

下面我们分析一段代码:

public class StackFrameInfo {
    public int info(String initStr){
        byte b = 1;
        int i = 128;
        double d = 0.1;
        boolean bool = false;
        Integer in = 129;
        Object o = new Object();
        int sum = b + i;
        return sum;
    }
}

在这里插入图片描述

通过上面的截图可以看到,在class字节码级别,就可以在杂项中看到响应的数据。局部变量表最大10。

我们先猜测一下局部变量表中有哪些:

0.this 
1.initStr 
2.b 
3.i 
4.d 
5.bool 
6.in 
7.o 
8.sum

可以看到只有9个局部变量,为什么局部变量最大槽数10;这是因为double是占用2个slot。所以solt数是10个。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmCDEGh6-1637070186746)(images/image-20211022165005147.png)]

通过上面的图可以验证到我们的猜想了。

字节码长度为39,我们看看jClassLib中的字节码:0-38. 正好为39;

 0 iconst_1
 1 istore_2
 2 sipush 128
 5 istore_3
 6 ldc2_w #2 <0.1>
 9 dstore 4
11 iconst_0
12 istore 6
14 sipush 129
17 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
20 astore 7
22 new #5 <java/lang/Object>
25 dup
26 invokespecial #1 <java/lang/Object.<init> : ()V>
29 astore 8
31 iload_2
32 iload_3
33 iadd
34 istore 9
36 iload 9
38 ireturn

LineNumberTable则是字节码指令序号和代码需要的对应关系;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RfqvXDey-1637070186748)(images/image-20211022165628133.png)]

字节码指令释义:

  1. iconst_<i> 将int类型的常数(-1, 0, 1, 2, 3, 4 or 5) 推入到操作数栈顶
  2. istore_ 存储一个int类型的数据放入下标为N的局部变量表
  3. sipush 将一个short类型的数放入到操作数据栈顶

  4. ldc2_w #2 <0.1> 从运行时常量池中获取一个long或double类型的数据压入到操作数栈中

  5. dstore 4 存储一个double类型的数据到本地变量表的下标4的位置。

  6. invokestatic 调用一个类方法【static】

  7. astore 存储一个引用reference到本地变量表

  8. dup 复制操作数栈顶元素并压入到栈顶

  9. iadd 从操作数栈顶中取两个int类型的数据,相加,并将结果压入到操作数栈顶中。

  10. iload 从局部变量表中获取一个int类型的数

  11. ireturn 方法返回一个int类型的值

第一步

操作的是栈顶栈帧,然后在创建栈帧的时,首先先把this和形参initStr放入到本地变量表的0和1位置上。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dvp3TkQ9-1637070186749)(images/image-20211022212206493.png)]

第二步

执行iconst_1指令。将常数1压入到操作数栈栈顶【从下到上】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nArQT2XW-1637070186751)(images/image-20211022212542874.png)]

第三步

执行istore_2指令。将操作数栈顶的元素弹出,存储到下标为2的本地变量表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMxxSzSy-1637070186752)(images/image-20211022213638969.png)]

第四步

执行sipush 128指令。将short类型的128数据压入到操作数栈栈顶中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NV24iyM-1637070186753)(images/image-20211022214223278.png)]

第五步

执行istore_3指令。将操作数栈顶的元素弹出,并存储在本地变量表下标为3的位置上。

在这里插入图片描述

第六步

执行ldc2_w #2指令。从运行时常量池中区到long类型或者double类型的数据压入到操作数栈顶中。【这里取的是double类型的】

在这里插入图片描述

第七步

执行dstore 4指令。将操作数据栈顶元素【double类型】弹出,并存储到本地变量表下标为4的位置。

在这里插入图片描述

第八步

执行iconst_0指令。将int类型的常数0压入到操作数栈栈顶中。【使用0代表false,非0代表true】。

在这里插入图片描述

第九步

执行istore 6指令。将操作数栈栈顶的int元素弹出,并存储到局部变量表下标为6的位置。

在这里插入图片描述

第十步

执行sipush 129指令。将一个short类型的数据129压入到操作数栈顶中。

在这里插入图片描述

第十一步

调用invokestatic #4 <java/lang/Integer.valueOf:(I)Ljava/lang/Integer;>指令。即调用Integer的valueOf方法,将129包装成Integer类型。会将

在这里插入图片描述

第十二步

执行new指令,在堆中分配一块内存,并对新对象进行默认的初始化。并将该对象的引用压入到操作数栈中。

执行dup指令,赋值操作数栈顶的值,并压入到操作数栈栈顶。

在这里插入图片描述

第十三步

执行invokespecial #1 <java/lang/Object.<init>:()V指令。从当前操作数栈中弹出栈顶元素,作为参数传递给Object的init方法。

<init>方法是没有返回值。

在这里插入图片描述

第十四步

执行astore 8指令,将操作数栈中的栈顶元素弹出,并存储到本地变量表的下标为8的位置。【astore操作的是引用】

在这里插入图片描述

第十五步

执行iload_2指令,加载本地变量表下标位置为2的int类型的数据,并压入到操作数栈的栈顶。

在这里插入图片描述

第十六步

执行iload_3指令。加载本地变量表下标为3的位置上的int类型的数据。并将其压入到操作数栈顶。

在这里插入图片描述

第十七步

执行iadd指令。分别将两个操作数栈顶的数据取出。并相加,最后将结果压入到操作数栈顶中。

在这里插入图片描述

第十八步

执行istore 9指令,将操作数据栈顶的int类型的元素存储到本地变量表下标为9的位置上。

在这里插入图片描述

最后,执行iload 9,加载本地变量表下标位置为9的元素压入到操作数栈栈顶。最后执行ireturn,将这个int类型返回。

方法区的演进细节

  1. 首先明确:只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java 虚拟机规范》管束,并不要求统一
  2. Hotspot 中方法区的变化:
JDK1.6 及之前有永久代(permanet),静态变量存储在永久代上
JDK1.7有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中
JDK1.8无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。

在这里插入图片描述

为什么永久代要被元空间替代?

官网地址:JEP 122: Remove the Permanent Generation (java.net)

JRockit 是和 HotSpot 融合后的结果,因为 JRockit 没有永久代,所以他们不需要配置永久代

随着 Java8 的到来,HotSpot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)

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

这项改动是很有必要的,原因有:

  • 为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生 Perm 区的 oom。比如某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。

    "Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space"
    

    而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。

  • 对永久代进行调优是很困难的。

有些人认为方法区(如 HotSpot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK 11 时期的 ZGC 收集器就不支持类卸载)。 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

StringTable 为什么要调整位置?

jdk7 中将 StringTable 放到了堆空间中。因为永久代的回收效率很低,在 full gc 的时候才会触发。而 full gc 是老年代的空间不足、永久代不足时才会触发。

这就导致 StringTable 回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

静态变量存放在那里?

从《Java 虚拟机规范》所定义的概念模型来看,所有 Class 相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java 虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7 及其以后版本的 HotSpot 虚拟机选择把静态变量与类型在 Java 语言一端的映射 class 对象存放在一起,存储于 Java 堆之中。

方法区的垃圾回收

有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 zGC 收集器就不支持类卸载)。

一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。

先说说方法区内常量池之中主要存储的两大类常量:字面量和符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:

  • 类和接口的全限定名。
  • 字段的名称和描述符。
  • 方法的名称和描述符。

HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收

回收废弃常量与回收 Java 堆中的对象非常类似。

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGI,JSP的重载等。否则通常很难达成。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法。

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而不是和对象一样,没有引用了就必然会回收。关于是否对类型进行回收,Hotspot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading-XX:+TraceClassUnloading查看类加载和卸载的信息。

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP和OSGi这类频繁自定义加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

总结

在这里插入图片描述


http://www.niftyadmin.cn/n/3682133.html

相关文章

yperLink控件、LinkButton控件 之间的异同

yperLink控件、LinkButton控件 之间的异同 对于Web访问者而言&#xff0c; HyperLink、LinkButton控件是一样的&#xff0c; 但它们在功能方面仍然有较大的差异。 当用户点击控件时&#xff1a; HyperLink控件 会立即将用户“导航”到目标URL&#xff0c;表件不会回送到服务器上…

使用sharding-jdbc实现水平分库分表和读写分离

使用 Sharding-Jdbc 实现 读写分离和水平分表 服务器准备 我们克隆四台虚拟机 【 可参考克隆虚拟机】。ip地址分别为&#xff1a; 192.168.17.123192.168.17.124192.168.17.125192.168.17.126 在四台机器上分别按照好mysql。【可以现在一台服务器上按照好&#xff0c;然后克隆…

ImageButton控件

扩展ImageButton控件定制自己需要的功能 虽然现在网上可以找到n多第三方控件&#xff0c;可我总是看那些单独的dll不爽&#xff0c;在微软提供的标准控件无法满足实际需求时&#xff0c;大多采取扩展标准控件定制个性功能的方法解决&#xff0c;本文描述了给ImageButton控件增加…

综合案例SpringBoot+shiro+Quartz+RabbitMq+Redis实现自动化订单管理系统+部署和源码

前端LayUI ECharts 后端&#xff1a;SpringBoot&#xff0c;MyBatisPlus&#xff0c;Mysql&#xff0c;Redis&#xff0c;Shiro&#xff0c;Quartz&#xff0c;Swagger2, RabbitMQ 技术&#xff1a;SpringBoot整合上述插件实现权限管理&#xff0c;定时任务&#xff0c;Myba…

Linkbutton控件

Linkbutton控件在项目中的简单应用我们知道&#xff0c;在web控件中有一组用于表单提交和回传的控件&#xff0c;即Button控件。这类控件用于将带有用户输入值的页面提交给服务器&#xff0c;以便用页面中的代码对这些值进行处理。它会在服务器上产生一个Click事件&#xff0c;…

博客导航 -- Spring+SpringMVC+Mybatis+SpringCloud

文章目录SpringCloudAlibabaSpring SpringMVC 教程Mybatis 教程最后注意&#xff1a;可以提供PDF原稿SpringCloudAlibaba Windows安装单机版NacosDocker安装单机版NacosLinux安装单机版NacosNacos服务注册和发现Ribbon和RestTemplate负载均衡Feign服务调用No Feign Client fo…

TextBox组件

TextBox组件&#xff08;文本框组件&#xff09;是一种常用的&#xff0c;也是比较容易掌握的组件。应用程序主要使用它来接收使用者于输入文字信息。在前面内容中已经或多或少的接触到TextBox组件。本节就来详细探讨一下Visual Basic .Net中TextBox组件的使用方法。    一.T…

checkboxlist控件的使用

checkboxlist控件的使用 /// <summary> /// 获取CheckBoxList复选框组中被选中的2.并生成0和1组成的字符串 /// </summary> /// <param name"cblName">CheckBoxList的实例</param> /// <returns>返回一个0和1组成的字…