关于java:JVM内存模型

5次阅读

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

程序计数器(PC 寄存器)

定义

程序计数器是一块较小的内存空间(逻辑上的),是以后线程正在执行的那条字节码指令的地址。若以后线程正在执行的是一个本地办法,那么此时程序计数器为Undefined

作用

  • 字节码解释器通过改变程序计数器来顺次读取指令,从而实现代码的流程管制。
  • 在多线程状况下,程序计数器记录的是以后线程执行的地位,从而当线程切换回来时,就晓得上次线程执行到哪了。

特点

  • 是一块较小的内存空间。
  • 线程公有,每条线程都有本人的程序计数器。
  • 生命周期:随着线程的创立而创立,随着线程的销毁而销毁。
  • 是惟一一个不会呈现 OutOfMemoryError 的内存区域。

Java 虚拟机栈(Java 栈)

定义

Java 虚拟机栈是形容 Java 办法运行过程的内存模型。
Java 虚构机会为每一个行将运行的 Java 办法创立一块叫做“栈帧”的区域,用于寄存该办法运行过程中的一些信息,如:

  • 局部变量表
  • 操作数栈
  • 动静链接
  • 办法进口信息

压栈出栈过程

当执行到一个 Java 办法时,首先创立栈帧,将栈帧压入栈顶,再将程序计数器执行指向此栈帧。
栈顶的栈帧为流动栈帧,只有这个流动栈帧是的 本地变量 能够被 操作数栈 应用,当在这个栈帧中调用另一个办法,与之对应的栈帧又会被创立,新创建的栈帧压入栈顶,变为以后的流动栈帧。
当办法运行过程中须要创立局部变量时,就将局部变量的值存入栈帧中的局部变量表中。
办法完结后,以后栈帧被移出,栈帧的返回值编程新的流动栈帧中操作数栈的一个操作数。如果没有返回值,那么新的流动栈帧中操作数栈的操作数没有变动。

因为 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 时,内存空间可能还有很多。

本地办法栈(C 栈)

定义

本地办法栈是为 JVM 运行 native 办法筹备的空间,因为很多 native 办法都是用 C 语言实现的,所以通常又叫 C 栈。它与 Java 虚拟机栈实现的性能相似,只不过本地办法栈是形容本地办法运行过程的内存模型。

栈帧变动过程

本地办法被执行时,在本地办法栈也会创立一块栈帧,用于寄存该办法的局部变量、操作数栈、动静链接、办法进口信息等。
办法执行完结后,相应的栈帧也会出栈,并开释内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异样。

如果 Java 虚拟机自身不反对 native 办法,或自身不依赖于传统栈,那么能够不提供本地办法栈。如果反对本地办法栈,那么这个栈个别会在线程创立的时候按线程调配。

定义

堆是用来寄存对象的内存空间,简直所有的对象都存储于堆中。

