敞开生长之旅!这是我参加「日新方案 2 月更文挑战」的第 2 天,点击查看活动详情

JVM 内存结构

Java 虚拟机的内存空间分为 5 个部分:

  • 程序计数器
  • Java 虚拟机栈
  • 本地办法栈
  • 办法区

聊聊对JVM内存结构的理解吧

JDK 1.8 同 JDK 1.7 比,最大的不同便是:元数据区替代了永久代。元空间的实质和永久代类似,都是对 JVM 标准中办法区的完结。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是运用本地内存。

程序计数器(PC 寄存器)

程序计数器的界说

程序计数器是一块较小的内存空间,是当时线程正在履行的那条字节码指令的地址。若当时线程正在履行的是一个本地办法,那么此刻程序计数器为Undefined

程序计数器的效果

  • 字节码解释器经过改动程序计数器来顺次读取指令,然后完结代码的流程操控。
  • 在多线程状况下,程序计数器记载的是当时线程履行的方位,然后当线程切换回来时,就知道前次线程履行到哪了。

程序计数器的特色

  • 是一块较小的内存空间。
  • 线程私有,每条线程都有自己的程序计数器。
  • 生命周期:跟着线程的创立而创立,跟着线程的完毕而毁掉。
  • 是唯一一个不会呈现 OutOfMemoryError 的内存区域。

Java 虚拟机栈(Java 栈)

Java 虚拟机栈的界说

Java 虚拟机栈是描绘 Java 办法运转进程的内存模型。

Java 虚拟机栈会为每一个行将运转的 Java 办法创立一块叫做“栈帧”的区域,用于寄存该办法运转进程中的一些信息,如:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 办法出口信息
  • ……

聊聊对JVM内存结构的理解吧

压栈出栈进程

当办法运转进程中需求创立局部变量时,就将局部变量的值存入栈帧中的局部变量表中。

Java 虚拟机栈的栈顶的栈帧是当时正在履行的活动栈,也便是当时正在履行的办法,PC 寄存器也会指向这个地址。只有这个活动的栈帧的本地变量能够被操作数栈运用,当在这个栈帧中调用另一个办法,与之对应的栈帧又会被创立,新创立的栈帧压入栈顶,变为当时的活动栈帧。

办法完毕后,当时栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。假如没有返回值,那么新的活动栈帧中操作数栈的操作数没有改动。

因为 Java 虚拟机栈是与线程对应的,数据不是线程同享的(也便是线程私有的),因而不用关怀数据一致性问题,也不会存在同步锁的问题。

局部变量表

界说为一个数字数组,主要用于存储办法参数、界说在办法体内部的局部变量,数据类型包括各类基本数据类型,方针引证,以及 return address 类型。

局部变量表容量巨细是在编译期承认下来的。最基本的存储单元是 slot,32 位占用一个 slot,64 位类型(long 和 double)占用两个 slot。

关于 slot 的了解:

  • JVM 虚拟机会为局部变量表中的每个 slot 都分配一个拜访索引,经过这个索引即可成功拜访到局部变量表中指定的局部变量值。
  • 假如当时帧是由结构办法或者实例办法创立的,那么该方针引证 this,会寄存在 index 为 0 的 slot 处,其他的参数表次序持续摆放。
  • 栈帧中的局部变量表中的槽位是能够重复的,假如一个局部变量过了其效果域,那么其效果域之后声明的新的局部变量就有或许会复用过期局部变量的槽位,然后到达节省资源的目的。

在栈帧中,与功能调优联系最亲近的部分,便是局部变量表,办法履行时,虚拟机运用局部变量表完结办法的传递局部变量表中的变量也是重要的废物收回根节点,只需被局部变量表中直接或间接引证的方针都不会被收回。

操作数栈

  • 栈顶缓存技能:因为操作数是存储在内存中,频频的进行内存读写操作影响履行速度,将栈顶元素全部缓存到物理 CPU 的寄存器中,以此降低对内存的读写次数,提升履行引擎的履行功率。
  • 每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就界说好。32bit 类型占用一个栈单位深度,64bit 类型占用两个栈单位深度操作数栈。
  • 并非选用拜访索引办法进行数据拜访,而是只能经过标准的入栈、出栈操作完结一次数据拜访。

