JVM组成

❤️‍万字深度解析JVM内存模型,看完起飞

各个组件的说明

子系统称号 描绘 线程同享
类加载子系统 担任将.class文件加载到内存区域中。
堆内存(Heap) Java虚拟机中最大的一片空间,存储new出来的目标,以及经过其它办法构建出来的目标。也是GC首要收回的一片区域。 同享
办法区(Method Area) 存储一些类的界说信息,类的元信息,类的办法界说,类中的常量。(元数据) 同享
虚拟机栈(VM Stack) 存储线程运转过程中的栈信息,程序的调用与履行过程,就比如反常时打印出来的栈信息便是其中之一。 线程私有
程序计数器 保存程序运转到哪里了。 线程私有
本地办法栈 与虚拟机栈相似,可是这个是针对于本地办法native办法的栈信息。而虚拟机栈是针对于Java办法的 线程私有

履行引擎

担任翻译字节码指令到操作系统认知的指令,调用操作系统的指令


1.程序计数器

用于记载咱们的程序履行到哪个方位的一个组件。

咱们的Java程序其实便是字节码,履行需求字节码指令,咱们知道Java是依据线程的,而且能够多个线程一起履行,由于CPU核心有限,想要履行多线程任务的话,就需求和谐好各个线程的履行次序,便是经过时刻片来进行和谐办理,那么就会存在线程的中止和发动,假设没有记载程序运转方位的话,程序中止后在运转就找不到该从哪开端履行,而程序计数器就恰恰处理了这个问题。

❤️‍万字深度解析JVM内存模型,看完起飞

假设调用的是navite办法,那么程序计数器为空。

由于native办法直接是调用的是c程序,处于两个不同的内存空间。因而获取不到相关信息。

特性

  • 程序计数器是记载着当时线程所即将履行的字节码指令行号
  • 每一个线程都具有自己的计数器
  • 履行Java办法时,程序计数器是有值的
  • 履行native本地办法时,计数器值为空
  • 程序计数器占用内存十分少,不会呈现OutOfMemoryError

2.虚拟机栈

栈介绍:它是一种数据结构,特性如下

  • 接连紧密存储
  • 先进后出
  • 压栈与出栈
  • 集装箱的摆放办法便是栈结构,入栈便是堆叠集装箱,出栈便是挪走集装箱
  • ❤️‍万字深度解析JVM内存模型,看完起飞

虚拟机栈的生命周期是和线程一致的

线程运转时则虚拟机栈存在,线程毁掉时,虚拟机栈也毁掉。

栈巨细、空间

  • Java1.5后默许每个栈巨细为1mb,在此之前为256kb
  • Java在发动参数中装备-Xss数值[k|m|g] 能够装备栈巨细,例:-Xss10m
    • 不主张手动设置巨细,1mb能够满足运用,假设设置的太大会导致OS内存压力增大,影响高并发环境下的功能
  • 栈分配的内存决议了栈的深度,超出栈的深度则会抛出栈溢出的反常
    • StackOverflowError 仓库溢出
    • OutOfMemoryError 内存溢出

栈帧的组成

一个栈帧就对应一个办法调用,也便是一个办法,除了办法具体的流程外,栈帧还包含其它内容:

  • 局部变量表
    • 办法内部的局部变量信息
  • 操作数栈
    • 保存中间计算的暂时成果
  • 动态链接
    • 将符号引证转化为直接引证
  • 回来地址
    • 寄存调用办法的程序计数器值
❤️‍万字深度解析JVM内存模型,看完起飞

局部变量表

局部变量表存储以下两块的内容:

  • 存储办法参数
  • 存储办法内的局部变量

何为局部变量?

public class Test{
   // 成员变量
   private int name;
   public void sayHi(){
      // 局部变量,由于作用域仅仅是办法内
      String text = "h";
   }
}

局部变量的特性

  • 线程私有,不允许跨线程访问,随办法调用创立,办法退出毁掉
  • 编译期间局部变量表长度(变量个数)已确定,局部变量元数据会存在在字节码文件中。
  • 局部变量表是栈帧中最首要的存储空间,巨细影响栈的深度。

字节码文件内界说的局部变量表元数据

运用Jclasslib插件检查

sayHi()实例办法的源代码

