关于java:JVM运行时数据区概述

6次阅读

共计 20396 个字符,预计需要花费 51 分钟才能阅读完成。

运行时数据区概述

程序计数器(Program Counter Register)

是一块较小的内存空间,能够看作是以后线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。

虚拟机栈 (Stack Area)

栈是线程公有, 栈帧是栈的元素。每个办法在执行时都会创立一个栈帧。栈帧中存储了局部变量表、操作数栈、动静连贯和办法进口等信息。每个办法从调用到运行完结的过程,就对应着一个栈帧在栈中压栈到出栈的过程。

本地办法栈 (Native Method Area)

JVM 中的栈包含 Java 虚拟机栈和本地办法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 办法服务,本地办法栈则为 JVM 应用到的 Native 办法服务。

堆 (Heap Area)

堆是 Java 虚拟机所治理的内存中最大的一块存储区域。堆内存被所有线程共享。次要寄存应用 new 关键字创立的对象。所有对象实例以及数组都要在堆上调配。垃圾收集器就是依据 GC 算法,收集堆上对象所占用的内存空间。

Java 堆分为年老代(Young Generation)和老年代(Old Generation);年老代又分为伊甸园(Eden)和幸存区(Survivor 区);幸存区又分为 From Survivor 空间和 To Survivor 空间。

办法区(Method Area)、元空间区(MetaSpace)

办法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码。更具体的说,动态变量 + 常量 + 类信息(版本、办法、字段等)+ 运行时常量池存在办法区中。常量池是办法区的一部分。

JDK 8 应用元空间 MetaSpace 代替办法区,元空间并不在 JVM 中,而是在本地内存中

在运行时数据区中包含那几个区域?

1、线程公有区域:1. 程序计数器 2. 虚拟机栈 3. 本中央办法栈

2、线程共享区域:4. 办法区(元空间)5. 堆

JVM 中的线程阐明

线程是一个程序中的运行单元,JVM 容许一个利用有多个线程并行的执行工作。

在 Hotspot JVM 中,每个线程都与操作系统的本地线程之间映射,当一个 Java 线程筹备好执行后,此时一个操作系统的本地线程也会同时创立,Java 线程执行终止后,本地线程也会回收。

操作系统负责所有线程的安顿调度到任何一个可用的 CPU 上。一旦本地线程初始化胜利,他就会调用 Java 线程的 run()办法。

JVM 线程的次要几类:

  • 虚拟机线程: 这种线程的操作是须要 JVM 达到平安点才会呈现,这些操作必须在不同的线程中产生的起因是它们都要达到平安点,这样堆才不会发生变化。这种线程的执行类型包含 ”stop-the-world” 的垃圾收集,线程栈收集,线程挂起以及偏差锁的撤销。
  • 周期工作线程: 这种线程是工夫周期事件的体现(比方中断),他们个别用于周期性操作的调度执行。
  • GC 线程: 这种线程对在 JVM 中不同类的垃圾收集行为提供了反对。
  • 编译线程: 这种线程在运行时会将字节码编译成本地代码。
  • 信号调度线程: 这种线程接管信号并发送给 JVM,在它外部通过调用适当的办法进行解决。

PC 寄存器(PC Register)

PC 寄存器介绍

JVM 中的程序计数器(Program Counter Register)中,Register 的命名源于 CPU 的寄存器,寄存器存储指令相干的现场信息。CPU 只有把数据装载到寄存器能力运行。

这里,并非狭义上所指的物理寄存器,或者将其翻译为 PC 寄存器 (或指令计数器) 会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种形象模仿。

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

  • 他是一块很小的内存空间,也是运行速度最快的存储区域。
  • 在 JVM 标准中,每个线程都有它本人的 PC 寄存器,是 线程公有 的,生命周期与线程的生命周期保持一致。
  • 任何工夫一个线程都只有一个办法执行,也就是所谓的 以后办法。PC 寄存器会存储以后线程正在执行的 Java 办法的 JVM 指令地址,如果执行的 native 办法,则是 undefined。
  • 它是程序控制流中的指示器、分支、循环、跳转、异样解决、线程复原等根底性能都须要 PC 寄存器来实现。
  • 字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令。
  • 他是惟一一个在 Java 虚拟机标准中,没有规定任何 OutOfMemoryError 的区域。

应用举例

public class PCRegister {public static void main(String[] args) {
        int i = 20;
        int j = 30;
        int k = i + j;
        String str = "hello";
        System.out.println(str);
    }

}

咱们应用 jclasslib 看一下编译后:

左侧是数字其实就是偏移地址,PC 寄存器就是存储的这个,而右侧就是指令。

后面操作比较简单,其实就是将常量值 20 压入栈而后存入索引 1 的地位,而后将常量值 30 压入栈而后存入索引 2,而后取出 1,2,相加之后存入索引 3。

咱们重点说一下前面的操作,偏移地址 10 的地位。

ldc: 将 int, float 或 String 型常量值从常量池中推送至栈顶。
而前面的 #2 的地位从下图常量池中,咱们能够看到对应的是 String,他又关联了 #27,#27 对应的 UTF-8 字符串为:hello。存入索引 4 的地位。然而咱们发现偏移地址从 10 跳到了 12,就是因为咱们在 ldc 中执行了两步操作。

getstatic: 获取动态变量援用,并将其援用推到操作数栈中。
咱们能够看到他对应的常量池 #3 对应的属性 #28.#29 两个,
#28对应的是 Class 找到 34 咱们能够看到是 java.lang.System,#29对应了#35#36,也就是 out 和 printStream。

