jvm 内存模型概述
一、Jvm 的介绍
1、JVM 体系结构
2、JVM 运行时数据区
3、JVM 内存模型
JVM 运行时内存 = 共享内存区 + 线程内存区
3.1、共享内存区
共享内存区 = 长久带(办法区 + 其余)+ 堆(Old Space + Young Space(den + S0 + S1))
长久代:
JVM 用长久带(Permanent Space)实现办法区,次要寄存所有已加载的类信息,办法信息,常量池等等。可通过 -XX:PermSize 和 -XX:MaxPermSize 来指定长久带初始化值和最大值。Permanent Space 并不等同于办法区,只不过是 Hotspot JVM 用 Permanent Space 来实现办法区而已,有些虚拟机没有 Permanent Space 而用其余机制来实现办法区。
堆 (heap):
次要用来寄存类的对象实例信息(包含 new 操作实例化的对象和定义的数组)。
堆分为 Old Space(又名,Tenured Generation)和 Young Space。Old Space 次要寄存应用程序中生命周期长的存活对象;Eden(伊甸园)次要寄存新生的对象;S0 和 S1 是两个大小雷同的内存区域,次要寄存每次垃圾回收后 Eden 存活的对象,作为对象从 Eden 过渡到 Old Space 的缓冲地带(S 是指英文单词 Survivor Space)。堆之所以要划分区间,是为了不便对象创立和垃圾回收.
3.2、线程内存区
线程内存区(JVM 栈):
线程内存区 = 单个线程内存 + 单个线程内存 +…….
单个线程内存 =PC Regster+JVM 栈 + 本地办法栈
JVM 栈 = 栈帧 + 栈帧 +…..
栈帧 = 局域变量区 + 操作数区 + 帧数据区
在 Java 中,一个线程会对应一个 JVM 栈 (JVM Stack),JVM 栈里记录了线程的运行状态。JVM 栈以栈帧为单位组成,一个栈帧代表一个办法调用。栈帧由三局部组成:局部变量区、操作数栈、帧数据区。
线程在栈区,不能共享数据,只能通过复制共享区的数据作为一块缓存,所有多线程写会有 bug,voliate 使得取到的数据不做缓存,是实时更新的。关键字 volatile 是轻量级的同步机制。
Volatile 变量对于 all 线程的可见性,指当一条线程批改了这个变量的值,新值对于其余 线程来说是可见的、立刻得悉的。Volatile 变量在多线程下不肯定平安,因为他只有可见性、有序性,然而没有原子性。
二、JVM 内存空间治理
JVM 把内存划分了如下几个区域:
共享内存区 = 长久带 (办法区 + 其余)+ 堆(Old Space + Young Space(den + S0 + S1));
Java 内存模型和线程:
每个线程都有一个工作内存,线程只能够批改本人工作内存中的数据,而后再同步回主内存,主内存由多个内存共享。
2.1 办法区(共享内存区的长久带)
办法区 (又称为长久代):要加载的类的信息(名称、修饰符等)、类中的动态变量、类中定义为 final 类型的常量、类中的 Field 信息、类中的办法信息。办法区域也是全局共享的,当开发人员调用类对象中的 getName、isInterface 等办法来获取信息时,这些数据都来源于办法区。
在肯定条件下它也会被 GC,当办法区域要应用的内存超过其容许的大小时,会抛出 OutOfMemory:PermGen Space 异样。的错误信息。在 Sun JDK 中这块区域对应 Permanet Generation,,默认最小值为 16MB,最大值为 64MB,可通过 -XX:PermSize 及 -XX:MaxPermSize 来指定最小值和最大值。
在 Hotspot 虚拟机中,这块区域对应的是 Permanent Generation(长久代),个别的,办法区上执行的垃圾收集是很少的,因而办法区又被称为长久代的起因之一,但这也不代表着在办法区上齐全没有垃圾收集,其上的垃圾收集次要是针对常量池的内存回收和对已加载类的卸载。在办法区上进行垃圾收集,条件刻薄而且相当艰难,对于其回前面再介绍。
运行时常量池(Runtime Constant Pool)是办法区的一部分,用于存储编译期就生成的字面常量、符号援用、翻译进去的间接援用(符号援用就是编码是用字符串示意某个变量、接口的地位,间接援用就是依据符号援用翻译进去的地址,将在类链接阶段实现翻译);
运行时常量池除了存储编译期常量外,也能够存储在运行工夫产生的常量,比方 String 类的 intern()办法,作用是 String 保护了一个常量池,如果调用的字符“abc”曾经在常量池中,则返回池中的字符串地址,否则,新建一个常量退出池中,并返回地址。JVM 办法区的相干参数,最小值:–XX:PermSize;最大值 –XX:MaxPermSize。
2.2 堆区 (堆区由所有线程共享)
堆用于存储对象实例及数组值,能够认为 Java 中所有通过 new 创立的对象的内存都在此调配,堆区由所有线程共享。Heap 中对象所占用的内存由 GC 进行回收,在 32 位操作系统上最大为 2GB,在 64 位操作系统上则没有限度,其大小可通过 -Xms 和 -Xmx 来管制,-Xms 为 JVM 启动时申请的最小 Heap 内存,默认为物理内存的 1 /64 但小于 1GB;-Xmx 为 JVM 可申请的最大 Heap 内存,默认为物理内存的 1 / 4 但小于 1GB,默认当空余堆内存小于 40% 时,JVM 会增大 Heap 到 -Xmx 指定的大小,可通过 -XX:MinHeapFreeRatio= 来指定这个比例;当空余堆内存大于 70% 时,JVM 会减小 Heap 的大小到 -Xms 指定的大小,可通过 -XX:MaxHeapFreeRatio= 来指定这个比例,对于运行零碎而言,为防止在运行时频繁调整 Heap 的大小,通常将 -Xms 和 -Xmx 的值设成一样。
堆区是了解 JavaGC 机制最重要的区域。在 JVM 所治理的内存中,堆区是最大的一块,堆区也是 JavaGC 机制所治理的次要内存区域,堆区由所有线程共享,在虚拟机启动时创立。堆区用来存储对象实例及数组值,能够认为 java 中所有通过 new 创立的对象都在此调配。
2.3 本地办法栈(Native Method Stack)
本地办法栈用于反对 native 办法的执行,存储了每个 native 办法调用的状态。本地办法栈和虚拟机办法栈运行机制统一,它们惟一的区别就是,虚拟机栈是执行 Java 办法的,而本地办法栈是用来执行 native 办法的,在很多虚拟机中(如 Sun 的 JDK 默认的 HotSpot 虚拟机),会将本地办法栈与虚拟机栈放在一起应用。
2.4 虚拟机栈(JVM Stack)(线程公有)
JVM 办法栈:为线程公有,其在内存调配上十分高效。当办法运行结束时,其对应的栈帧所占用的内存也会主动开释。当 JVM 办法栈空间有余时,会抛出 StackOverflowError 的谬误,在 Sun JDK 中能够通过 -Xss 来指定其大小。
虚拟机栈占用的是操作系统内存,每个线程都对应着一个虚拟机栈,它是线程公有的,而且调配十分高效。一个线程的每个办法在执行的同时,都会创立一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动静链接、办法进口等,当办法被调用时,栈帧在 JVM 栈中入栈,当办法执行实现时,栈帧出栈。
局部变量表中存储着办法的相干局部变量,包含各种根本数据类型,对象的援用,返回地址等。在局部变量表中,只有 long 和 double 类型会占用 2 个局部变量空间(Slot,对于 32 位机器,一个 Slot 就是 32 个 bit),其它都是 1 个 Slot。须要留神的是,局部变量表是在编译时就曾经确定好的,办法运行所须要调配的空间在栈帧中是齐全确定的,在办法的生命周期内都不会扭转。
虚拟机栈中定义了两种异样,如果线程调用的栈深度大于虚拟机容许的最大深度,则抛出 StatckOverFlowError(栈溢出);不过少数 Java 虚拟机都容许动静扩大虚拟机栈的大小(有少部分是固定长度的),所以线程能够始终申请栈,直到内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。
2.5 程序计数器(Program Counter Register)(线程公有)
程序计数器是一个比拟小的内存区域,可能是 CPU 寄存器或者操作系统内存,其次要用于批示以后线程所执行的字节码执行到了第几行,能够了解为是以后线程的行号指示器。字节码解释器在工作时,会通过扭转这个计数器的值来取下一条语句指令。每个程序计数器只用来记录一个线程的行号,所以它是线程公有(一个线程就有一个程序计数器)的。
如果程序执行的是一个 Java 办法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由 C 语言编写实现)办法,则计数器的值为 Undefined,因为程序计数器只是记录以后指令地址,所以不存在内存溢出的状况,因而,程序计数器也是所有 JVM 内存区域中惟一一个没有定义 OutOfMemoryError 的区域。
三、内存溢出与内存透露
内存透露:调配进来的内存回收不了
内存溢出:指零碎内存不够用了
1、堆溢出
能够分为:内存透露和内存溢出,这两种状况都会抛出 OutOfMemoryError:java heap space 异样:
a、内存透露:
内存透露是指对象实例在新建和应用结束后,依然被援用,没能被垃圾回收开释,始终积攒,直到没有残余内存可用。如果内存泄露,咱们要找出泄露的对象是怎么被 GC ROOT 援用起来,而后通过援用链来具体分析泄露的起因。剖析内存透露的工具有:Jprofiler,visualvm 等。
public class OOMTest {
public static void main(String[] args) {
List<UUID> list = new ArrayList<UUID>();
while(true){list.add(UUID.randomUUID());
}
}
}
看看控制台的输入后果,因为我这边的 JVM 设置的参数内存足够大,所以须要期待肯定的工夫,能力看到成果:
b、内存溢出
内存溢出是指当咱们新建一个实力对象时,实例对象所需占用的内存空间大于堆的可用空间。如果呈现了内存溢出问题,这往往是程序本生须要的内存大于了咱们给虚拟机配置的内存,这种状况下,咱们能够采纳调大 -Xmx 来解决这种问题。
public class OOMTest_1 {
public static void main(String args[]){List<byte[]> byteList = new ArrayList<byte[]>();
byteList.add(new byte[1000 * 1024 * 1024]);
}
}
2、栈溢出
栈(JVM Stack)寄存次要是栈帧 (局部变量表, 操作数栈 , 动静链接 , 办法进口信息) 的中央。留神辨别栈和栈帧:栈里蕴含栈帧。
与线程栈相干的内存异样有两个::
a:StackOverflowError(办法调用档次太深,内存不够新建栈帧)
b:OutOfMemoryError(线程太多,内存不够新建线程)
a、java.lang.StackOverflowError
栈溢出抛出 java.lang.StackOverflowError 谬误,呈现此种状况是因为办法运行的时候,申请新建栈帧时,栈所剩空间小于栈帧所需空间。例如,通过递归调用办法, 不停的产生栈帧, 始终把栈空间堆满, 直到抛出异样:
public class SOFTest {
public void stackOverFlowMethod(){stackOverFlowMethod();
}
/**
* 通过递归调用办法, 不停的产生栈帧, 始终把栈空间堆满, 直到抛出异样:* @param args
*/
public static void main(String[] args) {SOFTest sof = new SOFTest();
sof.stackOverFlowMethod();}
}
b、OutOfMemoryError(暂不介绍)
四、JVM 内存调配
Java 对象所占用的内存次要在堆上实现,因为堆是线程共享的,因而在堆上分配内存时须要进行加锁,这就导致了创建对象的开销比拟大。当堆上空间有余时,会登程 GC,如果 GC 后空间依然有余,则会抛出 OutOfMemory 异样。
为了晋升内存调配效率,在年老代的 Eden 区 HotSpot 虚拟机应用了两种技术来放慢内存调配,别离是 bump-the-pointer 和 TLAB(Thread-Local Allocation Buffers)。因为 Eden 区是间断的,因而 bump-the-pointer 技术的外围就是跟踪最初创立的一个对象,在对象创立时,只须要查看最初一个对象前面是否有足够的内存即可,从而大大放慢内存调配速度;而对于 TLAB 技术是对于多线程而言的,它会为每个新创建的线程在新生代的 Eden Space 上调配一块独立的空间,这块空间称为 TLAB(Thread Local Allocation Buffer),其大小由 JVM 依据运行状况计算而得。可通过 -XX:TLABWasteTargetPercent 来设置其可占用的 Eden Space 的百分比,默认是 1%。在 TLAB 上分配内存不须要加锁,个别 JVM 会优先在 TLAB 上分配内存,如果对象过大或者 TLAB 空间曾经用完,则依然在堆上进行调配。因而,在编写程序时,多个小对象比大的对象调配起来效率更高。可在启动参数上减少 -XX:+PrintTLAB 来查看 TLAB 空间的应用状况。
对象如果在年老代存活了足够长的工夫而没有被清理掉(即在几次 Minor GC 后存活了下来),则会被复制到年轻代,年轻代的空间个别比年老代大,能寄存更多的对象,在年轻代上产生的 GC 次数也比年老代少。当年老代内存不足时,将执行 Major GC,也叫 Full GC。
能够应用 -XX:+UseAdaptiveSizePolicy 开关来管制是否采纳动静控制策略,如果动态控制,则动静调整 Java 堆中各个区域的大小以及进入老年代的年龄。如果对象比拟大(比方长字符串或大数组),年老代空间有余,则大对象会间接调配到老年代上(大对象可能触发提前 GC,应少用,更应防止应用长寿的大对象)。用 -XX:PretenureSizeThreshold 来管制间接升入老年代的对象大小,大于这个值的对象会间接调配在老年代上。
五、内存的回收形式
1、收集器:援用计数收集器、跟踪收集器
1、援用计数收集器:
在上图中,ObjectA 开释了对 ObjectB 的援用后,ObjectB 的援用计数器变为 0,此时可回收 ObjectB 所占有的内存。援用计数器须要在每次对象赋值时进行援用计数器的增减,他有肯定耗费。另外,援用计数器对于循环援用的场景没有方法实现回收。例如在下面的例子中,如果 ObjectB 和 ObjectC 相互援用,那么即便 ObjectA 开释了对 ObjectB 和 ObjectC 的援用,也无奈回收 ObjectB、ObjectC,因而对于 java 这种会造成简单援用关系的语言而言,援用计数器是十分不适宜的,SunJDK 在实现 GC 时也未采纳这种形式。
2、跟踪收集器实现算法:
跟踪收集器采纳的为集中式的治理形式,会全局记录数据援用的状态。基于肯定条件的触发(例如定时、空间有余时),执行时须要从根汇合来扫描对象的援用关系,这可能会造成应用程序暂停。次要有复制(Copying):年老代的 Eden 区、标记 - 革除(Mark-Sweep)和标记 - 压缩(Mark-Compact)三种实现算法。
1、复制:
特色:当要回收的空间中存活对象较少时,复制算法会比拟高效,其带来的老本是要减少一块空的内存空间及进行对象的挪动。
进行 - 复制算法:它将可用内存依照容量划分为大小相等的两块,每次只应用其中一块。当这一块的内存用完了,则就将还存活的对象复制到另一块下面,而后再把曾经应用过的内 存空间一次清理掉。商业虚拟机:将内存分为一块较大的 eden 空间和两块较小的 survivor 空间,默认比例是 8:1:1,即每次新生代中可用内存空间为整个新生代容量的 90%,每次使 用 eden 和其中一个 survivour。当回收时,将 eden 和 survivor 中还存活的对象一次性复制到 另外一块 survivor 上,最初清理掉 eden 和方才用过的 survivor,若另外一块 survivor 空间没有足够内存空间寄存上次新生代收集下来的存活对象时,这些对象将间接通过调配担保机制进入老年代。
复制采纳的形式为从根汇合扫描出存活的对象,并将找到的存活的对象复制到一块新的齐全未被应用的空间中,如图所示:
复制收集器形式仅须要从根汇合扫描所有存活对象,当要回收的空间中存活对象较少时,复制算法会比拟高效(年老代的 Eden 区就是采纳这个算法),其带来的老本是要减少一块空的内存空间及进行对象的挪动。
2、标记 - 革除:
特色:在空间中存活对象较多的状况下较为高效,但因为标记 - 革除采纳的为间接回收不存活对象所占用的内存,因而会造成内存碎片。
标记 - 革除采纳的形式为从根汇合开始扫描,对存活的对象进行标记,标记结束后,再扫描整个空间中未标记的对象,并进行革除,标记和革除过程如下图所示:
上图中蓝色的局部是有被援用的存活的对象,褐色局部没被援用的可回收的对象。在 marking 阶段为了 mark 对象,所有的对象都会被扫描一遍,扫描这个过程是比拟耗时的。
革除阶段回收的是没有被援用的对象,存活的对象被保留。内存分配器会持有闲暇空间的援用列表,当有调配申请时会查问闲暇空间援用列表进行调配。标记 - 革除动作不须要进行对象挪动,且仅对其不存活的对象进行解决。在空间中存活对象较多的状况下较为高效,但因为标记 - 革除间接回收不存活对象占用的内存,因而会造成内存碎片。
3、标记 - 压缩
特色:在标记 - 革除的根底上还须进行对象的挪动,老本绝对更高,益处则是不产生内存碎片。
标记 - 压缩和标记 - 革除一样,是对活的对象进行标记,然而在革除后的解决不一样,标记 - 压缩在革除对象占用的内存后,会把所有活的对象向左端闲暇空间挪动,而后再更新援用其对象的指针,如下图所示:
很显著,标记 - 压缩在标记 - 革除的根底上对存活的对象进行了挪动规整动作,解决了内存碎片问题,失去更多间断的内存空间以进步调配效率,但因为须要对对象进行挪动,因而老本也比拟高。
总结:
JVM 通过 GC 来回收堆和办法区中的内存,这个过程是主动执行的。说到 Java GC 机制,其次要实现 3 件事:确定哪些内存须要回收;确定什么时候须要执行 GC;如何执行 GC。JVM 次要采纳收集器的形式实现 GC,次要的收集器有援用计数收集器和跟踪收集器。
垃圾回收算法:1. 援用计数算法 2. 追踪回收算法 3. 压缩回收算法 4. 复制回收算法 5. 按代回收算法。为什么要按代回收。Java 对象的生命周期个别不长。
6、虚拟机中的 GC 过程
6.1 为什么要分代回收?
在一开始的时候,JVM 的 GC 就是采纳标记 - 革除 - 压缩形式进行的,这么做并不是很高效,因为当对象调配的越来越多时,对象列表也越来也大,扫描和挪动越来越耗时,造成了内存回收越来越慢。然而,通过依据对 java 利用的剖析,发现大部分对象的存活工夫都十分短,只有少部分数据存活周期是比拟长的,
分代收集:
新生代 进行 - 复制算法
老年代 标记 - 清理或标记 - 革除
6.2 虚拟机中 GC 的过程
通过下面介绍,咱们曾经晓得了 JVM 为何要分代回收,上面咱们就具体看一下整个回收过程。
1 在初始阶段,新创建的对象被调配到 Eden 区,survivor 的两块空间都为空。
当 Eden 区满了的时候,minor garbage(Minor GC 年老代垃圾回收机制) 被触发
2 通过扫描与标记,存活的对象被复制到 S0,不存活的对象被回收
3 在下一次的 Minor GC 中,Eden 区的状况和下面统一,没有援用的对象被回收,存活的对象被复制到 survivor 区。然而在 survivor 区,S0 的所有的数据都被复制到 S1,须要留神的是,在上次 minor GC 过程中挪动到 S0 中的两个对象在复制到 S1 后其年龄要加 1。此时 Eden 区 S0 区被清空,所有存活的数据都复制到了 S1 区,并且 S1 区存在着年龄不一样的对象,过程如下图所示:
4 再下一次 MinorGC 则反复这个过程,这一次 survivor 的两个区对换,存活的对象被复制到 S0,存活的对象年龄加 1,Eden 区和另一个 survivor 区被清空。
5 上面演示一下 Promotion 过程,再通过几次 Minor GC 之后,当存活对象的年龄达到一个阈值之后(可通过参数配置,默认是 8),就会被从年老代 Promotion 到老年代。
6 随着 MinorGC 一次又一次的进行,一直会有新的对象被 promote 到老年代。
7 下面基本上笼罩了整个年老代所有的回收过程。最终,MajorGC 将会在老年代产生,老年代的空间将会被革除和压缩。
总结:
从下面的过程能够看出,Eden 区是间断的空间,且 Survivor 总有一个为空。通过一次 GC 和复制,一个 Survivor 中保留着以后还活着的对象,而 Eden 区和另一个 Survivor 区的内容都不再须要了,能够间接清空,到下一次 GC 时,两个 Survivor 的角色再调换。因而,这种形式分配内存和清理内存的效率都极高,这种垃圾回收的形式就是驰名的“进行 - 复制(Stop-and-copy)”清理法(将 Eden 区和一个 Survivor 中依然存活的对象拷贝到另一个 Survivor 中),这不代表着进行复制清理法很高效,其实,它也只在这种状况下(基于大部分对象存活周期很短的事实)高效,如果在老年代采纳进行复制,则是十分不适合的。老年代存储的对象比年老代多得多,而且不乏大对象,对老年代进行内存清理时,如果应用进行 - 复制算法,则相当低效。个别,老年代用的算法是标记 - 压缩算法,即:标记出依然存活的对象(存在援用的),将所有存活的对象向一端挪动,以保障内存的间断。在产生 Minor GC 时,虚构机会查看每次降职进入老年代的大小是否大于老年代的残余空间大小,如果大于,则间接触发一次 Full GC,否则,就查看是否设置了 -XX:+HandlePromotionFailure(容许担保失败),如果容许,则只会进行 MinorGC,此时能够容忍内存调配失败;如果不容许,则依然进行 Full GC(这代表着如果设置 -XX:+Handle PromotionFailure,则触发 MinorGC 就会同时触发 Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。
对于办法区(共享内存区的长久代)即永恒代的回收,永恒代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简略,没有援用了就能够被回收。对于无用的类进行回收,必须保障 3 点:
类的所有实例都曾经被回收
加载类的 ClassLoader 曾经被回收
类对象的 Class 对象没有被援用(即没有通过反射援用该类的中央)
永恒代的回收并不是必须的,能够通过参数来设置是否对类进行回收。