public void sayHi(int printTotal) {
   for (int i = 0; i < printTotal; i++) {
      int a = 0;
      String text = "你好";
      System.out.println(text + a);
   }
}

sayHi()实例办法对应的本地变量表

开端PC代表该变量是在第几行字节码创立的

❤️‍万字深度解析JVM内存模型,看完起飞

main()静态办法的源代码

public static void main(String[] args) {
   new LocalVarSample().sayHi(3);
}

main()办法对应的局部变量表

❤️‍万字深度解析JVM内存模型,看完起飞

对比这两个办法(静态办法,实例办法),能够发现以下不同,也是最首要的区别

  • 静态办法的局部变量表没有this,由于是static办法,而sayHi()办法对应的0号槽位(0号序号)便是this变量

剩下的便是相同的地方

  • 局部变量表的次序由变量的书写次序决议,假设是结构办法或许实例办法,则0号槽位(序号为0)寄存this,静态办法则是第一个书写的变量。

  • 局部变量表的长度固定(多少槽位),在字节码创立时就固定下来,称之为静态局部变量表

  • 一个局部变量至少占用一个Slot槽位,对应LocalVariableTableIndex列,也便是jclasslib的序号列。

  • StartPC代表字节码行号(并非代码行号),Length代表从StartPC开端的后几行能够运用这个变量,

高级特性

  • 上边说到局部变量表中,每个变量至少占用一个Slot,那什么状况下会占用多个Slot呢?答案如下:

    • 占用槽位多少是依据数据类型巨细来决议的,32位以内的类型(int\float\char\引证类型..)占用1个槽位,大于32位的数据类型(long/double)就需求运用到2个槽位

    • 如下图所示,money字段(序号2)为double类型,id字段(序号4)为long类型,他俩的序号能够发现是从2-3,4-5,一个变量占用了两个槽位,由于他们超过了32位。

    • ❤️‍万字深度解析JVM内存模型,看完起飞
    • double money = 32.1;
      long id = 1111111111111111111L;
      
  • Slot复用,Slot在字节码中被称为是静态的本地变量表,而JVM运转时会把静态变量表动态的加载到内存中去,因而槽位或许会发生改变,例如Slot复用的概念。示例代码如下:

    • public void sayHi(){
         int a = 0;
         int b = 1;
         if(a < b){
            int sum = a+b;
      		a = sum;
         }
         int d = 4;
         System.out.println(a+d);
      }
      
    • 在上边的示例代码中,咱们创立了4个变量,absumd

    • 字节码文件的静态变量表如下:

      • Slot槽位序号 变量名
        0 this
        1 a
        2 b
        3 sum
        4 d
    • 那程序真正履行过程中,由于if内的局部变量sum作用域仅仅只有if内部,当程序履行到if结尾时,就会把3号槽位的sum给铲除去,由于已经不或许会被访问到了,在将d加载到3号槽位,实现复用。好处如下:

      • 能够节约栈帧的空间,空间大了栈的深度就会更深。
      • 进步功能:当槽位被复用时,能够避免创立新的槽位,然后削减内存的分配和收回,进步功能。
      • 优化废物搜集:未复用的局部变量槽位会误导废物搜集器,阻止内存收回。经过槽位复用,废物搜集器能更精确地收回内存。

操作数栈

字节码指令履行过程中的暂时成果数据寄存的地方就叫做操作数

假设咱们有一个办法,对应的字节码指令如下,由于办法还未运转,所以操作数栈为空:

❤️‍万字深度解析JVM内存模型,看完起飞
❤️‍万字深度解析JVM内存模型,看完起飞

当履行第一条指令bipush 10时,10将被压入栈顶。此刻栈如下

❤️‍万字深度解析JVM内存模型,看完起飞

履行第二条指令istore_1 时,栈顶元素将被放入局部变量表,此刻局部变量表和栈的状况如下:

10将被弹出栈,并被放入局部变量表,操作数栈清空

❤️‍万字深度解析JVM内存模型,看完起飞
❤️‍万字深度解析JVM内存模型,看完起飞

履行第三条指令bipush 18时,18将被压入栈顶。此刻局部变量表和栈的状况如下:

❤️‍万字深度解析JVM内存模型,看完起飞

履行第四条指令istore_2时,栈又会被清空,并将18放入局部变量表。