办法的调用

  • 静态链接:当一个字节码文件被装载进 JVM 内部时,假如被调用的方针办法在编译期可知,且运转时期间坚持不变,这种状况下降调用方的符号引证转为直接引证的进程称为静态链接。

  • 动态链接:假如被调用的办法无法在编译期被承认下来,只能在运转期将调用的办法的符号引证转为直接引证,这种引证转化进程具有动态性,因而被称为动态链接。

  • 办法绑定

    • 前期绑定:被调用的方针办法假如在编译期可知,且运转期坚持不变。
    • 晚期绑定:被调用的办法在编译期无法被承认,只能够在程序运转期依据实践的类型绑定相关的办法。
  • 非虚办法:假如办法在编译期就承认了具体的调用版别,则这个版别在运转时是不可变的,这样的办法称为非虚办法静态办法。私有办法,final 办法,实例结构器,父类办法都是非虚办法,除了这些以外都是虚办法。

  • 虚办法表:面向方针的编程中,会很频频的运用动态分配,假如每次动态分配的进程都要从头在类的办法元数据中搜索适宜的方针的话,就或许影响到履行功率,因而为了进步功能,JVM 选用在类的办法区树立一个虚办法表,运用索引表来代替查找。

    • 每个类都有一个虚办法表,表中寄存着各个办法的实践进口。
    • 虚办法表会在类加载的链接阶段被创立,并开端初始化,类的变量初始值预备完结之后,JVM 会把该类的办法也初始化完毕。
  • 办法重写的实质

    • 找到操作数栈顶的第一个元素所履行的方针的实践类型,记做 C。假如在类型 C 中找到与常量池中描绘符和简略名称都相符的办法,则进行拜访权限校验。
    • 假如经过则返回这个办法的直接引证,查找进程完毕;假如不经过,则返回 java.lang.IllegalAccessError 反常。
    • 不然,依照继承联系从下往上顺次对 C 的各个父类进行上一步的搜索和验证进程。
    • 假如一直没有找到适宜的办法,则抛出 java.lang.AbstractMethodError 反常。

Java 中任何一个普通办法都具有虚函数的特征(运转期承认,具有晚期绑定的特色),C++ 中则运用关键字 virtual 来显式界说。假如在 Java 程序中,不希望某个办法拥有虚函数的特征,则能够运用关键字 final 来符号这个办法。

Java 虚拟机栈的特色

  • 运转速度特别快,只是次于 PC 寄存器。

  • 局部变量表跟着栈帧的创立而创立,它的巨细在编译时承认,创立时只需分配事前规定的巨细即可。在办法运转进程中,局部变量表的巨细不会产生改动。

  • Java 虚拟机栈会呈现两种反常:StackOverFlowError 和 OutOfMemoryError。

    • StackOverFlowError 若 Java 虚拟机栈的巨细不答应动态扩展,那么当线程恳求栈的深度超过当时 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 反常。
    • OutOfMemoryError 若答应动态扩展,那么当线程恳求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 反常。
  • Java 虚拟机栈也是线程私有,跟着线程创立而创立,跟着线程的完毕而毁掉。

  • 呈现 StackOverFlowError 时,内存空间或许还有许多。

常见的运转时反常有:

  • NullPointerException – 空指针引证反常
  • ClassCastException – 类型强制转化异
  • IllegalArgumentException – 传递不合法参数反常
  • ArithmeticException – 算术运算反常
  • ArrayStoreException – 向数组中寄存与声明类型不兼容方针反常
  • IndexOutOfBoundsException – 下标越界反常
  • NegativeArraySizeException – 创立一个巨细为负数的数组错误反常
  • NumberFormatException – 数字格局反常
  • SecurityException – 安全反常
  • UnsupportedOperationException – 不支持的操作反常

本地办法栈(C 栈)

本地办法栈的界说

本地办法栈是为 JVM 运转 Native 办法预备的空间,因为许多 Native 办法都是用 C 言语完结的,所以它通常又名 C 栈。它与 Java 虚拟机栈完结的功用类似,只不过本地办法栈是描绘本地办法运转进程的内存模型。

栈帧改动进程

本地办法被履行时,在本地办法栈也会创立一块栈帧,用于寄存该办法的局部变量表、操作数栈、动态链接、办法出口信息等。

办法履行完毕后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 反常。

假如 Java 虚拟机本身不支持 Native 办法,或是本身不依赖于传统栈,那么能够不提供本地办法栈。假如支持本地办法栈,那么这个栈一般会在线程创立的时候按线程分配。

堆的界说

堆是用来寄存方针的内存空间,简直一切的方针都存储在堆中。

堆的特色

  • 线程同享,整个 Java 虚拟机只有一个堆,一切的线程都拜访同一个堆。而程序计数器、Java 虚拟机栈、本地办法栈都是一个线程对应一个。
  • 在虚拟机启动时创立。
  • 是废物收回的主要场所。
  • 堆可分为重生代(Eden 区:From SurviorTo Survivor)、老时代。
  • Java 虚拟机标准规定,堆能够处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
  • 关于 Survivor s0,s1 区: 复制之后有交换,谁空谁是 to。

