Java技术体系的自动内存管理最根本目标是自动化地给对象分配内存以及自动回收分配给对象的内存。
《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪种垃圾收集器,以及虚拟机中与内存相关的参数设定。
HotSpot虚拟机中对象的分配原则
对象优先在Eden分配
在大多数情况下,对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时,虚拟机将发起一次minorGC。
当survivor区的空间无法存放Eden区的对象时,就会通过担保机制提前转移到老年代去。
-Xms:20M 堆内存初始值20M
-Xmx:20M 堆内存最大20M
-Xmn10M 新生代占用10M(剩下的10M留给老年代)
-XX:SurvivorRatio=8 决定了新生代Eden区与一个Survivor区的空间比例是8:1:1
大对象直接进入老年代
大对象是需要大量连续内存空间的Java对象,如很长的字符串或者元素数量很多的数组。
在分配空间时,如果对象比较大,就会提前触发垃圾收集,以获取足够的连续空间才能存放该对象, 当复制对象时大对象意味着高额的内存复制开销。
-XX:PretenureSizeThreshold=3145728 (大于3M的对象会直接分配到老年代中)
该参数只对Serial和ParNew两款收集器有效。
长期存活的对象将进入老年代
内存回收时必须决策那些存活对象应该存放在新生代,哪些存活对象存放在老年代。
虚拟机为每个对象定义了一个对象年龄(Age)存储在对象头重。
对象通常在Eden区诞生,如果经过一次MinorGC后仍然存活,并且能被Survivor容纳的话,会被移动到Survivor空间,并且将对象的年龄设置为1岁。对象在Survivor中每经过一次MinorGC,Age增加1, 当年龄增加到一定程度的时候,该对象会被移动到老年代中。
可以通过-XX:MaxTenuringThreshold设置(默认为15次)。
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。
也就是当前Survivor空间大小为1M,有两个Age=1且大小为256k的对象时,这两个对象就会直接进入老年代。
空间分配担保
在发生MinorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
如果条件成立,那么这次MinorGC时安全的;;
如果不成立,虚拟机会先查看-XX:HandlerPromotionFailure参数的设置值是否允许担保失败
如果允许担保失败,继续检查老年代最大可用的连续空间是否大于历次今生到老年代对象的平均大小
大于,尝试进行一次MinorGC;
小于,或者-XX:HandlePromotionFailure不允许冒险,则变更为FullGC。
在JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代总大小或者历次晋升的平均大小,就会进行MinorGC,否则就进行FullGc。-XX:HandlePromotionFailure参数不在生效。
垃圾的回收
- 哪些内存需要回收
- 什么时候回收
- 如何回收
如何判断对象已死
- 引用计数器
- 可达性分析算法(固定可作为GCRoot的对象有)
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如每个线程背调用的方法堆栈中使用的参数、局部变量、临时变量
- 在方法区中类静态属性引用的对象,如Java类的引用类型静态变量
- 在方法区中常量引用的对象,如字符串常量池中的引用
- 在本地方法栈中JNI引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器
- 所有背同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
对象的引用
- 强引用,如Object obj = new Object(); 无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉背引用的对象。
- 软引用,描述有用但非必须的对象,被软引用关联的对象在系统将要发生内存溢出前会把这些对象例如回收范围内进行二次回收。(SoftReference)
- 弱引用,描述非必须对象,被弱引用的对象只能生存到下一次垃圾收集发生位置。无论内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用,一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的时为了能在这个对象被收集器回收时受到一个系统通知(NIO用的RdirectMemory)
垃圾收集器算法
分代收集理论
建立在两个分代假说上:
1)弱分代假说,绝大数对象都是朝生夕灭的。
2)强分代假说,经过越多次收集过程的对象月难消亡。
3)(推理假说)跨代引用假说,跨代引用相对于同代来说仅占极少数。
分代收集区域
- 部分收集(Partial GC)
- 新生代收集(Minor GC/ Yong GC)
- 老年代收集(Major GC / Old GC)(只有CMS会单独回收老年代)
- 混合收集(Mixed GC),目前只有GC存在混合回收行为。
- 整堆收集(Full GC)
标记清除算法
标记需要被回收的对象后,回收掉所有被标记的对象;
或者标记不需要被回收的对象,同一回收未被标记的对象。
缺点:
不稳定,标记清除的性能随着对象的增多而降低;
空间碎片化问题,肯能会导致在分配较大对象时无法找到足够的连续空间内存不得不提前触发另一次垃圾收集动作。
标记复制算法
为了解决标记清除算法面对大量可回收对象时执行效率低的问题。
优点:只需要复制少数存活对象,简单高效
缺点:浪费内存空间。
根据弱分代假说,以及实际情况,大部分的对象在第一次垃圾回收时就会被回收,因此并不需要按照1:1的比例来划分新生代的内存空间。
内存被分为较大的Eden区和较小的两个Survivor区域,HotSpot默认比例是8:1:1。
打给你Survivor空间不足容纳一次MinorGC后存活的对戏那个时,就需要依赖其他内存区域进行分配担保。
标记整理算法
根据老年代对象的存亡特征以及强分代假说提出的算法。
将所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。
吞吐量:赋值器和收集器的效率总和。
如果在清理时移动,必然会影响到对象回收的效率。
如果清理时不移动,必然会影响到对象分配的效率。
常见的垃圾收集器
Serial / SerialOld
Serial收集器是最古老的收集器,使用标记拷贝算法,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:在内存较小的桌面应用或者Client模式下,额外内存消耗最小,简单高效。
SerialOld同样是单线程收集起,使用标记整理算法。当CMS收集起发生失败时会转为SerialOld工作。
PS / PO
Paralle Scavenge/ Paralle Old 的目标是达到一个可控的吞吐量。
ParalleScavenge 同样是基于标记-复制算法,支持多线程并发收集,
吞吐量 = (运行用户代码时间)/(运行用户代码时间+运行垃圾收集时间)
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
-XX:GCTimeRatio 控制垃圾收集时间占总时间的比率
-XX:+UseAdaptiveSizePolicy 就不需要人工指定新生代的大小,Eden于Survivor的比例,以及晋升老年代的大小,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
ParalleOld 收集老年代,基于标记整理算法。
ParNew / CMS
ParNew是Serial收集器的多线程并行版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集起完全一致。
CMD是一种以获取最短回收停顿时间为目的的收集器。符合希望系统停顿时间尽可能短,以给用户带来良好交互体验的需求。他是一种标记-清除算法实现的。
包含四个步骤:
1)初始标记
2)并发标记
3)重新标记
4)并发清除
其中初始标记和重新标记这两个步骤仍需要STW。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象、速度很快;
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长,但不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
重新标记为了修正并发标记期间因为用户程序继续运行导致标记产生变动的部分,这个阶段比初始标记耗时稍长;
并发清除,清理删除掉标记阶段判断已经死亡的对象,不需要移动哦存活对象,因此这个阶段也是与用户线程同时并发。
CMS缺点:
1) 耗CPU资源
2) 无法处理浮动垃圾,由于预留的内存无法分配新对象时,有可能出现回收失败而导致退化到Serial Old的模式收集。
3)分配占用大空间的对象时,会出现老年代还有很多剩余空间,但是无法找到足够大的连续空间分配对象,不得不提前触发FullGC。
提供了两个参数优化,但是JDK1.9之后已经废弃:
-XX:UseCMS-CompactAtFullCollection 不得不进行FullGC时开启内存碎片的合并整理。
-XX:CMSFullGCsBefore-Compaction,要求CMS收集器在执行过若干次不整理空间的FullGC之后,下一次进入FullGC前进行碎片整理。
Garbage First
G1时一款主要面向服务端应用的垃圾收集器。
在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
G1面向堆内存任何部分来组成回收集(CollectionSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的MixedGC模式。
G1把连续的Java堆内存划分为多个大小相等的独立区域(Region),每个Region都可以根据需要扮演 新生代的Eden空间Survivor空间,或者老年代空间。收集器能够对不同角色的Region采用不同的=策略取处理,这样无论是新创建的对象还是存活了一段时间、经过多次收集的旧对象都能获得很好的收集再熬过。
Region中有一类Humongous区域专门存放大对象。 G1认为只要大小超过了一个Region容量的一般的对象就可以判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围1MB~32MB,且应为2的N次幂。那些超过了整个Region容量的超级大对象,会被存放在N个连续的HumongousRegion之中,G1的大多数行为都把HumongousRegion作为老年代的一部分进行看待。
G因为将Region作为单词回收的最小单元,所以可以建立可预测的停顿时间模型。
每次根据用户设置的停顿时间-XX:MaxGCPauseMillis, 优先处理回收价值收益最大的那些Region。
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在悠闲的时间内获取尽可能高的收集效率。
需要解决的问题
1)跨Region引用的对象: 每个Region都维护自己的记忆集避免全堆作为GCRoots扫描, 这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
G1的记忆集是一种Hash表,Key时别的Region的起始地址,Value时一个集合,里面存储的元素时卡表的索引号。这种双向的卡表结构记录了我指向谁和谁指向了我,因此G1只少需要耗费大约10%~20%的额外内存来维持收集工作。
2)在并发阶段如何保证收集线程与用户线程互不干扰地运行?
用户线程在改变对象引用关系的时候,必须保证不能打破原本的对象图结构,导致比阿吉结果出现错误,
CMS使用增量更新算法,G1使用原始快照算法来实现。
垃圾收集对用户线程的影响还体现在回收过程中心窗卷对象的内存分配上,程序要继续运行就肯定会持续有新对象呗创建,G1位每个Region设计了两个名为TAMS(TOp at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时心分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象时被隐式标记过的,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致FullGC而产生长时间的STW。
3)如何建立可靠的停顿模型
G1的停顿预测模型时以衰减均值为理论基础实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可以测量的步骤话费的成本,并分析得出平均值,标准偏差、可行度等统计信息,衰减平均值时比普通的平均值更容易受到新数据的影响,平均值更容易受到新数据的影响,平均值代表整体平均状态,衰减均值代表最近的平均状态。 Region的统计状态越新越能决定器回收的价值。通过这些信息预测现在开始回收的话,哪些region组成回收集采可以在不超过期望停顿时间的约束下获得最高的收益。
G1的回收步骤
初始标记
只标记一下GCRoots能直接关联到的对象,并且修改TAMS指针的值,让下一个阶段的用户线程并发运行时,能正确滴在可用的Region中分配新对象。这个阶段需要停顿线程,但是耗时短,而且是借用进行MinorGC的时候同步完成,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记
从GCRoots开始对对子红对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时较长,但是可以和用户程序并发执行,当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记
对用户线程做另外一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
筛选回收
负责更新Region的统计数据,对哥哥Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间,对存活对象移动时必须暂停用户线程,但是由多条线程完成的。
G1从整体上来看是基于标记整理算法,但从局部来看时基于标记复制算法。无论如何,这两种算法意味着G1再与性期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发限一次收集。
G1位了垃圾收集产生的内存占用以及额外的执行副在都比CMS搞。
内存占用是由于双向卡表占用空间大。CMs只记录了老年代到新生代的卡表记录。
负载:CMS用写后屏障维护更新卡表;
G1除了使用写后屏障维护卡表外,位了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发前的指针变化。 原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点。
CMS的写屏障是同步操作,G1使用的是消息队列,把写前屏障和写后屏障中要做的事情都放在队列里,再异步处理。