JVM 内存模型与垃圾回收
1. JVM 内存模型
JVM内存模型1.1 程序计数器
- 概述: 该区域分配了一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 作用: 通过改变计数器的值来指定下一条需要执行的字节码指令。
- 特点: 1.线程私有 2. 唯一无内存溢出的区域
1.2 虚拟机栈 (JVM Stack)
- 概述: Java 方法执行的内存模型,每一个方法从调用直至执行完成的过程,对应着一个栈帧在虚拟机中从入栈到出栈的过程。
- 作用: 创建一个栈帧用于存储 局部变量表、操作数栈、动态链接、方法出口等信息; -Xss 调整栈大小
- 特点: 1.线程私有 2. 生命周期与线程执行结束一致
局部变量表
- 作用: 存放编译期可知的各种基本数据类型(8 种)、对象引用、returnAddress类型(指向了一条字节码指令的地址)。
- 占用空间:64 位bit长度的long 和double 类型占用两个局部变量空间(Slot),其余类型只占用一个。
- 分配时机:在编译期间完成分配,当进入一个方法时,该方法需要在帧中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量空间分配的大小。
虚拟机栈异常
- SOF 栈溢出:线程请求的栈深度大于虚拟机所允许的深度时抛出该异常。
- OOM 内存溢出:虚拟机允许栈深度动态扩展,但是扩展时无法申请到足够的内存。
1.3 本地方法栈
- 概述: 与虚拟机栈类似,是为虚拟机使用到的Native方法服务的内存区域。
- 区别:
- 虚拟机栈为虚拟机执行Java方法服务;
- 本地方法栈为虚拟机使用到的Native方法服务。
- 异常: 与虚拟机栈一致。
1.4 Java堆(Java Heap)
- 创建时间:JVM启动时创建该区域
- 占用空间:该区域是JVM管理内存最大一块
- 作用: 存放对象实例和数组
- 特点:
- 线程间共享
- GC主要区域
- 内存空间可扩展,通过设置 xms (最小值) xmx(最大值),通常设置成一样大,避免运行时频繁调整大小
1.5 方法区 (Method Area)
-
描述:虚拟机规范将它描述为堆的一个逻辑部分,但是它有个别名叫 Non-Heap,目的是与Java堆区分开来。
-
作用: 存放虚拟机加载的 类信息、常量、静态变量、即时编译器编译的代码等数据。
-
特点:
- 线程间共享
- 可通过 -XX:MaxPermSize 调整其大小
-
异常:方法区无法满足内存分配需求时抛出 OOM异常
1.6 运行时常量池
-
描述:方法区的一部分,Class文件中除了有类的(版本、字段、方法、接口)等描述信息外,还有一项信息是常量池。
-
作用: 存放编译器生成的各种字面量和符号引用。
-
动态性:
- Java语言并不要求常量池只有编译期才能产生,也就是并非预置入Class文件中的常量池内容后才能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中。这种特性用的比较广泛的便是String 类的intern()方法。
-
异常:方法区无法满足内存分配需求时抛出 OOM异常(与方法区一致)。
1.7 直接内存
-
描述:1.非虚拟机运行时数据区一部分,也不是Java虚拟机规范定义的内存区域。2. 频繁使用,可能导致OOM。
-
作用:
- 1.4开始引入NIO可以使用 Native函数库直接分配堆外内存;通过存储在Java堆中的DirectByteBuffer对象作为直接内存的引用,这样做的好处是可以显著提高性能,避免在java堆和 Native堆之间来回复制数据。
-
异常:收到物理内存限制,导致扩展时申请不到新的内存空间出现OOM
2. 垃圾回收 GC
2.1 回收的对象? 什么时候回收 ?
在垃圾回收之前,我们必须确定的一件事就是对象是否存活?这就牵扯到了判断对象是否存活的算法了。
引用计数算法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1,当引用失效,计数器-1.任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率高效,被actionscript3和python中广泛应用。
缺点:无法解决对象之间的相互引用问题。java没有采纳
可达性分析算法:
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的时候,则证明此对象是不可用的。
比如如下,右侧的对象是到GCRoot时不可达的,可以判定为可回收对象。
可达性分析算法在java中,可以作为GCRoot的对象包括以下几种:
虚拟机栈中引用的对象。
方法区中静态属性引用的对象。
方法区中常量引用的对象。
JNI(Native)方法中引用的对象。
例子:
类中的静态变量
public AClass{
public static Something;
public static final Apple;
''''''
}
Java方法的参数和方法中的局部变量,可以作为root.
public Aclass{
public void doSomething(Object A){
ObjectB b = new ObjectB;
}
}
基于以上,我们可以知道,当当前对象到GCRoot中不可达时候,即会满足被垃圾回收的可能。
那么是不是这些对象就非死不可,也不一定,此时只能宣判它们存在于一种“缓刑”的阶段,要真正的宣告一个对象死亡。至少要经历两次标记:
第一次:对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选。
第二次:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时会被认定为没必要执行。
2.2 如何回收? How?
上述的两点讲解之后,我们大概明白了,哪些对象会被回收,以及回收的依据是什么,但回收的这个工作实现起来并不简单,首先它需要扫描所有的对象,鉴别谁能够被回收,其次在扫描期间需要 ”stop the world“ 对象能被冻结,不然你刚扫描,他的引用信息有变化,你就等于白做了。
2.2.1 分代回收
我们从一个object1来说明其在分代垃圾回收算法中的回收轨迹。
1、object1新建,出生于新生代的Eden区域。
2、minor GC,object1 还存活,移动到Fromsuvivor空间,此时还在新生代。
3、minor GC,object1 仍然存活,此时会通过复制算法,将object1移动到ToSuv区域,此时object1的年龄age+1。
4、minor GC,object1 仍然存活,此时survivor中和object1同龄的对象并没有达到survivor的一半,所以此时通过复制算法,将fromSuv和Tosuv 区域进行互换,存活的对象被移动到了Tosuv。
5、minor GC,object1 仍然存活,此时survivor中和object1同龄的对象已经达到survivor的一半以上(toSuv的区域已经满了),object1被移动到了老年代区域。
6、object1存活一段时间后,发现此时object1不可达GcRoots,而且此时老年代空间比率已经超过了阈值,触发了majorGC(也可以认为是fullGC,但具体需要垃圾收集器来联系),此时object1被回收了。fullGC会触发 stop the world。
在以上的新生代中,我们有提到对象的age,对象存活于survivor状态下,不会立即晋升为老生代对象,以避免给老生代造成过大的影响,它们必须要满足以下条件才可以晋升:
1、minor gc 之后,存活于survivor 区域的对象的age会+1,当超过(默认)15的时候,转移到老年代。
2、动态对象,如果survivor空间中相同年龄所有的对象大小的综合和大于survivor空间的一半,年级大于或等于该年级的对象就可以直接进入老年代。
以上采用分代垃圾收集的思想,对一个对象从存活到死亡所经历的历程。期间,在新生代的时刻,会用到复制算法,在老年代时,有可能会用到标记-清楚算法(mark-sweep)算法或者标记-整理算法,这些都是垃圾回收算法基于不同区域的实现,我们看下这几种回收算法的实现原理。
2.2.2 垃圾回收算法
标记清除法(Mark-Sweep)
标记清除法是垃圾回收算法的思想基础。标记清除算法将垃圾分为两个阶段:标记阶段和清除阶段。
标记阶段,通过根节点,标记所有从根节点开始的可达对象,未标记过的对象就是未被引用的垃圾对象。
清除阶段,清除所有未被标记的对象。
标记-清除复制算法(Copying)
复制算法是,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在适用的内存中存活对象复制到未使用的内存块,然后清除使用的内存块中所有的对象。
复制算法IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存分为一块较大的Eden空间(比例8)和两块较小的Survivor空间(比例1),每次使用Eden和其中一块Survivore。
标记整理算法(Mark-Compact)
标记-整理标记压缩算法是一种老年代的回收算法。
标记阶段和标记清除算法一致,对可达对象做一次标记。
清理阶段,为了避免内存碎片产生,将所有的存活对象压缩到内存的一端。
2.2.3 垃圾收集器
垃圾收集器是内存回收的具体实现,不同的厂商提供的垃圾收集器有很大的差别,一般的垃圾收集器都会作用于不同的分代,需要搭配使用。以下是各种垃圾收集器的组合方式:
垃圾回收器搭配各种组合的优缺点:
新生代GC策略 | 老年代GC策略 | 说明 | |
---|---|---|---|
组合1 | Serial | Serial Old | Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。 |
组合2 | Serial | CMS+Serial Old | CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。 |
组合3 | ParNew | CMS | 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。 |
组合4 | ParNew | Serial Old | 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。 |
组合5 | Parallel Scavenge | Serial Old | Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。 |
组合6 | Parallel Scavenge | Parallel Old | 是Serial Old的并行版本 |
组合7 | G1GC | G1GC | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启-XX:MaxGCPauseMillis =50 #暂停时间目标-XX:GCPauseIntervalMillis =200 #暂停间隔目标-XX:+G1YoungGenSize=512m #年轻代大小-XX:SurvivorRatio=6 #幸存区比例 |
插入:各垃圾收集器的详细说明:
https://www.jianshu.com/p/ab49398328e9
https://www.jianshu.com/p/becfa1f2a9e9
2.3 新生代转移到老年代的触发条件?
长期存活的对象
大对象直接进入老年代
minor gc后,survivor仍然放不下
动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代
2.4 什么时候触发Full GC ?
空间分配检查和GC时机- 没有配置 -XX:+DisableExplicitGC情况下 System.gc()方法的调用
- 老年代空间不足:老年代最大可用空间小于历史平均水平
- 永生代空间不足(JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据),达到MaxMetaspaceSize阈值;
- GC时出现promotion failed和concurrent mode failure
- 统计得到的Minor GC晋升到旧生代平均大小大于老年代剩余空间
堆中分配很大的对象
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;
concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。