特点

  • 线程共享,整个 Java 虚拟机只有一个堆,所有的线程都拜访同一个堆。而程序计数器、Java 虚拟机栈、本地办法栈都是一个线程对应一个。
  • 在虚拟机启动时候创立。
  • 是垃圾回收的次要场合。
  • 堆可分为新生代(Eden 区:From Survivor,To 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 中残余的对象移到 Survivor 区
  • 再次触发垃圾回收,此时上次 Survivor 下来的,放在 Survivor0 区的,如果没有回收,就会收到 Survivor1 区。
  • 再次经验垃圾回收,又会将 Survivor 从新放回到 Survivor0 区,顺次类推
  • 默认是 15 此循环,超过 15 次,则会将 Survivor 转去老年区,JVM 调参 -XX:MaxTenuringThreshold=15 进行设置
  • 频繁在新生代手机,很少在老年代收集,简直不在永恒区、元空间收集。

Full GC/Major GC 触发条件

  • 显示调用 System.gc(),老年代的空间不够,办法区的空间不够都会触发 Full GC,同时对新生代和老年代回收,Full GC 的 STW 工夫最长,应该防止。
  • 在呈现 Major GC 之前,会先触发 Minor GC,如果老年代的空间还是不够就会触发 Major GC,STW 的工夫长于 Minor GC。

逃逸剖析

标量替换

  • 标量是不可再合成的量,java 的根本数据类型就是标量,标量的对抗就是能够被进一步合成的量,而这种量称之为聚合量。而在 java 中对象就是能够被进一步合成的聚合量。
  • 替换过程,通过逃逸剖析确定该对象会不会被内部拜访,并且对象能够被进一步合成时,JVM 不会创立该对象,而会将该对象的成员变量合成若干个被这个办法应用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上调配空间。

对象和数组并非都是在堆上分配内存的

  • 随着 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敞开逃逸剖析。
  • 从 jdk1.7 开始曾经默认开启逃逸剖析。

TLAB

  • TLAB 的全称是 Thread Local Allocation Buffer,即线程本地调配缓存区,是属于 Eden 区的,这是一个线程专用的内存调配区域,线程公有,默认开启的(当然也不是相对的,也要看哪种类型的虚拟机)。
  • 堆是全局共享的,在同一时间,可能会有多个线程在堆上申请空间,但每次的对象调配须要同步的进行(虚拟机采纳 CAS 配上失败重试的形式保障更新操作的原子性)然而效率却有点降落。
  • 所以用 TLAB 来防止多线程抵触,在给对象分配内存时,每个线程应用本人的 TLAB,这样能够使得线程同步,进步对象的调配效率。
  • 当然并不是所有的对象都能够在 TLAB 中分配内存胜利的,如果失败了就会应用加锁的机制来放弃操作的原子性。
  • JVM 调参 -XX:+UseTLAB 应用 TLAB,-XX:+TLABSize设置 TLAB 大小。

四种援用形式

  • 强援用:创立一个对象并把这个对象赋给一个援用变量,一般 new 进去的对象的变量援用都是强援用,有援用变量指向时永远不会被垃圾回收,jvm 即便抛出 OOM,能够将援用赋值为 null,那么它所指向的对象就会被垃圾回收。
  • 软援用:如果一个对象具备软援用,内存空间足够,垃圾回收期就不会回收他,如果内存空间有余了,就会回收这些对象的内存。只有垃圾回收器没有回收它,该对象就能够被程序应用。将对象交给 SoftReference 即可变成软援用。
  • 弱援用:非必须对象,当 JVM 进行垃圾回收时,无论内存是否短缺,都会回收被弱援用关联的对象。将对象交给 WeakReference 即可变成弱援用。
  • 虚援用:虚援用并不会决定对象的生命周期,如果一个对象仅持有虚援用,那么它就和没有任何援用一样,在任何时候都可能被垃圾回收器回收。将对象交给 PhantomReference 即可变成虚援用。

办法区

办法的定义

Java 虚拟机标准中定义方法区是对的一个逻辑局部。办法区寄存以下信息:

  • 曾经被虚拟机加载的类的信息
  • 常量
  • 动态变量
  • 即时编译器编译后的代码

办法区的特点

  • 线程共享。办法区是堆的一个逻辑局部,因而和堆一样,都是线程共享的。整个虚拟机中只有一个办法区。
  • 永恒代。办法区中的信息个别都须要长期存在,而且它又是堆的逻辑分区,因而用堆的划分办法,把办法区称为“永恒代”。
  • 内存回收效率低。办法区中的信息个别须要长期存在,回收一遍之后可能只有大量信息有效。次要回收指标是:对常量池的回收;对类的卸载。
  • Java 虚拟机标准对办法区的要求比拟宽松。和堆一样,容许固定大小,也容许动静扩大,还容许不实现垃圾回收。

运行时常量池

办法区中寄存:类信息、常量、动态变量、即时编译器编译后的代码。常量就寄存在运行时常量池中。
当类被虚拟机加载后,.class文件中的常量就寄存在办法区的运行时常量池中。而且运行期间,能够向常量池中增加新的常量。入 String 类的 intern() 办法就能够在运行期间向常量池中增加字符串常量。

间接内存(堆外内存)

间接内存是除 Java 虚拟机之外的内存,但也能够被 Java 应用。

操作间接内存

在 NIO 中引入了一种基于通道和缓冲的 IO 形式。它能够通过调用本地办法间接调配 Java 虚拟机之外的内存,而后通过一个存储在堆中的 DirectByteBuffer 对象间接操作改内存,而无需先将内部内存中的数据复制到堆中再进行操作,从而进步了数据的操作效率。
间接内存的大小不受 Java 虚拟机管制,但既然是内存,当内存不足时就会抛出 OutOfMemoryError 异样。

间接内存与堆内存比拟

  • 间接内存申请空间更消耗性能。
  • 间接内存读取 IO 的性能要优于一般的堆内存。
  • 间接内存作用链:本地 IO> 间接内存 > 本地 IO
  • 堆内存作用链:本地 IO> 间接内存 > 非间接内存 > 间接内存 > 本地 IO

援用自:
https://doocs.gitee.io/jvm/01-jvm-memory-structure.html#%E6%96%B9%E6%B3%95%E7%9A%84%E8%B0%83%E7%94%A8

正文完
 0