而后读取 aload 4 也就是 str 的值进行输出,最初 return 完结。

局部变量表,操作数栈都是由执行引擎来操作的,再翻译成机器指令来操作 cpu。

问题:应用 PC 寄存器存储字节码指令地址有什么用?为什么应用 PC 寄存器存储?

因为 CPU 须要不停的切换各个线程,这个时候切换回来后,须要晓得从哪里接着继续执行。

JVM 的字节码解释器就须要通过扭转 PC 寄存器的值来明确下一条应该执行什么样的字节码指令。

问题:为什么是线程公有?

多线程在一个特定的时间段内指挥执行其中某一个线程的办法,CPU 会不停地做工作切换,这必然会导致常常中断或者复原。

简略来说就是不便各个线程之间能够独立计算,不会呈现互相烦扰的问题。

虚拟机栈

概述

每个线程都会有一个虚拟机栈,多线程就会有多个虚拟机栈。是 线程公有,虚拟机栈外面是一个一个的栈帧(Stack Frame),每一个栈帧都是在办法执行的同时创立的,形容的是 Java 办法执行的内存模型。每一个办法从调用开始至执行实现的过程,都对应着一个栈帧在虚拟机外面从入栈到出栈的过程。栈是先进后出的。

作用是主管 Java 程序的运行,它保留办法的局部变量、局部后果、并参加办法的调用与返回。

在流动线程中,只有一个栈帧是处于沉闷状态的,也就是说只有位于栈顶的栈帧才是无效的,称为以后栈帧,与这个栈帧相关联的办法称为以后办法。

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

长处:跨平台,指令集小,编译器容易实现。

毛病:性能降落,实现同样的性能须要更多的指令。

虚拟机栈可能抛出的异样

  • 若是固定大小的 JAVA 虚拟机栈,那每一个线程的 JAVA 虚拟机栈容量能够在线程创立的时候独立选定,如果线程申请调配的栈容量超过 JAVA 虚拟机栈容许的最大容量,JAVA 虚拟机将会抛出一个 StackOverflowError 异样
  • 若是 JAVA 虚拟机栈能够动静扩大,并且在尝试扩大时的时候无奈申请到足够的内存,或者在创立新的线程时没有足够的内存去创立相应的虚拟机栈,那 JAVA 虚拟机将会抛出一个 OutofMemroyError 异样。

解决方案:

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

在启动参数退出 -Xss256k 或者随便大小。

栈的存储单位

  • 每个线程都有本人的栈,栈中的数据都是以栈帧(Stack Frame)的格局存在。
  • 在这个线程上正在执行的每个办法都各自对应一个栈帧(Stack Frame)。
  • 栈帧是一个内存区块,是一个数据集,维系着办法执行过程中的各种数据信息。

栈运行原理

  • 虚拟机栈的操作只有两个就是 压栈 出栈 ,遵循 后进先出 准则。
  • 在一条流动线程中,一个工夫点上,只会有一个流动的栈帧,即只有以后正在执行的办法的栈帧(位于栈顶)是无效的,这个栈帧被称为 以后栈帧(Current Frame),定义这个办法的类就是 以后类(Current Class)
  • 执行引擎运行的所有字节码指令只针对以后栈帧进行操作。
  • 如果在该办法调用了其余办法,对应新的栈帧就会被创立进去,压栈后成为新的以后栈帧。
  • 不同线程中所蕴含的栈帧是不容许存在互相援用的,即不可能在一个栈帧中援用另一个线程的栈帧。
  • 如果以后办法调用了其余办法,办法返回之际,以后栈帧会传回办法的执行后果给前一个栈帧,而后 JVM 抛弃掉以后栈帧,之后前一个栈帧变为栈顶的栈帧。
  • Java 有两种返回函数的形式,一种是失常函数返回,一种是抛出异样返回,不论哪一种都会导致栈帧弹出。

栈帧的内部结构

分为五大类:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)
  • 动静链接(Dynamic Linking)
  • 办法返回地址(Return Address)
  • 一些附加信息

局部变量表(Local Variables)

  1. 局部变量表也被称之为局部变量数组或本地变量表。
  2. 定义为一个数字数组,次要用于存储办法参数和定义在方体内的局部变量,这些数据蕴含根本数据类型,对象援用,以及 returnAddress 类型。
  3. 因为局部变量表是建设在线程的栈上,是线程的公有数据,因而 不存在数据的平安问题
  4. 局部变量表所需的容量大小是在编译期间确定下来的,并保留在办法的 Code 属性的 maximum local variables 数据项中。在办法运行期间是不会扭转局部变量表大小的。
  5. 办法嵌套调用的次数由栈的大小来决定。一般来说,栈越大,办法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表收缩,它的栈帧就越大,以满足办法调用所需传递的信息赠的需要。进而函数调用就会占用更多的栈空间,导致其嵌套的次数就会缩小。
  6. 局部变量表中的变量只在以后办法调用中无效。在办法执行时,虚拟机通过应用局部变量表实现参数值到参数变量列表的传递过程。当办法调用完结后,随着办法栈帧的销毁,局部变量表也会随之销毁。
  7. 局部变量表中最根本的存储单元是 Slot(变量槽)