履行第五条指令iload_1和第六条指令iload_2后,局部变量表的1018将被压入栈顶:

❤️‍万字深度解析JVM内存模型,看完起飞

履行第七条指令iadd时,操作数栈的栈顶元素与第二位元素将相加。

履行第八条指令istore_3时,栈顶寄存的相加后的元素28将先被弹出栈顶,再被存入本地变量表。

履行第九条指令iload_3时,28将被压入栈顶。

❤️‍万字深度解析JVM内存模型,看完起飞

履行最终一条指令ireturn时将回来栈顶元素28

此刻办法完毕,局部变量表将被清空,操作数栈将被清空。办法回来成果。


动态链接

什么是动态链接?将字节码中的符号引证转化成内存的直接引证。

字节码文件中存储的引证信息都是cp_info #27这种东西,能够理解为便是个字符串信息,也叫字面量,实际上便是一个引证的信息,假设字节码new一个目标,在字节码中存储的是new #14这种字符串,那在JVM运转时,想真正的找到这个目标的信息,就要去JVM办法区里去找,而这个#14就比如是目标信息存储的方位,有了#14就能找到LocalVarSample这个目标。

在内存中履行的做个转化过程,就叫动态链接。

❤️‍万字深度解析JVM内存模型,看完起飞

为什么要这么干呢?

  • 由于栈帧的空间是有限的,不能直接存储目标的信息,假设只存储目标的指针,就能大大的节约栈帧的空间。而且方便JVM办理。

动态链接示意图

❤️‍万字深度解析JVM内存模型,看完起飞

回来地址

它首要用来指示办法履行完毕后程序的履行流应该跳转到的方位。简略来说,便是告诉程序:我这个办法履行完了,你应该去哪里继续履行。

举个比如,假设你正在履行一个名为A的办法,然后在A中又调用了一个名为B的办法。那么在调用B的时分,虚拟机会在栈帧中保存一个回来地址,这个地址指向的是办法A中调用办法B那条指令的下一条指令。当办法B履行完毕后,程序就会跳转到这个回来地址,也便是回到办法A中继续履行。

爆栈

虚拟机栈,是个栈结构,既然是一个存储的结构那么必然是有巨细限制,不或许无限制的一向入栈,这样内存也扛不住,那么爆栈的意思其实也很好理解,便是办法运转的层级过深,就比如递归,没有中止条件,就会导致一向入栈,一向入栈,直到把栈给顶爆,就抛出,StackOverflowError 反常。

public static void main(String[] args) {
   // 调用爆栈(递归)办法
   System.out.print(method1());
}
public int method1(){
   // 无限制的递归
   return method1();
}

成果

Exception in thread "main" java.lang.StackOverflowError
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
	at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
   ... 省掉n行

3.本地办法栈

本地办法栈是专供于本地办法(navite修饰)运用的,当JVM经过JNI调用本地办法时,本地办法栈便是首要记载JVM调用本地办法时的一些状态信息。


4.堆(Heap)

堆是JVM中最核心的内存区域,寄存运转时实例化的目标实例。堆在JVM创立的时分就被创立,空间也被分配。堆是线程同享的一片大区域。

  • 堆内存在物理上是涣散的,在逻辑上是接连的,也便是说堆内存的数据能够涣散在不同的内存颗粒上,可是JVM在运用时会经过指针将数据接连起来,在咱们看来JVM堆便是接连的一片空间。

  • 堆中包含线程私有的缓冲区(TLAB),能够有用的进步JVM的并发功率

TLAB(Thread-Local Allocation Buffer)是线程本地分配缓冲区的缩写。在Java中,为了进步内存分配的功率,JVM一般会为每个线程在堆中分配一个私有的缓冲区,这便是TLAB。

TLAB的首要作用是削减线程之间的竞赛。在多线程环境下,假设每个线程都直接在堆上分配内存,那么这些线程或许会竞赛同一块内存区域,然后导致功能下降。经过运用TLAB,每个线程都能够在自己的缓冲区中分配内存,然后避免了这种竞赛。

TLAB只用于分配小目标。当一个线程需求分配一个大目标时,它会直接在堆上进行分配。当TLAB用完时,线程会请求一个新的TLAB。

总的来说,TLAB的引入是为了进步内存分配的功率,特别是在多线程环境下。

