1. 内存分区
1.1 程序计数器
线程私有,可以看作当前线程所执行的字节码的行号指示器。如果执行native方法,程序计数器值为undefined。此区域不会发生OutOfMemoryError。
1.2 Java虚拟机栈
- 线程私有,生命周期与线程相同。描述了Java方法执行的内存模型。每个方法执行时都会创建一个栈帧,存放局部变量表,操作数栈,动态链接和方法出口等信息。每个方法从调用到执行完成,对应一个栈帧入栈出栈。
- 通常意义上的Java堆内存和“栈内存”,这里的“栈内存”可以理解为虚拟机栈中的局部变量表部分。
- 局部变量表中存放了编译器可知的各种基本数据类型和对象引用,64位的需要两个slot空间,这些内存空间在编译器就完全确定了,方法运行期间不会发生变化。
- 虚拟机规范定义了两种异常:
1、StackOverflowError:线程请求的栈深度大于虚拟机允许的栈深度。
2、OutOfMemoryError:栈可以扩展,当扩展到无法申请足够的内存时。
1.3 本地方法栈
- Java虚拟机栈为Java方法服务。
- 本地方法栈为native方法服务。
1.4 Java堆
- 虚拟机管理的最大的一块内存。所有线程共享。虚拟机启动时创建,用于存放对象实例和数组。但是随着JIT,并不绝对。
- 堆是GC的主要区域。采用分代收集时分为新生代(Eden、From Survivor和To Survivor)和老年代。
- OutOfMemoryError:堆中没有内存可以分配实例且堆无法再扩展。
1.5 方法区
- 所有线程共享,存储已被虚拟机加载的类信息(包括类的名称、方法信息、字段信息),常量,静态变量,JIT编译后的代码数据。
- 永久代?HotSpot将GC分代收集扩展至方法区,或者说用永久代试下了方法区而已。虚拟机规范中并未强制要实现方法区GC。
- 这一部分的GC主要针对常量池的回收和类型的卸载。
- OutOfMemoryError:方法区无法满足内存分配需求。
- 运行时常量池在方法区(JDK1.6之前)中:包含class文件中的常量池和运行期新增的常量,如String.intern()
2. 垃圾收集
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
- 程序计数器、Java虚拟机栈和本地方法栈随线程而生,随线程而灭。栈帧分配多少内存都是编译期可知的(JIT不谈)。不需要考虑回收的问题。
- 堆和方法区的分配都是动态的,回收也比较复杂。
2.1 哪些内存需要回收?(对象还存活吗?)
引用计数法
- 给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;引用失效时,计数器减1。计数器为0时,代表对象不可用了。此法不能解决互相循环引用的问题。
可达性分析
- 通过一系列称为“GC-Roots”的对象作为起始点,从这些起始点向下搜索,走过的路径称为引用链,当一个对象到GC-Roots没有任何引用链相连接时,证明此对象不可用了。
【可作为GC-Roots的对象】:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
【四种引用】
- 强引用:代码中普遍存在的,只要强引用存在,就不会GC引用的对象。
- 软引用:在系统将要发生内存溢出之前,将会把这些对象列为回收范围进行第二次回收。JDK中有SoftReference类。
- 弱引用:被弱引用关联的对象只能生存到下一次GC之前,无论当前内存是否足够,这些对象都会被回收。JDK中有WeakReference类。
- 虚引用:一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能够在对象被GC时收到一个系统通知。JDK中有PhantomReference类。
【真正判定对象死亡】
- 对象进行可达性分析后发现没有与GC-Roots相关联,那么就进行第一次标记并且进行一次筛选。
- 筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize方法或者finalize已经被虚拟机执行过了,那就GG了。
- 如果需要执行finalize方法,那么对象进入F-Queue队列中,稍后由虚拟机自动建立的低优先级线程Finalizer去执行。虚拟机并不保证能够执行结束。
- 稍后GC将对F-Queue中的对象进行第二次小规模标记,如果finalize方法中成功将自己与引用链相连,那就复活了。
- finalize方法只能执行一次,下次就不能复活了。
【方法区回收】
- 方法区回收并不划算,新生代回收可以获得大量空间,方法区效率很低。
- 方法区主要回收废弃常量和无用的类。
- 常量:判定很简单,和堆相似。例如“ABC”,如果当前系统没有一个String对象引用常量池的“ABC”,那么有必要的话,将回收这个常量。
- 类:需要同时满足三个条件。代表可以回收,并不一定。
第一、该类的所有实例已经被回收,就是说Java堆中没有该类的实例了。
第二、加载该类的ClassLoader已经被回收。
第三、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2.2 如何回收?(GC算法)
标记清除
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 效率问题:标记和清除两个过程效率都不高。
-
空间问题:产生大量内存碎片,导致下次分配大对象时空间不够,提前GC。
复制
将可用内存按容量分为相等的两块,每次只使用其中一块,当这一块用完了,就将还存活的对象复制到另外一块上,然后把已经使用的内存空间一次清理掉。
- 优点:没有内存碎片,实现简单。
-
缺点:空间利用率低。
【新生代回收】
- 由于新生代的对象大多数“朝生夕死”,所以不需要按照1:1的比例分配空间。
- 新生代采用8:1:1的比例分为eden区和两个survivor区。每次只使用eden区和其中一块survivor区,GC时将存活的对象复制进入另外一个survivor区,同时清理掉原来的eden和survivor。接着继续在eden区分配对象。
- 大对象和老年对象会进入老年区,survivor空间不够时,也会进入老年区。
- 这种方法保证了连续的空间,同时避免了空间的浪费。
标记-整理算法
- 复制算法在对象存活率较高时需要较多的复制操作,效率将会很低。所以老年代一般不用复制算法。
- 和标记清除一样,先标记,但是后续步骤不是直接清除,而是将存活对象都向一端移动,然后直接清理掉端边界外的内存。
2.3 垃圾收集器
Serial
- HotSpot在Client模式下默认的新生代收集器,只用一个CPU或者一条线程去完成垃圾收集工作,并且收集时需要STP。
-
特点:简单高效,没有线程交互的开销。
ParNew
- Serial的多线程版本,Server模式下新生代认。
- 除了Serial ,只有ParNew可以和CMS配合。
-
由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).
Parallel Scavenge
- 与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器。但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量。
-
系统吞吐量=运行用户代码时间(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务.
Serial Old
- Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法。主要在Client模式下。
- 如果在Server模式下,Serial Old应用场景如下:
- JDK 1.5之前与Parallel Scavenge收集器搭配使用;
-
作为CMS收集器的后备预案, 在并发收集发生Concurrent Mode Failure时启用。
Parallel Old
- Parallel Old是Parallel Scavenge收老年代版本, 使用多线程和“标记-整理”算法, 吞吐量优先, 主要与Parallel Scavenge配合在 注重吞吐量 及 CPU资源敏感 系统内使用。
CMS
- CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现。
【四个步骤】:
- 初始标记:仅仅标记GC-Roots直接关联到的对象,需要STW。
- 并发标记:进行GC-Roots Tracing。
- 重新标记:修正并发标记期间因用户程序继续运作导致的变动。需要STW。
- 并发清理:并发清除。
需要STW的两个步骤耗时很短,其它步骤都是和用户线程并发工作的,所以停顿时间很短。
【缺点】:
- 对CPU资源非常敏感。CMS默认回收线程数:(CPU数量+3)/ 4。当CPU数量大于4时,GC线程最多不超过25%资源;小于4时,GC会占用较多CPU资源。
- 无法清除浮动垃圾。并发清理阶段,用户线程还在运行,会有新的垃圾出现,这一次GC无法处理它们,只能留在下次GC。因此CMS需要流出一定内存给用户线程使用,如果内存不够用户线程使用,会出现Concurrent Mode Failure导致另一次Full GC。
- CMS基于标记清除,容易产生内存碎片。因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).
G1
-
G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合。
- 并行与并发:G1可以充分利用多CPU优势来缩短STW时间。
- 分代收集。
- 空间整合:G1整体上基于“标记-整理”,局部上基于复制算法。因此不会产生碎片。
- 可预测停顿:有计划的避免在整个堆中进行全区域GC。G1跟踪各个Region里面垃圾的价值大小和回收成本,后台维护一个优先列表,优先回收价值最大的Region。
- Remembered Set:用于避免扫描全堆。每个region都有一个与之对应的Remembered Set,虚拟机发现程序在对reference类型的数据进行写操作时,会产生一个write barrier 暂时中断写操作,检查reference引用的对象是否处于不同的region中,如果是,通过cardtable把引用信息记录到被引用对象所属的region中的Remembered Set中,进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描。
【四个步骤】
- 初始标记:扫描GC-Roots直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,需要STW。
- 并发标记:进行可达性分析。
- 最终标记:修正在并发标记期间因用户线程继续运作导致标记产生的变动部分,虚拟机将这段时间内的变化记录在Remembered Set Logs里,最终标记阶段需要把Remembered Set Logs合并到Remembered Set中。需要STW,也可以并发。
- 筛选回收:根据回收价值和回收成本进行GC。可以并发,但是由于只回收一部分Region,时间是可控的,停顿线程可以提高效率。
3. 内存分配策略
对象优先在eden区分配
- 大多数情况下,对象优先在新生代eden区分配,当eden没有足够空间时,虚拟机进行一次Minor GC。
大对象直接进入老年代
- 需要大量连续内存空间的Java对象,如长字符串和数组。最可怕的不是大对象,而是朝生夕死的大对象。虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个值的对象直接进入老年代。可以避免新生代大量的内存复制。此参数只对serial和parNew有效。
长期存活的对象进入老年代
- 对象在survivor中熬过一次Minor GC ,年龄加1。达到一定年龄,进入老年代。-XX:MaxTenuringThreshold可以设置,默认15。
动态对象年龄判断
-如果survivor中相同年龄所有对象大小总和大于survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年代。不用等待XX:MaxTenuringThreshold。
空间分配担保
- 在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
- JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。