对于 Slot 的了解

  1. 在局部变量表中,32 位以内的类型占一个 Slot,64 位的类型占用两个 Slot。
  2. JVM 会为局部变量表中的每一个 Slot 都调配一个拜访索引,通过这个索引即可胜利拜访到局部变量表中指定的局部变量值。
  3. 当一个实例办法被调用的时候,它的办法参数和办法体外部定义的局部变量将会依照 程序 被复制到局部变量表中的每一个 Slot 上。
  4. 如果须要拜访局部变量表中的一个 64 位的局部变量值时,只须要应用前一个索引即可。
  5. 如果以后帧是由构造方法或者实例办法创立的,那么该对象的援用 this 将会寄存在 index 为 0 的 Slot 处,其余参数依照顺序排列。

代码小例子:

public String test(Date dateP,String name2){
        dateP = null;
        name2 = "Jack";
        double weight = 1.1;
        char gender = '男';
        return dateP + name2;
}

咱们应用 jclasslib 来看的话能够看到 Index 也就是 Slot,咱们发现 3 也就是 double 是占了两个 Slot 的。

操作数栈

  • 每一个独立的栈帧中除了蕴含局部变量表以外,还蕴含一个 后进先出 的操作数栈,也称之为 表达式栈
  • 操作数栈,在办法执行的过程中,依据字节码指令、往栈中写入或取出数据,即入栈 / 出栈
  • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,进行操作之后再将后果压入栈。
  • 比方:复制、替换、求和等操作。

  • 如果被调用的办法带有返回值的话,其返回值将会被压入以后栈帧的操作数栈中,并更新 PC 寄存器中下一条须要执行的字节码指令。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类测验阶段的数据流分析阶段要再次验证。
  • 操作数栈,次要用于保留计算过程的两头后果,同时作为计算过程中变量长期的存储空间。
  • 操作数栈是 JVM 执行引擎的一个工作区,当一个办法开始执行的时候,一个新的栈帧就会随之创立进去,这个时候办法的操作数栈是空的。
  • 每一个操作数栈都会有一个明确的栈深度用于存储数值,其所需的最大深度在编译期间就曾经定义好了,保留在办法的 Code 属性中,为 max_stack 的值。
  • 栈中任意一个元素都能够是任意的 Java 数据类型。

    • 32bit 占用一个栈单位深度
    • 64bit 占用二个栈单位深度

操作数栈的字节码指令剖析

首先咱们创立如下简略的代码:

public class OperandStackTest {public void testAddOperand(){
        byte i = 15;
        int j = 8;
        int k = i + j;

    }

}

应用 jclasslib 反编译当前咱们看到如下指令:

 Code:
    stack=2, locals=4, args_size=1
        0 bipush 15
        2 istore_1
        3 bipush 8
        5 istore_2
        6 iload_1
        7 iload_2
        8 iadd
        9 istore_3
        10 return

在标注灰色的中央,咱们看一看到指令地址是 0,所以右侧的 PC 寄存器就是 0,bipush 操作就是将常量值 15 存入咱们的操作数栈的栈顶,当初局部变量表中还是初始化的状态。

当指令执行到了 2 的地位,PC 寄存器里寄存的就是 2,执行的 istore 指令,将操作数栈中数据取出存入对应的局部变量表中。

当指令执行到了 3 的地位,PC 寄存器里寄存的就是 3,bipush 操作就是将常量值 8 存入咱们的操作数栈的栈顶,当初局部变量表中只有对应下标为 i 的值为 15。

当指令执行到了 5 的地位,PC 寄存器里寄存的就是 5,执行的 istore 指令,将操作数栈中数据取出存入对应的局部变量表中。

当指令执行到了 6 的地位,PC 寄存器里寄存的就是 6,执行的 iload 指令,将局部变量表中的数据取出存入操作数栈的栈顶。(指令地址 7 也同理)

当指令执行到了 8 的地位,PC 寄存器里寄存的就是 8,执行的 iadd 指令,将栈顶的两个数据取出进行相加,将后果存入操作数栈栈顶。(相加操作由执行引擎将字节码指令来翻译成机器指令来操作 cpu。)

当指令执行到了 9 的地位,PC 寄存器里寄存的就是 9,执行的 istore 指令,将栈顶的元素取出存入对应的局部变量表中。

stack=2, locals=4, args_size=1

stack 对应的就是咱们的操作数栈的深度。

locals 对应的就是咱们的局部变量表的长度。

args_size 对应的就是参数的长度,动态代码块为 0。

动静链接(或指向运行时常量池的办法援用)

  • 每一个栈帧外部都蕴含一个指向 运行时常量池 中栈帧所属办法的援用。蕴含这个援用的目标就是为了反对以后办法的代码可能实现 动静链接。比方:invokedynamic 指令
  • 在 Java 源文件中被编译到字节码文件中时,所有的变量和办法援用都作为符号援用(Symbolic Reference)保留在 class 文件的常量池中指向办法的符号援用来示意的,那么 动静链接的作用就是为了将这些符号援用转换为调用办法的间接饮用
public class DynamicLinkingTest {

    int num = 10;

    public void methodA() {System.out.println("methodA...");
    }

    public void method() {System.out.println("methodB...");
        methodA();
        num++;
    }
}

应用 jclasslib 反编译当前咱们看到如下指令:

 Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method a:()V
         4: return

invokevirtual 前面的 #2 符号援用对应的就是 Constant pool 中的间接援用。
#2对应了办法援用,#3#17……最终对应到办法 A()