不同的区域寄存不同生命周期的方针,这样能够依据不同的区域运用不同的废物收回算法,更具有针对性。

堆的巨细既能够固定也能够扩展,但关于主流的虚拟机,堆的巨细是可扩展的,因而当线程恳求分配内存,但堆已满,且内存已无法再扩展时,就抛出 OutOfMemoryError 反常。

Java 堆所运用的内存不需求确保是连续的。而因为堆是被一切线程同享的,所以对它的拜访需求注意同步问题,办法和对应的属性都需求确保一致性。

重生代与老时代

  • 老时代比重生代生命周期长。
  • 重生代与老时代空间默许比例 1:2:JVM 调参数,XX:NewRatio=2,表明重生代占 1,老时代占 2,重生代占整个堆的 1/3。
  • HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省所占的比例是:8:1:1
  • 简直一切的 Java 方针都是在 Eden 区被 new 出来的,Eden 放不了的大方针,就直接进入老时代了。

方针分配进程

  • new 的方针先放在 Eden 区,巨细有约束
  • 假如创立新方针时,Eden 空间填满了,就会触发 Minor GC,将 Eden 不再被其他方针引证的方针进行毁掉,再加载新的方针放到 Eden 区,特别注意的是 Survivor 区满了是不会触发 Minor GC 的,而是 Eden 空间填满了,Minor GC 才趁便整理 Survivor 区
  • 将 Eden 中剩余的方针移到 Survivor0 区
  • 再次触发废物收回,此刻前次 Survivor 下来的,放在 Survivor0 区的,假如没有收回,就会放到 Survivor1 区
  • 再次经历废物收回,又会将幸存者从头放回 Survivor0 区,顺次类推
  • 默许是 15 次的循环,超过 15 次,则会将幸存者区幸存下来的转去老年区 jvm 参数设置次数 : -XX:MaxTenuringThreshold=N 进行设置
  • 频频在重生区收集,很少在养老区收集,简直不在永久区/元空间收集

Full GC /Major GC 触发条件

  • 显示调用System.gc(),老时代的空间不行,办法区的空间不行等都会触发 Full GC,一起对重生代和老时代收回,FUll GC 的 STW 的时间最长,应该要防止
  • 在呈现 Major GC 之前,会先触发 Minor GC,假如老时代的空间仍是不行就会触发 Major GC,STW 的时间善于 Minor GC

逃逸剖析

  • 标量替换

    • 标量不可在分化的量,java 的基本数据类型便是标量,标量的对立便是能够被进一步分化的量,而这种量称之为聚合量。而在 JAVA 中方针便是能够被进一步分化的聚合量
    • 替换进程,经过逃逸剖析承认该方针不会被外部拜访,并且方针能够被进一步分化时,JVM 不会创立该方针,而会将该方针成员变量分化若干个被这个办法运用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。
  • 方针和数组并非都是在堆上分配内存的

  • 《深化了解 Java 虚拟机中》关于 Java 堆内存有这样一段描绘:跟着 JIT 编译期的开展与逃逸剖析技能逐渐成熟,栈上分配,标量替换优化技能将会导致一些改动,一切的方针都分配到堆上也渐渐变得不那么”肯定”了。

  • 这是一种能够有效减少 Java 内存堆分配压力的剖析算法,经过逃逸剖析,Java Hotspot 编译器能够剖分出一个新的方针的引证的运用范围然后决议是否要将这个方针分配到堆上。

  • 当一个方针在办法中被界说后,它或许被外部办法所引证,如作为调用参数传递到其他地方中,称为办法逃逸

  • 再如赋值给类变量或能够在其他线程中拜访的实例变量,称为线程逃逸

  • 运用逃逸剖析,编译器能够对代码做如下优化:

    • 同步省略:假如一个方针被发现只能从一个线程被拜访到,那么关于这个方针的操作能够不考虑同步。
    • 将堆分配转化为栈分配:假如一个方针在子程序中被分配,要使指向该方针的指针永久不会逃逸,方针或许是栈分配的候选,而不是堆分配。
    • 分离方针或标量替换:有的方针或许不需求作为一个连续的内存结构存在也能够被拜访到,那么方针的部分(或全部)能够不存储在内存,而是存储在 CPU 寄存器中。
public static StringBuffer createStringBuffer(String s1, String s2) {
​
  StringBuffer s = new StringBuffer();
​
  s.append(s1);
​
  s.append(s2);
​
  return s;
}

s 是一个办法内部变量,上边的代码中直接将 s 返回,这个 StringBuffer 的方针有或许被其他办法所改动,导致它的效果域就不只是在办法内部,即使它是一个局部变量,但仍是逃逸到了办法外部,称为办法逃逸