引证类型与堆的联系

  • 引证类型的本质便是一个指针,指向堆内存的行列实例地址。
  • 堆是废物收回(GC)的要点区域,办法完毕后并不会立即进行废物收回,而是等JVM判别需求废物收回时才进行废物收回。

❤️‍万字深度解析JVM内存模型,看完起飞

堆结构

  • 新生代,首要寄存刚创立的目标,这些目标是不稳定的,或许会被频频GC收回。
  • 老时代,寄存相对稳定的目标(GC屡次未被收回),不会进行频频的GC行为。
  • 元空间,内存中永久保存区域,用于寄存类的描绘信息,几乎不会GC。
❤️‍万字深度解析JVM内存模型,看完起飞

堆巨细

参数相关

  • -Xms数量[k|m|g] 设置堆空间(年轻代+老时代)的初始化内存巨细,例如-Xms2g
  • -Xmx数量[k|m|g] 设置堆空间(年轻代+老时代)的最大内存巨细,例如-Xmx2g
  • -XX:+PrintGCDetails 设置该参数能够检查GC相关的数据,在程序退出时打印
Heap
 PSYoungGen      total 1378816K, used 161309K [0x0000000716580000, 0x000000076e180000, 0x00000007c0000000)
  eden space 1364480K, 11% used [0x0000000716580000,0x0000000720307790,0x0000000769a00000)
  from space 14336K, 0% used [0x000000076d380000,0x000000076d380000,0x000000076e180000)
  to   space 33792K, 0% used [0x0000000769f80000,0x0000000769f80000,0x000000076c080000)
 ParOldGen       total 459264K, used 73890K [0x00000005c3000000, 0x00000005df080000, 0x0000000716580000)
  object space 459264K, 16% used [0x00000005c3000000,0x00000005c7828ba0,0x00000005df080000)
 Metaspace       used 92836K, capacity 98120K, committed 98752K, reserved 1134592K
  class space    used 12143K, capacity 13082K, committed 13184K, reserved 1048576K

设置主张:主张整个堆巨细设置为FullGC后存活目标的3-4倍

默许值

  • 堆默许巨细为操作系统内存的64分之1
  • 堆最大内存为操作系统的4分之1
  • 新生代与老时代份额
    • 新生代占用总堆的3分之1
    • 老时代占用总堆的3分之2

新生代

新生代分为三个小区域:Eden(伊甸园)、Survivor-0(From)、Survivor-1(To)

  • 内存分配份额为:8:1:1
  • 假设新生代有1G内存,那分配成果便是:800M100M100M
  • 绝大多数刚创立的目标都会放在Eden区,因而需求有足够的空间寄存这些目标

刚初始化的新生代,此刻还没有数据时是长这样的

❤️‍万字深度解析JVM内存模型,看完起飞

假设新创立一个目标:User user = new User() 此刻User()目标会被放入Eden区。

❤️‍万字深度解析JVM内存模型,看完起飞

假设Eden区满了(默许)的时分,无法分配新目标的时分,就会触发一次Minor GC,也叫Young GC对新生代进行废物收回。

  • 依据可达性算法扫描过一切目标后,会将目标分为两拨:无引证目标,持有引证目标
  • 持有引证目标将会被经过仿制拷贝算法到S0区,剩下的一切目标将被废物收回掉。
  • 并将目标的头特点age + 1
❤️‍万字深度解析JVM内存模型,看完起飞

Eden区再次满了之后,再次触发Minor GC,再次经过可达性算法剖析目标是否持有引证,再次将目标进行拷贝交互和铲除。

假设咱们的User目标仍然持有引证,User目标将被仿制并拷贝到S1age + 1,再将无引证目标进行废物收回

❤️‍万字深度解析JVM内存模型,看完起飞

假设Eden区再次满了,又会触发Minor GC,此刻咱们的User目标仍然持有引证,那么它将被移动到S0age + 1

❤️‍万字深度解析JVM内存模型,看完起飞

假设一向Minor GC,直到Userage大于15时,User目标会被JVM认定为是稳定的目标,会被放入Old Gen老时代里。

❤️‍万字深度解析JVM内存模型,看完起飞

问题一