Constant pool:
   #1 = Methodref          #4.#16         // java/lang/Object."<init>":()V
   #2 = Methodref          #3.#17         // com/suanfa/jvm/OperandStackTest.a:()V
   #3 = Class              #18            // com/suanfa/jvm/OperandStackTest
   #4 = Class              #19            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Lcom/suanfa/jvm/OperandStackTest;
  #12 = Utf8               a
  #13 = Utf8               b
  #14 = Utf8               SourceFile
  #15 = Utf8               OperandStackTest.java
  #16 = NameAndType        #5:#6          // "<init>":()V
  #17 = NameAndType        #12:#6         // a:()V
  #18 = Utf8               com/suanfa/jvm/OperandStackTest
  #19 = Utf8               java/lang/Object

办法调用

在 JVM 中,将符号援用转换为调用办法的间接援用与办法的绑定机制无关。

动态链接

当一个字节码文件被装载进 JVM 外部时,如果被调用的 指标办法在编译期可知,且运行期间放弃不变时。这种状况下将调用办法的符号援用转换为间接援用的过程称之为动态链接。

动静链接

如果 被调用办法在编译期间无奈被确定下来,只能在程序运行时将调用办法的符号援用转换为间接援用,因为这种援用转换的过程具备动态性,因而也就被称为动静链接。

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

晚期绑定

晚期绑定就是指被调用的 指标办法如果在编译期可知,且运行期放弃不变 时,即可将这个办法所属的类型进行绑定,这样一来,因为明确了被调用办法到底是哪一个,因而也就能够应用动态链接的形式将符号援用替换为间接援用。

早期绑定

如果 被调用的办法在编译期无奈被确定下来,只可能在程序运行期依据理论的类型绑定相干办法 这种绑定就叫做早期绑定。

办法的调用:虚办法与非虚办法

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

动态变量、公有办法、final 办法、实例结构器、父类办法都是非虚办法。

其余办法称之为虚办法、

一般调用指令:

  1. invokestatic:静态方法,解析阶段确定惟一办法版本
  2. invokespecial:调用 <init> 办法、公有办法以及父类办法,解析阶段确定惟一办法版本
  3. invokevirtual:调用所有虚办法
  4. invokeinterface:调用接口办法

动静调用指令:

  1. invokedynamic:动静解析所须要调用的办法,而后执行

前四条指令固化在虚拟机的外部,办法的调用执行不可人为干涉,而 invokedynamic 指令则反对由用户确定版本。其中 invokevirtual 和 invokestatic 指令调用的办法称为非虚办法,其余的(final 润饰除外)称为虚办法。

/**
 * 解析调用中非虚办法、虚办法的测试
 */
class Father {public Father(){System.out.println("Father 默认结构器");
    }

    public static void showStatic(String s){System.out.println("Father show static"+s);
    }

    public final void showFinal(){System.out.println("Father show final");
    }

    public void showCommon(){System.out.println("Father show common");
    }

}

public class Son extends Father{public Son(){super();
    }

    public Son(int age){this();
    }

    public static void main(String[] args) {Son son = new Son();
        son.show();}

    // 不是重写的父类办法,因为静态方法不能被重写
    public static void showStatic(String s){System.out.println("Son show static"+s);
    }

    private void showPrivate(String s){System.out.println("Son show private"+s);
    }

    public void show(){
        //invokestatic
        showStatic("大头儿子");
        //invokestatic
        super.showStatic("大头儿子");
        //invokespecial
        showPrivate("hello!");
        //invokespecial
        super.showCommon();
        //invokevirtual 因为此办法申明有 final 不能被子类重写,所以也认为该办法是非虚办法
        showFinal();
        // 虚办法如下
        //invokevirtual
        showCommon();// 没有显式加 super,被认为是虚办法,因为子类可能重写 showCommon
        info();

        MethodInterface in = null;
        //invokeinterface  不确定接口实现类是哪一个 须要重写
        in.methodA();}

    public void info(){}

}

interface MethodInterface {void methodA();
}

invokedynamic 指令

  • invokedynamic 指令是在 JDK7 中减少的,为了实现动静类型语言反对而做的一种改良。
  • 然而在 JDK7 中并没有提供间接生成的 invokedynamic 指令的办法,须要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。直到 JDK8 的 Lambda 表达式的呈现,invokedynamic 指令的生成,在 Java 中才有了间接生成的形式。

办法返回地址(Return Address)

  • 寄存调用该办法的 PC 寄存器的值。
  • 一个办法的完结要么失常执行完结,要么呈现未解决异样,非正常退出。
  • 无论哪种形式退出,在办法退出后都返回到该办法被调用的地位。办法失常退出时,调用者的 PC 寄存器的值作为返回地址,即调用该办法的下一条指令地址,而通过异样退出的,返回地址是要通过异样表来确定,栈帧中不会保留这部分信息。

区别在于,通过异样实现的进口不会给他下层调用者产生任何的返回值

栈的相干面试题

  1. 举例栈溢出的状况?

栈帧寄存空间有余导致呈现 StackOverflowError 异样。通过 -Xss 设置栈的大小。

  1. 调整栈的大小,就能保障不呈现溢出吗?

不能保障。

  1. 调配的栈内存越大越好吗?

