JVM 内存构造
Java 虚拟机的内存空间分为 5 个局部:
- 程序计数器
- Java 虚拟机栈
- 本地办法栈
- 堆
- 办法区
JDK 1.8 同 JDK 1.7 比,最大的差异就是:元数据区取代了永恒代。元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。不过元空间与永恒代之间最大的区别在于:元数据空间并不在虚拟机中,而是应用本地内存。
程序计数器(PC 寄存器)
程序计数器的定义
程序计数器是一块较小的内存空间,是以后线程正在执行的那条字节码指令的地址。若以后线程正在执行的是一个本地办法,那么此时程序计数器为Undefined
。
程序计数器的作用
- 字节码解释器通过改变程序计数器来顺次读取指令,从而实现代码的流程管制。
- 在多线程状况下,程序计数器记录的是以后线程执行的地位,从而当线程切换回来时,就晓得上次线程执行到哪了。
程序计数器的特点
- 是一块较小的内存空间。
- 线程公有,每条线程都有本人的程序计数器。
- 生命周期:随着线程的创立而创立,随着线程的完结而销毁。
- 是惟一一个不会呈现
OutOfMemoryError
的内存区域。
Java 虚拟机栈(Java 栈)
Java 虚拟机栈的定义
Java 虚拟机栈是形容 Java 办法运行过程的内存模型。
Java 虚拟机栈会为每一个行将运行的 Java 办法创立一块叫做“栈帧”的区域,用于寄存该办法运行过程中的一些信息,如:
- 局部变量表
- 操作数栈
- 动静链接
- 办法进口信息
- ……
压栈出栈过程
当办法运行过程中须要创立局部变量时,就将局部变量的值存入栈帧中的局部变量表中。
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 Survior
,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 中残余的对象移到 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
异样。
本文由 mdnice 多平台公布