还有或许被外部线程拜访到,譬如赋值给类变量或能够在其他线程中拜访的实例变量,称为线程逃逸

  • 在编译期间,假如 JIT 经过逃逸剖析,发现有些方针没有逃逸出办法,那么有或许堆内存分配会被优化成栈内存分配。
  • jvm 参数设置,-XX:+DoEscapeAnalysis :敞开逃逸剖析 ,-XX:-DoEscapeAnalysis : 关闭逃逸剖析
  • 从 jdk 1.7 开端现已默许开端逃逸剖析。

TLAB

  • TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区,是属于 Eden 区的,这是一个线程专用的内存分配区域,线程私有,默许敞开的(当然也不是肯定的,也要看哪种类型的虚拟机)
  • 堆是大局同享的,在同一时间,或许会有多个线程在堆上申请空间,但每次的方针分配需求同步的进行(虚拟机选用 CAS 配上失利重试的办法确保更新操作的原子性)可是功率却有点下降
  • 所以用 TLAB 来防止多线程抵触,在给方针分配内存时,每个线程运用自己的 TLAB,这样能够使得线程同步,进步了方针分配的功率
  • 当然并不是一切的方针都能够在 TLAB 中分配内存成功,假如失利了就会运用加锁的机制来坚持操作的原子性
  • -XX:+UseTLAB运用 TLAB,-XX:+TLABSize 设置 TLAB 巨细

四种引证办法

  • 强引证:创立一个方针并把这个方针赋给一个引证变量,普通 new 出来方针的变量引证都是强引证,有引证变量指向时永久不会被废物收回,jvm 即使抛出 OOM,能够将引证赋值为 null,那么它所指向的方针就会被废物收回。
  • 软引证:假如一个方针具有软引证,内存空间足够,废物收回器就不会收回它,假如内存空间不足了,就会收回这些方针的内存。只需废物收回器没有收回它,该方针就能够被程序运用。
  • 弱引证:非必需方针,当 JVM 进行废物收回时,无论内存是否足够,都会收回被弱引证相关的方针。
  • 虚引证:虚引证并不会决议方针的生命周期,假如一个方针仅持有虚引证,那么它就和没有任何引证一样,在任何时候都或许被废物收回器收回。

办法区

办法区的界说

Java 虚拟机标准中界说办法区是堆的一个逻辑部分。办法区寄存以下信息:

  • 现已被虚拟机加载的类信息
  • 常量
  • 静态变量
  • 即时编译器编译后的代码

办法区的特色

  • 线程同享。 办法区是堆的一个逻辑部分,因而和堆一样,都是线程同享的。整个虚拟机中只有一个办法区。
  • 永久代。 办法区中的信息一般需求长期存在,并且它又是堆的逻辑分区,因而用堆的区分办法,把办法区称为“永久代”。
  • 内存收回功率低。 办法区中的信息一般需求长期存在,收回一遍之后或许只有少数信息无效。主要收回方针是:对常量池的收回;对类型的卸载。
  • Java 虚拟机标准对办法区的要求比较宽松。 和堆一样,答应固定巨细,也答应动态扩展,还答应不完结废物收回。

运转时常量池

办法区中寄存:类信息、常量、静态变量、即时编译器编译后的代码。常量就寄存在运转时常量池中。

当类被 Java 虚拟机加载后, .class 文件中的常量就寄存在办法区的运转时常量池中。并且在运转期间,能够向常量池中增加新的常量。如 String 类的 intern() 办法就能在运转期间向常量池中增加字符串常量。

直接内存(堆外内存)

直接内存是除 Java 虚拟机之外的内存,但也或许被 Java 运用。

操作直接内存

在 NIO 中引入了一种基于通道和缓冲的 IO 办法。它能够经过调用本地办法直接分配 Java 虚拟机之外的内存,然后经过一个存储在堆中的DirectByteBuffer方针直接操作该内存,而无须先将外部内存中的数据复制到堆中再进行操作,然后进步了数据操作的功率。

直接内存的巨细不受 Java 虚拟机操控,但既然是内存,当内存不足时就会抛出 OutOfMemoryError 反常。

直接内存与堆内存比较

  • 直接内存申请空间消耗更高的功能
  • 直接内存读取 IO 的功能要优于普通的堆内存
  • 直接内存效果链: 本地 IO -> 直接内存 -> 本地 IO
  • 堆内存效果链:本地 IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地 IO

服务器管理员在装备虚拟机参数时,会依据实践内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存约束,然后导致动态扩展时呈现OutOfMemoryError反常。