不是。在同一台机器上,如果 jvm 设置的内存过大,就会导致其它程序所占用的内存小。比方 elasticsearch、kafka,尽管它们都是基于 jvm 运行的程序(java 和 scala 都是依赖于 jvm),然而它们的数据不是放到 jvm 内存中,而是放到 os cache 中(操作系统治理的内存区域),防止了 jvm 垃圾回收的影响。

  1. 垃圾回收是否会波及到虚拟机栈?

    不会

    运行时数据区 Error GC
    程序计数器 × ×
    虚拟机栈 ×
    本地办法栈 ×
    办法区
  1. 办法中定义的局部变量是否线程平安?

不肯定,可能会产生办法逃逸。

public StringBuilder escapeDemo1() {StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("a");
    stringBuilder.append("b");
    return stringBuilder;
}

办法逃逸:在一个办法体内,定义一个局部变量,而它可能被内部办法援用,比方作为调用参数传递给办法,或作为对象间接返回。或者,能够了解成对象跳出了办法。

本地办法栈

什么是本地办法?

一个 Native Method 是这样的 Java 办法:该办法的实现由非 Java 语言实现,比方 C。

本地办法的作用就是为了交融不同编程语言为 Java 所用。

应用 native 关键字润饰的办法就是本地办法。

本地办法栈简介

  • Java 虚拟机栈用于治理 Java 办法的调用,而本地办法栈用于治理本地办法的调用。
  • 本地办法栈也是线程公有的。
  • 容许被实现成固定或者可动静扩大的内存大小。
  • 本地办法是用 C 语言实现的。
  • 具体做法就是 Native Method Stack 中注销 native 办法,在 Execution Engine 执行时加载本地办法库。

  • 当某个线程调用一个本地办法时,它就进入了一个全新的并且不再受虚拟机限度的世界。它和虚拟机领有同样的权限。
  1. 本地办法能够通过本地办法接口来拜访虚拟机外部的运行时数据区。2. 它甚至能够间接应用本地处理器中的寄存器。3. 间接从本地内存的堆中调配任意数量的内存。

概述

  • 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存治理的外围区域。
  • Java 堆区在 JVM 启动的时候即被创立,其空间大小也就确定了。是 JVM 治理的最大一块内存空间。
  • 堆内存的大小是能够调节的。

《Java 虚拟机标准》规定,堆能够处于物理. 上不间断的内存空间中,但在逻辑上它应该被视为间断的。

  • 所有的线程共享 Java 堆,在这里还能够划分线程公有的缓冲区(ThreadLocal Allocation Buffer,TLAB)。
  • 《Java 虛拟机标准》中对 Java 堆的形容是: 所有的对象实例以及数组都该当在运行时调配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated)
  • 我要说的是: 简直”所有的对象实例都在这里分配内存。–从理论. 应用角度看的。

数组和对象可能永远不会存储在栈,上,因为栈帧中保留援用,这个援用指向对象或者数组在堆中的地位。

  • 在办法完结后,堆中的对象不会马. 上被移除,仅仅在垃圾收集的时候才会被移除。
  • 堆,是 GC (Garbage Collection, 垃圾收集器)执行垃圾回收的重点区域。

堆的外围概述

堆空间大小的设置

Java 堆区用于存储 Java 对象实例,那么堆的大小在 JVM 启动时就曾经设定好了,能够通过选项 ”-Xmx” 和 ”-Xms” 来进行设置。

  • “-Xms” 用 于示意堆区的起始内存,等价于 -XX: InitialHeapSize
  • “-Xmx” 则用于示意堆区的最大内存,等价于 -XX :MaxHeapSize
  • 一旦堆区中的内存大小超过“-Xmx” 所指定的最大内存时,将会抛出 OutOfMemoryError 异样。
  • 通常会将 -Xms 和 -Xmx 两个参数配置雷同的值,其目标是为了可能在 java 垃圾回收机制清理完堆区后不须要从新分隔计算堆区的大小,从而进步性能。
  • 默认状况下,初始内存大小: 物理电脑内存大小 / 64,最大内存大小: 物理电脑内存大小 / 4。

年老代和老年代

存储在 JVM 中的 Java 对象能够分为两类:

  • 一类是生命周期较短的刹时对象,这类对象的创立和沦亡都十分迅速。
  • 另外一类对象的生命周期较长,在某些极其的状况下还可能与 JVM 的生命周期保持一致。

Java 堆区进一步细分的话,能够划分为年老代(YoungGen)和老年代(OldGen)

其中年老代又划分为 Eden 空间、Survivor0 和 Survivor1 空间(也叫做 from 区、to 区)。

在 Hotspot 中,Eden 空间和 Survivor0 和 Survivor1 空间缺省比例是 8:1:1。

也能够应用 ”-XX:SurvivorRatio” 调整这个空间比例。比方 -XX:SruvivorRatio=8。

简直所有的 Java 对象都在 Eden 区被 new 进去的。

对象的调配过程

为新对象分配内存是一件十分谨严和简单的工作,JVM 的设计者们不仅须要思考内存如何调配、在哪里调配等问题,并且因为内存调配算法与内存回收算法密切相关,所以还须要思考 GC 执行完内存回收后是否会在内存空间中产生内存碎片。

  1. new 的对象先放在 Eden 区,此区域有大小限度。
  2. 当 Eden 的空间填满时,程序又须要创立新的对象,JVM 的垃圾回收器将对 Eden 区不再被其余对象所援用的对象进行销毁。再加载新的对象放到 Eden。
  3. 而后将 Eden 区残余的对象挪动到 Survivor0 区。

  1. 如果再次触发垃圾回收,此时上次幸存下来的放在 Survivor0 区,如果没有回收,就会放到 Survivor1 区。

  1. 如果再次经验垃圾回收,此时会从新放回 Survivor0 区,接着再去 Survivor1 区。
  2. 当 ” 年龄 ” 达到 15 之后就会被放到 old 区。能够设置参数:-XX:MaxTenuringThreshold=<N>

  1. 当 old 区内存不足时,再次触发 GC:Major GC,进行 old 区内存清理。
  2. 如果 old 区在进行了 GC 后仍然无奈进行对象的保留,就会产生 OOM 异样。