为什么User会从S1移动到S0呢?

  • JVM为了方便相关内存数据,处理内存碎片问题,所以会将S0区和S1区的数据进行互换,每次Minor GCEden区的目标会被放入空的Survivor区,并将该区的一切目标移动到另一个Survivor
  • 出目标的Survivor区被称为From区,移动到的Survivor区被称为To

问题二

为什么废物收回要分代(新生代、老时代)处理

  • 为了履行功率的考量,由于大多数目标的存活时刻或许极低,或许办法履行完目标就没有引证了,假设触发大局GCFull GC),则会牵连无关紧要的目标,影响整体功率,添加程序响应时刻。

新生代废物收回的特殊状况

其实特殊状况简略概括便是,新创立的目标没有足够的内存进行分配了该怎样办?

Eden区内存不足无法分配,目标被移动到S0区

  • 履行Minor GC,并将目标移动到S0区,可是S0区满了,目标放不进去。
  • 测验直接将目标放入Old Gen老时代里。
  • 假设老时代满了放不进去,则会触发Full GC后再次测验寄存
    • 假设还放不进去,则抛出OOM过错
❤️‍万字深度解析JVM内存模型,看完起飞

仿制交流算法


5.办法区

寄存类的信息,办法信息等字节码中的静态信息(元数据)。

  • 办法区是线程同享的区域,是物理上涣散,逻辑上接连的一片区域

保存的内容

  1. 类型信息:这包含类的称号、父类、接口、访问修饰符等信息。
  2. 字段信息:类中的字段(包含静态字段和非静态字段)的称号、类型、访问修饰符等信息。
  3. 办法信息:类中的办法(包含静态办法和非静态办法)的称号、回来类型、参数类型、访问修饰符等信息。
  4. 常量池:包含字面量(如文本字符串)和符号引证(如类和接口的全名、字段的称号和描绘符、办法的称号和描绘符)。
  5. 静态变量:类的静态变量。
  6. 即时编译后的代码:假设启用了即时编译(JIT),办法区还会保存编译后的本地机器代码。

永久代与元空间

永久代的数据是寄存在JVM内存之内的,和JVM同享内存空间,而元空间的数据是直接寄存在操作系统内存上的,与JVM内存互不影响,不占用JVM内存。

  • 永久代占用JVM内存,容易导致内存溢出
  • 元空间与物理机内存保持一致,最大可用内存为物理机剩下可用内存。

元空间巨细

默许为(12mb~21mb),依据操作系统的不同在此区间内动态调整。假设慢了则会触发Full GC,并进步元空间巨细或许打破21mb

能够经过-XX:MetaSpaceSize=xxx[M|G]设置元空间最小值、运用-XX:MaxMetaSpaceSize=xxx[M|G]设置最大值

  • 主张设置较大的元空间巨细,削减因元空间内存不足导致的Full GC
  • 假设元空间内存超过了MaxMetaSpaceSize则会抛出OutOfMemoryError:MetaSpace过错

不同版本JVM办法区的改变

办法区是个概念,JVM规范要求有必要要有这个东西,至于怎样实现便是JVM厂商做的工作。例如HotSpot虚拟机

  • 1.7前,办法区叫做永久代Permanent Generation
  • 1.7开端逐步扔掉永久代
  • 1.8彻底扔掉永久代并实现了元空间Meta Space

比如公共场所的消防设施,消防队要求有必要有,可是怎样去装置是公共场所的工作。

  • 某棋牌室放了16个灭火器 + 3个消防栓作为消防设施
  • 某酒店采用了热感喷头 + 36个消防栓作为消防设施

运转时常量区

寄存来自字节码中常量池的数据,或许动态发生的新常量。

运转时常量区位与元空间内,属于内部的一片空间。


办法区历史改变

静态变量是存储在哪的?public static User user = new User();

不同版本JVM是不一样的,如下图所示

JDK版本 改变
<=1.6 此刻有永久代的概念,静态变量寄存在永久代内
1.7 有永久代,可是逐步移除永久代,字符串常量池,静态变量从永久代移除,改为寄存在堆中
>= 1.8 已移除永久代,类型信息、字段、办法、常量保存在元空间内,字符串常量池,静态变量仍在堆中

静态变量是运用new关键字创立的,如new User(),那一定是存储在堆内存中的。

假设静态变量是根本类型如static int i = 1;,那它会寄存在元空间内。