一、运行时内存模型
Java虚拟机的内存空间分为五个部分,分别是:
- 程序计数器 :记录的是当前线程正在执行的那一条字节码指令的地址
- Java虚拟机栈 :描述Java方法运行过程的内存模型,方法执行/返回OR异常,对应一个栈帧入栈出栈
- 本地方法栈 :Native方法执行时的运行模型,实现由具体语言决定
- 堆 :对象都存储区域,GC在这里最频繁
- 方法区/元数据区域(JDK8+):存放类信息、常量、静态变量、即时编译器编译后的代码等
二、内存区域介绍
1.程序计数器
程序计数器是一块较小的内存空间,他的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器为空(undefined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.虚拟机栈
虚拟机栈是一个后入先出的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
3.本地方法栈
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区。如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。
4.堆
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域是用来存放对象实例的,几乎所有对象实例都会在这里分配内存。堆是Java垃圾收集器管理的主要区域(GC堆),垃圾收集器实现了对象的自动销毁。Java堆可以细分为:新生代和老年代;再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。可以通过-Xmx和-Xms控制。
5.方法区
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进人了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
三、垃圾回收
1.对象存活判断
- 引用计数算法
给对象添加一个引用计数器,被引用时计数器值+1,引用失效计数器值-1,当计数器值为0时对象不可能再被使用;
主流Java虚拟机未选用该算法管理内存(未解决对象之间相互循环引用的问题)
- 可达性分析算法
将"GC Roots"对象作为起始节点,向下搜索,搜索走过的路径为引用链;当一个对象到GC Roots没有引用链时,则该对象是不可用的;
可作为"GC Roots"的对象:
【1】方法区中静态属性引用的对象
【2】方法区中常量引用的对象
【3】虚拟机栈引用的对象 (栈帧中本地变量表)
【4】本地方法栈中JNI引用的对象 (Native方法)
2.垃圾收集算法
- 标记-清除算法
定义:先标记要回收的对象,然后统一回收;
适用:存活对象较多的垃圾回收
缺点:
【1】效率低; 标记和清除的过程效率不高;
【2】空间问题; 标记清除后产生大量不连续的内存碎片,给大对象分配内存时没有足够连续的内存空间,导致提前出发垃圾回收动作。
- 复制算法
定义:将可用内存划分成相等大小两块,每次只使用其中一块,当这一块用完后将还存活的对象复制到另一块,
然后将已使用过的内存一次清理。
适用:存活对象较少的垃圾回收
优点:每次对整个半区进行内存回收,不用考虑内存碎片问题,只要移动堆顶指针,按顺序分配内存即可;
实现简单,运行高效
缺点:将内存缩小了一半
其他:
将新生代内存按照8:1:1分为Eden,From Survivor,To Survivor三个空间,每次使用Eden和From Survivor两个空间给对象分配内存,
当内存不足垃圾回收时,将存活对象复制到To Survivor空间,然后清理Eden和From Survivor空间;这样相当于内存指浪费了10%;
如果10%的To Survivor空间不够存放存活对象时需要老年代进行分配担保(将存活对象通过分配担保机制直接进入老年代)
- 标记-整理算法
- 分代收集算法
根据对象存活周期将内存划分为新生代和老年代,然后根据每个年代的特点使用合适的回收算法;
如:新生代存活对象少可以采用复制算法; 老年代存活对象多并且没有分配担保必须使用标记清理或标记整理回收算法
3.垃圾回收器
- Serial收集器
定义:单线程收集器,收集时必须暂停其他所有用户线程,直到收集结束。
适用:新生代
配置:
-XX:PretenureSizeThreshold
-XX:HandlePromotionFailure
其他:
【1】单CPU环境Serial收集器没有现成交互开销,因此单线程的收集效率最高
【2】对于Client模式下的桌面应用,分配给虚拟机的内存不会很大,对于一两百兆的新生代内存回收停顿时间完全控制在一百多毫秒以内,
停顿不频繁发生,Serial收集器是最好的选择;
【3】收集过程会暂停服务(Stop the world)
- ParNew收集器
定义:是Serial收集器的多线程版本
适用:新生代
配置:
-XX:PretenureSizeThreshold
-XX:HandlePromotionFailure
-XX:+UseConcMarkSweepGC (设置默认新生代收集器)
-XX:+UserParNewGC (指定ParNew作为新生代收集器)
-XX:ParallelGCThreads(限制垃圾收集的线程数)
其他:
【1】与Serial收集器的控制参数,收集算法,Stop the world,对象分配规则,回收策略完全一样
【2】是运行Server模式下虚拟机首选新生代收集器(唯一能和CMS收集器配合工作)
CMS是并发收集器,第一次实现让收集线程和用户线程同时工作;
CMS属老年代收集器,无法与Parallel Scavenge配合工作;
CMS关注回收的停顿时间(暂停用户线程时间),停顿时间越短越适合于用户交互的程序,因为有较高的响应速度
【3】单CPU环境没有Serial收集器效率高
【4】并行的多线程收集器
- Parallel Scavenge收集器
定义:和ParNew收集器一样的收集器,区别于主要关注吞吐量的控制和GC自适应调节策略;
注: 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) ; 如果虚拟机总共运行100分钟,收集花费1分钟,则吞吐量为99%
适用:新生代; 较高的吞吐量,导致高效率的CPU利用率,主要适合后台运算
配置:
-XX:MaxGCPauseMillis (控制回收停顿的最大时间 ; 注:时间越小会牺牲吞吐量和新生代空间)
-XX:GCTimeRatio (设置吞吐量 0< X < 100 ; 垃圾回收时间占总时间的比例, 吞吐量的倒数)
如果将GCTimeRatio设置为19,最大GC时间占总时间的5%(即1/(1+19))
该值默认为99,则最大GC时间为1%
-XX:+UseAdaptiveSizePolicy (打开GC自适应调节策略)
GC自适应调节策略:虚拟机根据当前系统性能,自动调节参数已提供最合适的时间和最大吞吐量;
调节的参数包括:①,新生代大小(-Xmn),Eden和Survivor空间比例(-XX:SurvivorRatio)
②,晋升老年代对象的年龄(-XX:PretenureSizeThreshold)
... ...
GC自适应调节策略将内存管理交给虚拟机完成,只需要设置基本内存(-Xmx),停顿时间(MaxGCPauseMillis),吞吐量 (GCTimeRatio)等参数给虚拟机设立优化目标;
其他:
【1】并行:多条垃圾回收线程并行工作,单用户线程处于等待状态
并发:用户线程和回收线程同时进行
【2】并行的多线程收集器
- Serial Old收集器
定义:是Serial收集器的老年代版本的单线程收集器
适用:老年代(标记-整理算法); 主要给Client模式下的虚拟机使用;
其他:
如果在Server模式下主要有两大用途:①在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用
②作为CMS收集器的后备预案(并发收集发生Concurrent Mode Failure时使用)
- Parallel Old收集器
定义:是Parallel Scavenge收集器的老年代版本的多线程收集器
使用:老年代(标记-整理算法)
其他:
【1】JDK1.6开始提供该收集器
- CMS收集器(Concurrent Mark Sweep)
定义:CMS是一款并发收集,低停顿的收集器
关注目标:最短回收停顿时间
适用:互联网站,B/S系统服务端(较快的响应速度,最短的系统停顿时间)较好的用户体验
算法:标记-清除
回收步骤:
【1】初始标记(标记GC Roots能直接关联到的对象)(stop the world)
【2】并发标记(进行GC Roots Tracing的过程)
【3】重新标记(修正并发标记期间因用户程序继续运行而导致标记产生变动的对象的标记记录)(stop the world)
【4】并发清除
执行时间:
T(并发标记,并发清除) > T(重新标记) > T(初始标记)
由于耗时最长的并发标记和并发清除和用户线程一起工作,因此总体上CMS回收过程和用户线程一起并发执行的。
配置:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
缺点:
【1】对CPU资源非常敏感
并发阶段会占用部分线程(CPU资源)导致应用程序变慢,总吞吐量降低。
CMS默认回收线程数 = (CPU数量 + 3)/4; CPU数量↑回收线程↓; CPU数量=4时回收线程占用25%CPU资源;
CPU数量 < 4时,如CPU数量=2,则回收线程会占用一半CPU资源,导致用户程序执行速度直接降低50%,i-CMS收集器可以解决此问题;
增量式并发收集器(i-CMS): 并发标记和并发清理时让GC线程和用户线程交替运行,尽量减少GC线程独占资源的时间
【2】无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而产生Full GC
并发清理时用户线程还在运行而不断产生的垃圾,由于在标记之后,CMS无法当次收集清理,只能等下次GC时清理,这部分垃圾称为“浮动垃圾”;
由于当次无法处理浮动垃圾,这些浮动垃圾又占有一定内存,又考虑要预留给用户线程足够的内存,因此让老年代提供预留空间;
因此CMS默认老年代使用68%时触发一次回收,百分比配置(-XX:CMSInitiatingOccupancyFraction)
如果CMSInitiatingOccupancyFraction参数设置过高,预留空间不足就会出现Concurrent Mode Failure失败,
此时虚拟机启动Serial Old收集器进行老年代垃圾回收停顿时间变长,性能降低。
【3】回收后空间碎片过多
因为CMS采用标记-清除算法因此回收后会产生大量空间碎片,无法给大对象分配连续内存空间而触发Full GC;
+UseCMSCompactAtFullCollection参数,用于在触发Full GC之前开启内存碎片整理过程,整理阶段不能并发,因此停顿时间加长。
-XX:CMSFullGCsBeforeCompaction参数,设置进行多少次不整理的Full GC之后,进行一次带整理的Full GC。
- G1收集器
定义:一款面向服务端应用的垃圾收集器
算法:标记-整理
特点:
【1】并行与并发
充分利用多CPU,多核环境缩短Stop the World停顿时间,使用并发方式回收避免了GC时停顿java线程
【2】分代收集
【3】空间整合(使用了标记-整理回收算法,避免了大量空间内存碎片的产生)
【4】可预测的停顿(G1在追求低停顿同时建立了可预测的停顿时间模型,可以让使用者设置M毫秒内,GC所需要的时间不超过N毫秒)
原理:
1.G1将新生代和老年代分为大小相等的独立区域,进行全区域垃圾回收,新生代和老年代不再是物理隔离,都是部分独立区域的集合;
2.通过计算每个区域垃圾堆积的价值(回收可得到的空间/回收所需要的时间),然后根据价值大小有优先级地进行垃圾回收,保证了回收的效率;
3.虚拟机发现独立区域中的Reference类型数据进行写操作时,判断其他独立区域是否有Reference数据引用的对象(即老年代对象是否引用了新生代对象),
如果是就将引用信息通过CardTable记录到独立区域的Remembered Set中,在垃圾回收的时候将Remembered Set加入到GC根节点的枚举范围,可避免使用
可达性算法判断对象存活而进行的全堆扫描,也避免有存活对象的遗漏。
回收步骤:
【1】初始标记(标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的Region中创建对象)(Stop the World)
【2】并发标记(从GC Roots开始对堆中对象可达性分析,找出存活对象)(和用户线程并发进行)
【3】最终标记(修正并发标记期间因用户线程运行的而产生变动对象的标记记录,虚拟机将这些对象记录到Remembered Set Logs中,然后合并到Rembered Set中)(Stop the World)
【4】筛选回收(对每个独立区域进行价值排序,根据用户期望的GC停顿时间制定回收计划)