对于垃圾回收:频繁在新生区收集,很少在养老区收集,简直不在永恒区 / 元空间收集。

分配内存的非凡状况

如果对象一开始就过大,如果 Eden 区放不下的话会间接放入 old 区。

如果 old 区也放不下,则会产生 Full GC。如果 GC 后还是放不下则会报错 OOM。

Minor GC、Major GC、Full GC 比照

JVM 在进行 GC 时,并非每次都对下面三个内存(新生代、老年代;办法区 / 元空间)区域一起回收的,大部分时候回收的手是指新生代。

针对 Hotspot VM 的实现,它外面的 GC 依照回收区域又分为两大类,一种是局部收集(Partial GC),一种是残缺收集(Full GC)。

  • 局部收集

    • 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集。
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
目前只有 CMS GC 会有独自收集老年代的行为。` 留神,很多时候 Major GC 会和 Full GC 混同应用,须要具体分辨是老年代回收还是整堆回收。`

- 混同收集(Mixed GC):收集整个新生代以及局部老年代的垃圾收集。目前只有 G1 GC 会有这种行为。
  • 整堆收集(Full GC):收集整个 Java 堆和办法区的垃圾收集。

年老代 GC(Minor GC)触发机制

当年老代空间有余时候,就会触发 Minor GC,这里的年老代满指的是 Eden 区满,Survivor 满不会触发 GC。(每次 Minor GC 就会清理 Eden 区内存)

因为 Java 对象大多都具备朝生夕灭的个性,所以 Minor GC 十分频繁,个别回收速度也比拟快。

Minor GC 会引发 STW,暂停其它用户线程,等垃圾回收完结,用户线程才会复原运行。

老年代 GC(Major GC/Full GC)触发机制

指产生在老年代的 GC,对象从老年代隐没时,咱们说”Major GC“或者”Full GC”产生了。

呈现了 Major GC,常常会随同至多一次 Minor GC(但非相对的,在 Parallel Scavenge 收集器的收集策略里就有间接进行 Major GC 的策略抉择过程)。

也就是在老年代空间有余时,先尝试触发 Minor GC,如果之后空间还是有余,则触发 Major GC。

Major GC 的速度个别会比 Minor GC 慢 10 倍以上,STW 的工夫更长。

如果 Major GC 后内存还有余就会报 OOM 了。

Full GC 触发机制

  1. 调用 System.gc()时,零碎倡议执行 Full GC,然而不必然执行。
  2. 老年代空间有余
  3. 办法区空间有余
  4. 通过 Minor GC 后进入老年代的均匀大小大于老年代的可用内存
  5. 由 Eden 区、Survivor0 向 Survivor1 区复制时,对象大小大于 Survivor1 可用内存,则把对象转移到老年代,且老年代的可用内存小于该对象大小时。

阐明:在开发中尽量避免 Full GC,这样 STW 工夫会更短

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 空间中分配内存。

堆是调配对象存储的惟一抉择吗?

  • 在 Java 虚拟机中,对象是在 Java 堆中分配内存的,这是一个广泛的常识。然而,有一种非凡状况,那就是 如果通过逃逸剖析(Escape Analysis)后发现,一个对象并没有逃逸出办法的话,那么就可能被优化成栈上调配。这样就无需在堆上分配内存,也毋庸进行垃圾回收了。这也是最常见的堆外存储技术。
  • 此外,基于 OpenJDK 深度定制的 TaoBaoVM,其中翻新的 GCIH(GC invisible heap)技术实现 off-heap,将生命周期较长的 Java 对象从 heap 中移至 heap 外,并且 GC 不能治理 GCIH 外部的 Java 对象,以此达到升高 Gc 的回收频率和晋升 GC 的回收效率的目标。

逃逸剖析伎俩

  • 如何将堆上的对象调配到栈,须要应用逃逸剖析伎俩
  • 这是一种能够无效缩小 Java 程序中同步负载和内存堆调配压力的跨函数全局数据流剖析算法。
  • 通过逃逸剖析,Java Hotspot 编译器可能剖析出一个新的对象的援用的应用范畴从而决定是否要将这个对象调配到堆上。

逃逸剖析的根本行为就是剖析对象动静作用域:

  • 当一个对象在办法中被定义后,对象只在办法外部应用,则认为没有产生逃逸。
  • 当一个对象在办法中被定义后,它被内部办法所援用,则认为产生逃逸。例如作为调用参数传递到其余中央中。

论断:开发中能用局部变量的,就不要应用在办法外定义。

办法区

运行时数据区图解

栈、堆、办法区、的交互关系

办法区根本了解

《Java 虚拟机标准》中明确阐明:“只管所有的办法区在逻辑上是属于堆的一部分,但一些简略的实现可能不会抉择去进行垃圾收集或者进行压缩。”但对于 HotspotJVM 而言,办法区还有一个别名叫做 Non-Heap,目标就是要和堆离开。

