《深入理解Java虚拟机》
第二章 Java内存区域与内存溢出异常
JAVA运行时区域
参考:一、JVM之内存模型 http://www.jianshu.com/p/8dc1c291c972
分为五个部分:
1. 方法区
各个线程共享的内存区域
用于存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等等数据。
运行时常量池:存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池存放。
很多人将其称为永久代
2. 堆
各个线程共享的内存区域
所有的对象实例以及数组都要在堆上分配,Java堆是Java虚拟机所管理的内存中的最大的一块,是垃圾收集器管理的主要区域。
垃圾收集器基本都采用分代收集算法,所以可细分为:新生代和老年代。
可能划分出多个线程私有的分配缓存区(TLAB),为了更好的回收内存或分配内存。
3.虚拟机栈
线程私有,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操纵数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。
局部变量表:存放了编译器可知的各种基本数据类型,对象引用和returnAddress(指向一条字节码指令的地址)。
4. 本地方法栈
与虚拟机栈的作用相似,两者的区别虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机用到的Native方法服务。
5. 程序计数器
一块较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器来完成。
每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。
JVM创建JAVA对象
JVM 如何创建Java对象 http://blog.csdn.net/u010723709/article/details/47281349
1 首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查该符号引用所引用的类是否被加载、解析、初始化;如果没有,执行类加载过程
2 虚拟机为新对象分配内存,两种方法:指针碰撞(内存绝对规整)、空闲列表(非绝对规整),内存是否规整取决于垃圾回收期是否带有压缩整理功能
3 并发时可能出现问题:正在给A对象分配内存,指针还未修改,对象B又同时使用原指针分配内存。两种解决方案:
1.CAS配上失败重试,保证更新操作的原子性;
2.为每个线程预分配一小块内存(本地线程分配缓存TLAB),分配对象内存时在TLAB上。只有TLAB分配完了再分配新的TLAB,这时需要同步加锁。启动TLAB:-XX:=/-UseTLAB
4 对象内存空间初始化为零值(不包括对象头);若使用TLAB,可以提前至TLAB分配时进行
5 虚拟机设置对象头,包括对象是那个类的实例,如何找到元信息、哈希值、对象的GC分代年龄等,存放在对象头。根据虚拟机当前运行状态,还需设置对象头,比如是否启用偏向锁。
6 执行new指令之后执行<init>方法,按照程序员的要求进行初始化
对象内存布局
1 包括3块区域:对象头、实例数据、对其填充
2 对象头:
两部分信息:
1.存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳,被称为Mark Word。该数据部分长度32bit或64bit(对应32位机器、64为机器)
2.类型指针,即对象指向它的类元数据的指针,虚拟机需要通过该指针确定该对象是哪个类的实例,如果对象是数组,对象头还需记录数据长度,虚拟机可从普通对象的元数据知道其大小
3 实例数据,就是程序代码中定义的各种类型的字段内容。包括从父类中继承的在子类的前面
4 对齐填充,非必须,HotSpot虚拟机要求对象大小必须是8字节的倍数
对象的访问定位
1 通过栈上的reference数据操作堆上的具体对象
2 主流的访问方式2种:
1.使用句柄,在Java对中划分出一块内存作为句柄池,reference数据存放对象的句柄地址,句柄中包含对象实例数据地址与类型数据的地址
2.直接使用指针访问,使用该方法Java堆对象需放置类型数据的相关信息,reference中存放对象的地址
3 句柄方式的优点是稳定(对象被移动不要修改),直接指针的优势是快(节省一次指针定位的时间开销)。HotSpot虚拟机采用第二种方式
内存异常
1 堆溢出
1.参数:-Xms:堆最小值;-Xmx:堆最大值
2.内存溢出与内存泄露(指程序在申请内存后,无法释放已申请的内存空间)
2 虚拟机栈和本地方法栈溢出
1.参数:-Xoss:设置本地方法栈(实际无效);-Xss :设置栈容量(只有它有效)
2.StackOverflowError:线程请求栈深度大于虚拟机所允许的最大深度;
3.OutMemoryError:虚拟机栈扩展时无法申请到足够内存
4. 两种异常存在重叠,本质一样。单线程下一般是.StackOverflowError,多线程下OutMemoryError。因为这时内存溢出与栈空间大小无关系。为每个线程分配的栈内存越大,反而越容易出现。
操作系统为每个进程分配内存有限制。出去堆、方法区等就是栈内存,每个线程的栈越大能建立的线程数量越少
3 方法区和运行时常量池溢出
1. String.intern() 是一个Native方法:如果字符串常量池中包含一个等于此String对象的字符串,则返回代表池中 这个字符串的String对象,否则将此String对象包含的字符串添加到常量池
2. 参数-XX:PermSize和-XX:MaxPermSize,限制方法区的大小
3. jdk1.6及之前常量池分配在永久带,可以通过限制方法区的大小,从而间接限制常量池的容量。jdk7之后逐渐“去永久带”。
4. jdk1.6中intern() 方法将首次遇到的字符串实例复制到永久代中,返回永久代中实例的引用。jdk7不会复制实例,只是在常量池中记录首次出现实例的引用。
4 本机直接内存溢出
1.参数:-XX:MaxDirectSize,若不指定默认和Java堆最大值一样
第三章 垃圾收集器与内存分配策略
参考:http://www.jianshu.com/p/0ef07636796e
3.1 GC概述
GC(Garbage Collection)机制,是Java与C++/C的主要区别之一,Java开发者,一般不需要单独处理内存的回收,GC会负责内存的释放。
java运行时区域中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、线程而灭,Java堆、方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
3.2 对象已死吗
1 引用计数算法
算法通常是给对象中添加一个引用计数器,如果计数器值为0,对象就不会被再使用,就可以回收该对象了。
但该算法很难解决循环引用的问题。
2 可达性分析算法
在主流的商用程序语言(Java、C#)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。
通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
GC Roots对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈JNI(即一般说的Native方法)引用的对象
引用细分4种:
1.强引用,最常见的,只要强引用还在,垃圾回收器不会回收对象(Strong Reference)
2.软引用,有用但非必须,当系统将要发生内存溢出之前,对这些对象进行回收(SoftReference)
3.弱引用,更弱,只能生存到下一次垃圾收集之前(WeakReference)
4.虚引用,对对象的生存时间无影象,无法通过它取得一个对象实例,唯一目的是回收时接到一个系统通知(Phantom Reference)
3.2.4 生存还是死亡
一个对象死亡,至少要经历两次标记过程。
1.可到达性分析中不可到达的对象,不是马上死亡,第一次标记:筛选对象是否有必要执行 finalize() 方法,当对象没有覆盖,或者已经被执行过,就是为没有必要执行
2.当对象被判定有必要执行finalize()方法,就将对象放入一个F-Queue队列中,等待虚拟机自建的Finalizer线程执行。如果对象在finalize()中自救(只要将自己重新与引用链上的对象建立关联)就进行第二次标记将他移出“即将回收“集合,如果还没有逃脱就真正回收。
注意:一个对象的finalize()方法都只会被系统自动调用一次。
3.2.5 回收方法区
方法区(永久代)的垃圾回收主要有两个部分:废弃常量和无用的类。
判断废弃常量:比如,没有任何对象引用常量池中的字面量,则会被清理出常量池。常量池中的其他的符号引用类似。
判断无用的类:满足3个条件:
1 该类的所有实例都被回收
2 加载该类的ClassLoader已经被回收
3 该类对应的java.lang.Class对象没有被引用
3.3 垃圾收集算法
1 标记-清除算法
首先标记出所有需要回收的对象,再标记完成后统一回收所有被标记的对象。缺点效率不高,且容易产生内存碎片。
2 复制算法
为了解决效率问题,“复制”(Copying)的收集算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:实现简单,运行高效,缺点:将内存“浪费”。
现在的商业虚拟机都采用这种收集算法来回收新生代。新生代对象回收率高。
实际做法:一般是将内存分为一块较大的Eden空间和两块较小的Survivor空间,Hotspot默认比例8:1,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。当Survivor空间不够时,这些对象将通过分配担保机制进入老年代。
3 标记-整理算法
对象存活率高时复制算法效率变低,所以不适用老年代。标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4 分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法。将内存根据对象存活周期划分为几块,一般将Java堆分为新生代和老年代。新生代中对象每次收集时大量对象死去,使用复制算法;老年代中对象存活率高使用标记-清理或标记-整理算法
3.4 HotSpot的算法实现
3.4.1 枚举根节点
可达性分析对时间的敏感有两类:1 枚举根节点仅仅方法区就非常大 2 为了分析时对象引用关系的一致性,GC进行时必须停顿所有的Java执行线程
解决方法:采用准确式GC,直接得知哪些地方存放着对象引用,而不是数据。HotSpot中使用OopMap数据结构实现。在JIT编译过程中,也会在特定位置记录栈和寄存器中哪些位置是引用。
3.4.2 安全点
HotSpot没有为每条指令都生成OopMap,只在“特定的位置”记录这些信息,这些位置称为安全点(Safepoint)。
安全点基本上是以“是否具有让程序长时间执行的特征”为标准选定的。
解决GC时如何让所有线程(除了JNI调用的线程)跑到安全点上再停止,有两个方案:
- 抢先式中断:把所有线程全部中断,发现线程不在安全点上再恢复线程,运行到安全点。
- 主动式中断:不对线程操作,仅仅简单地设置一个标志,各个线程主动轮询该标志,发现中断标志为真则自己中断挂起。
3.4.3 安全区域
安全区域(Safe Region)指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方GC都是安全的。例子:线程处于Sleep状态或者Blocked状态,无法响应JVM的中断请求。
线程进入安全区域,首先标记自己已经进入,GC就不会管理该线程。线程要离开安全区域,检测系统是否已经完成根节点枚举(或者整个GC),未完成则等待收到可以离开安全区域的信号。
3.5 垃圾收集器
参考: http://www.jianshu.com/p/50d5c88b272d
垃圾收集器是内存回收的具体实现。
3.5.1 Serial 收集器
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
特点:这个收集器是一个单线程的收集器,Stop The World
应用场景:Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
3.5.2 ParNew收集器
特点:ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
3.5.3 Parallel Scavenge收集器
特点:Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
应用场景:目标是达到一个可控制的吞吐量。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
3.5.4 Serial Old收集器
特点:Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
应用场景:
- Client模式
Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
- Server模式
如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
3.5.5 Parallel Old收集器
特点:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
应用场景:在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
3.5.6 CMS收集器
特点:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
应用场景:
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
算法:
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:
- 初始标记(CMS initial mark)
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记(CMS concurrent mark)
并发标记阶段就是进行GC Roots Tracing的过程。
- 重新标记(CMS remark)
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
- 并发清除(CMS concurrent sweep)
并发清除阶段会清除对象。
于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
3个缺点:
- CMS收集器对CPU资源非常敏感:
在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
- CMS收集器无法处理浮动垃圾:
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
- CMS收集器会产生大量空间碎片:
基于“标记—清除”算法,收集结束时会有大量空间碎片产生。会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
3.5.7 G1收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。
特点:
并行与并发:
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者 CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的 GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:
与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其 他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已 经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:
与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实 现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这 两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种 特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一 次GC。
可预测的停顿:
这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关 注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一 个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实 时Java(RTSJ)的垃圾收集器的特征了。
执行过程:
G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Marking)
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking)
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking)
最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(Live Data Counting and Evacuation)
筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
概念理解
- 并发和并行
这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
- Minor GC 和 Full GC
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
- 吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
内存分配与回收策略
对象的内存分配,大方向上说就是在堆上分配,主要分配在新生代的Eden区上,如果启用本地线程分配缓冲,优先在TLAB上分配。少数情况下也可直接分配在老年代。
(1)对象优先分配在Eden上,需要eden可存下
(2)大对象直接进入老年代,指需要大量连续内存空间的对象,典型的大对象:长字符串及数组
(3)长期存活的对象进入老年代;虚拟机为每个对象定义一个对象年龄计数器。若对象在Eden中出生第一次Minor GC仍然活着进入Survior,设年龄为1,再Survior中每熬过一个Minor GC。年龄加1,当年龄到一定值(默认15)就进入老年代,年龄阈值可设置。
(4)动态对象年龄判定,当Survior中相同年龄的对象的大小和大于Survior的一半,年龄大于等于该年龄的对象直接进入老年代。
(5)空间分配担保,在发送Minor GC之前虚拟机先检查老年代最大可用连续空间是否大于新生代所有对象总空间,若大于,Minor GC是安全的;若小于,虚拟机查看 HandlePromotionFailture 设置值是否允许担保失败,若允许就检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,若大于,就尝试进行一次Minor GC;若小于或不允许担保失败就改为进行一次Full GC。
问题回答参考 http://icyfenix.iteye.com/blog/715301
知识精简
JAVA运行时区域分五个部分
1 方法区(永久代 常量池)
2 堆
3 虚拟机栈
4 本地方法栈
5 程序计数器
JVM创建对象的步骤
1 定位符号引用,
2 分配内存,法一:指针碰撞 法二:空闲列表;用CAS或者TLAB缓存解决并发问题
3 初始化对象内存空间
4 设置对象头(哈希值、元信息、GC分代年龄等)
5 执行<init>方法
JAVA的访问定位有两种方法
1 使用句柄
2 指针访问
判断对象是否可用的两种方法
1 引用计数法 有循环引用问题
2 可达性分析法 GC Roots
GC Roots对象
1 虚拟机栈中引用的对象
2 方法区中静态属性引用的对象
3 方法区中常量引用的对象
4 JNI中引用的对象
引用分为4中
1 强引用
2 软引用 内存溢出之间
3 弱引用 下次GC前
4 虚引用 无法获得对象实例,用于接收到系统通知
回收方法区
1 废弃常量 2 无用的类
收集算法
标记-清除算法
复制算法:将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上
标记-整理算法
分代收集算法
收集器对比
Serial收集器
新生代 单线程
优:没有线程交互的开销,简单而高效
缺:全程STW(Stop The World) 单线程
步骤:单线程复制算法
ParNew收集器
新生代 Serial的多线程版
优:除Serial只有它能与CMS收集器配合 多CPU环境更有效
缺:单CPU环境不如Serial;全程STW
步骤:并行复制算法
Parallel Scavenge收集器
新生代 并行多线程
优:可控制的吞吐量,适合后台运算;自适应调节策略(停顿时间、吞吐量)
缺点:吞吐量优先,停顿时间变长;全程STW
步骤:并行复制算法
Serial Old收集器
Serial的老年代版本 单线程
优:Server模式下,在CMS收集器并发收集发生Concurrent Mode Failure时使用
缺点:全程STW
步骤:标记-整理算法
Parallel Old收集器
Parallel Scavenge的老年代版本 多线程
优:适合注重吞吐量以及CPU资源敏感的场合;可配合Parallel Scavenge收集器
缺点:全程STW
步骤:标记-整理算法
CMS收集器
老年代 多线程
优:以获取最短回收停顿时间为目标;耗时最长的并发标记和并发清除过程与用户线程一起工作(并发收集、低停顿)
缺:耗CPU资源;无法处理浮动垃圾(无法在当次收集中处理掉);产生大量空间碎片(使用“标记—清除”算法)
步骤:初始标记(GC Roots关联的对象);并发标记(追踪GC Roots);重新标记(修正变动记录);并发清除
停顿阶段:初始标记,重新标记
G1收集器
独立管理整个GC堆 多线程
优:并行与并发(停顿阶段少);分代收集(新生代和老年代不再是物理隔离,都是一部分Region);空间整合(局部上基于“复制”算法,不会产生内存空间碎片);可预测的停顿(建立模型)
缺:?
步骤:初始标记(GC Roots关联的对象);并发标记(对象进行可达性分析);最终标记(修正变动记录);筛选回收(回收价值和成本进行排序)
停顿阶段:初始标记,最终标记(可并行),筛选回收(可并行)
内存分配与回收策略
对象分配,在新生代的eden区,不够发起Minor GC,仍然不够,通过分配担保机制转移到老年代。新对象分配到eden区。
对象大小超过PretenureSizeThreshold,直接分配到老年代。
动态对象年龄判定Survior中相同年龄的对象的大小和大于Survior的一半 及 对象年龄计数器超过MaxTenuringThreshold设置 都在Minor GC转移到老年代。
空间分配担保,Minor GC之前发现空间不够新生代对象总空间,则Minor GC不安全,若HandlePromotionFailture设置允许担保失败,再检查历次晋升对象的平均大小,大于尝试Minor GC;若小于或不允许担保失败就改为一次Full GC
面试题:“你能不能谈谈,java GC是在什么时候,对什么东西,做了什么事情?”