所以 办法区看作是一块独立于 Java 堆的内存空间。

  • 办法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域。
  • 办法区在 JVM 启动的时候被创立,并且它的理论的物理内存空间中和 Java 堆区一样都能够是不间断的。
  • 办法区的大小,和堆空间一样,能够抉择固定大小和可扩大。
  • 办法区的大小决定了零碎能够保留多少个类,如果零碎定义了太多的类,导致办法区溢出,虚拟机就会抛出内存溢出谬误:java.lang.OutOfMemoryError:PermGen space 或者 java.lang.OutOfMemoryError: Metaspace。
  • 敞开 JVM 就会开释这个区域的内存。

设置办法区内存大小

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 值为 21MB. 这就是初始的高水位线,一旦涉及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),而后这个高水位线将会重置。新的高水位线的值取决于 GC 后开释了多少元空间。如果开释的空间有余,那么在不超过 MaxMetaspaceSize 时,适当进步该值。如果开释空间过多,则适当升高该值。
  • 如果初始化的高水位线设置过低,上述高水位线调整状况会产生很屡次。通过垃圾回收器的日志能够察看到 Full GC 屡次调用。为了防止频繁地 GC,倡议将 -XX:MetaspaceSize 设置为一个绝对较高的值。
jdk7 及以前:查问 jps  -> jinfo -flag PermSize [过程 id]

-XX:PermSize=100m -XX:MaxPermSize=100m

jdk8 及当前:查问 jps  -> jinfo -flag MetaspaceSize [过程 id]

-XX:MetaspaceSize=100m  -XX:MaxMetaspaceSize=100m

解决报错 OOM:(内存透露、内存溢出)

  1. 要解决 00M 异样或 heap space 的异样,个别的伎俩是首先通过内存映像剖析工具(如 Eclipse Memory Analyzer)对 dump 进去的堆转储快照进行剖析,重点是确认内存中的对象是否是必要的,也就是要先分分明到底是呈现了内存透露(Memory Leak)还是内存溢出(Memory 0verflow)。
  2. 如果是内存透露,可进一步通过工具查看透露对象到 GC Roots 的援用链(堆当中的闲置对象因为援用链的援用关系无奈被回收,尽管它曾经属于闲置的资源)。于是就能找到透露对象是通过怎么的门路与 GCRoots 相关联并导致垃圾收集器无奈主动回收它们的。把握了透露对象的类型信息,以及 GC Roots 援用链的信息,就能够比拟精确地定位出透露代码的地位。
  3. 如果不存在内存透露,换句话说就是内存中的对象的确都还必须存活着,那就该当查看虚拟机的堆参数(一 Xmx 与一 Xms),与机器物理内存比照看是否还能够调大,从代码_上查看是否存在某些对象生命周期过长、持有状态工夫过长的状况,尝试缩小程序运行期的内存耗费。

代码案例:

/**
 * jdk6/ 7 中:* -XX:PermSize=10m -XX:MaxPermSize=10m
 *
 * jdk8 中:* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 *
 */
public class OOMTest extends ClassLoader {public static void main(String[] args) {
        int j = 0;
        try {OOMTest test = new OOMTest();
            for (int i = 0; i < 10000; i++) {
                // 创立 ClassWriter 对象,用于生成类的二进制字节码
                ClassWriter classWriter = new ClassWriter(0);
                // 指明版本号,修饰符,类名,包名,父类,接口
                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);
        }
    }
}

办法区的内部结构

《深刻了解 Java 虚拟机》书中对办法区存储内容形容如下:它用于存储已被虚拟机加载的 类型信息、常量、动态变量、即时编译器编译后的代码缓存 等。

类型信息

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

  1. 这个类型的残缺无效名称(全名 = 包名. 类名)。
  2. 这个类型间接父类的残缺无效名(对于 interface 或是 java. lang.Object,都没有父类)。
  3. 这个类型的修饰符(public,abstract,final 的某个子集)。
  4. 这个类型间接接口的一个有序列表。

域(Field)信息

  1. JVM 必须在办法区中保留类型的所有域的相干信息以及域的申明程序。
  2. 域的相干信息包含:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient 的某个子集)。

办法信息(method)

JVM 必须保留所有办法的以下信息,同域信息一样包含申明程序:

  1. 办法名称。
  2. 办法的返回类型(或 void)。
  3. 办法参数的数量和类型(按程序)。
  4. 办法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)。
  5. 办法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 办法除外)。
  6. 异样表(abstract 和 native 办法除外),每个异样解决的开始地位、完结地位、代码解决在程序计数器中的偏移地址、被捕捉的异样类的常量池索引。

non-final 的类变量(非申明为 final 的 static 动态变量)

  1. 动态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。
  2. 类变量被类的所有实例所共享,即便没有类实例你也能够拜访它。

全局常量(static final)

被申明为 final 的类变量的解决办法则不同,每个全局常量在编译的
时候就被调配了。

public static int count = 1;
public static final int number = 2;

反编译后就能够看到如下代码:

public static int count;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public static final int number;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2

文件中常量池的了解

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

一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码须要数据反对,通常这种数据会很大以至于不能间接存到字节码里,换另一种形式,能够存到常量池;而这个字节码蕴含了指向常量池的援用。在动静链接的时候会用到运行时常量池。

小结:字节码当中的常量池构造(constant pool),能够看做是一张表,虚拟机指令依据这张常量表找到要执行的类名,办法名,参数类型、字面量等信息。

运行时常量池

  • 运行时常量池(Runtime Constant Pool)是办法区的一部分。
  • 常量池表(Constant Pool Table)是 Class 文件的一部分,用于寄存编译期生成的各种字面量与符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中。
  • 运行时常量池,在加载类和接口到虚拟机后,就会创立对应的运行时常量池。
  • JVM 为每个已加载的类型(类或接口)都保护一个常量池。池中的数据项像数组项一样,是通过索引拜访的。
  • 运行时常量池中蕴含多种不同的常量,包含编译期就曾经明确的数值字面量,也包含到运行期解析后才可能取得的办法或者字段援用。此时不再是常量池中的符号地址了,这里换为实在地址。

运行时常量池,绝对于 Class 文件常量池的另一重要特色是:具备动态性。

  • 运行时常量池相似于传统编程语言中的符号表(symbol table),然而它所蕴含的数据却比符号表要更加丰盛一些。
  • 当创立类或接口的运行时常量池时,如果结构运行时常量池所需的内存空间超过了办法区所能提供的最大值,则 JVM 会抛 OutOfMemoryError 异样。

办法区的演进细节

首先明确:只有 HotSpot 才有永恒代。BEA JRockit、IBM J9 等来说,是不存在永恒代的概念的。原则上如何实现办法区属于虛拟机实现细节,不受《Java 虚拟机标准》管教,并不要求对立。

Hotspot 中 办法区的变动:

  • jdk1.6 及之前:有永恒代(permanent generation),动态变量寄存在 永恒代上。
  • jdk1.7:有永恒代,但曾经逐渐“去永恒代”,字符串常量池、动态变量移除,保留在堆中。
  • jdk1.8 及之后:无永恒代,类型信息、字段、办法、常量保留在本地内存的元空间,但字符串常量池、动态变量仍留在堆空间。

永恒代为什么要被元空间替换

  • 随着 Java8 的到来,HotSpot VM 中再也见不到永恒代了。然而这并不意味着类. 的元数据信息也隐没了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。
  • 因为类的元数据调配在本地内存中,元空间的最大可调配空间就是零碎可用内存空间。

这项改变是很有必要的,起因有:

  1. 为永恒代设置空间大小是很难确定的。在某些场景下,如果动静加载类过多,容易产生 Perm 区(永恒代)的 O0M。比方某个理论 Web 工程中,因为性能点比拟多,在运行过程中,要一直动静加载很多类,经常出现致命谬误。“Exception in thread’ dubbo client x.x connector’java.lang.OutOfMemoryError:PermGenspace” 而元空间和永恒代之间最大的区别在于:元空间并不在虚拟机中,而是应用本地内存。因而,默认状况下,元空间的大小仅受本地内存限度。
  2. 对永恒代进行调优是很艰难的。

StringTable 为什么要调整

  • jdk7 中将 StringTable 放到了堆空间中,正确。
  • 因为永恒代的回收频率很低,在 Full GC 的时候才会触发。而 Full GC 是老年代的空间有余、永恒代有余时才会触发。这就导致了 StringTable 回收效率不高。而咱们开发中会有大量的字符串被创立,回收效率低,导致永恒代内存不足。放到堆里,能及时回收内存.

办法区的垃圾回收

有些人认为办法区(如 Hotspot,虚拟机中的元空间或者永恒代)是没有垃圾收集行为的,其实不然。《Java 虚拟机标准》对办法区的束缚是十分宽松的,提到过能够不要求虚拟机在办法区中实现垃圾收集。事实上也的确有未实现或未能残缺实现办法区类型卸载的收集器存在(如 JDK11 期间的 ZGC 收集器就不反对类卸载)。

一般来说这个区域的回收成果比拟难令人满意,尤其是类型的卸载,条件相当刻薄。然而这部分区域的回收有时又的确是必要的。以前 Sun 公司的 Bug 列表中,曾呈现过的若干个重大的 Bug 就是因为低版本的 Hotspot 虚拟机对此区域未齐全回收而导致内存透露。

办法区的垃圾收集次要回收两局部内容:常量池中废奔的常量 不再应用的类型。

常量池中废奔的常量

  • 先来说说办法区内常量池之中次要寄存的两大类常量:字面量和符号援用。字面量比拟靠近 Java 语言档次的常量概念,如文本字符串、被申明为 final 的常量值等。而符号援用则属于编译原理方面的概念。

常量池中包含上面三类常量:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 办法的名称和描述符
  • HotSpot 虚拟机对常量池的回收策略是很明确的,只有常量池中的常量没有被任何中央援用,就能够被回收。回收废除常量与回收 Java 堆中的对象十分相似。

常量池中不再应用的类型

断定一个常量是否“废除”还是绝对简略,而要断定一个类型是否属于“不再被应用的类”的条件就比拟刻薄了。须要同时满足上面三个条件:

  1. 该类所有的实例都曾经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器曾经被回收,这个条件除非是通过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
  3. 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。

Java 虛拟机被容许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被容许”,而并不是和对象一样,没有援用了就必然会回收。对于是否要对类型进行回收,HotSpot 虚拟机提供了一 Xnoclassgc 参数进行管制,还能够应用一 verbose:class 以及一 XX:+TraceClass 一 Loading、一 XX:+TraceClassUnLoading 查 看类加载和卸载信息。

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

运行时数据区总结

正文完
 0