那些内存需要回收?
什么时候回收?
怎么回收?
程序计数器、虚拟机栈、本地方法栈这三个区域随线程的生命周期,这些区域产生的内存再线程结束后,内存自然的跟着回收了,但是java堆和方法区是不确定的,只有再运行时才知道会创建那些对象,多少对象,这部分的内存和回收都是动态的,也是GC最关注的一块。
确定那些对象是否已“死“,判断的方法:
- 引用计数器---对象中添加一个引用计数器,每当一个地方引用它时,计数值加1,引用失效,计数减1,当任何时刻都为0时,则不可能被使用可以被回收,引用计数算法需要占用一些额外的内存来进行计数。但是当两个对象的循环引用时,这两个对象不被使用了,但是两者之间存在引用,造成无法回收。 -XX:+PrintGCDetails (打印GC日志)
- 可达性分析算法---通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始向下搜索,搜索的过程形成的路径就是引用链,如果这个对象到GC Root之间没有任何引用链相连,也就是对象不可达时,则证明此对象是不可能再被使用的。可固定为GC Roots的对象包括:
- 在虚拟机栈中(帧栈的本地变量表)中引用的对象,线程调用方法堆中使用到的参数、局部变量、临时变量。
- 在方法区中类静态属性引用的对象,引用类型的静态变量。
- 在方法区中常量引用的引用对象,字符串常量池里的引用。
- 在本地方法栈中JNI(native方法)引用的对象。
- jvm内部的引用,基本数据类型对应的class对象,常驻的异常对象(NullPointExcepiton、OutOfMemoryError)等,系统类加载器。
- 所有被锁同步持有的对象。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
引用
- 强引用---程序间普遍存在的引用赋值,只要强引用关系还在,就不会回收。
- 软引用---描述一些还有用但非必须的对象,只要被关联着,在系统将要发生内存溢出异常前,会把这些对象划到回收范围内第二次回收,如果回收后内存还不够,才会抛出内存溢出异常。
- 弱引用---非必须的对象,被引用关联的对像只能存活到下一次垃圾收集发生为止。无论内存够不够用都会被回收。
- 虚引用---一个对象是否有虚引用的存在,完全不会对其生存构成任何影响,也无法通过虚引用来获取一个对象实例。唯一目的在这个对象被收集器回收时,会收到一个系统通知。
对象死亡的阶段:
通过可达性分析算法判定为不可达的对象,不是非死不可,这时候它们暂时还处于缓刑阶段,要宣布一个对象真正死亡,至少需要两次标记过程:如果对象进行可达性分析后发现没有与GC Roots相连的引用链,会进行第一次标记,之后再筛选一次,筛选条件是此对象是否有必要执行finalize()(系统只会执行一次)方法,假如对象没有覆盖finalize()方法,或者finalize已经被jvm调用过了,则视为没有必要执行。如果判定为有必要执行finalize方法,那么该对象将会放置在一个F-Queue的队列之中,并在稍后由一条jvm自行建立的、低调度优先级的Finalizer线程去触发他们的finalize()方法,不会等待返回结果(如果finalize执行缓慢或发生死循环,将会阻塞这个F-Queue队列的其他对象,导致整个内存回收子系统崩溃)。收集器会对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize中被引用(自我拯救)了,则第二次标记它将被移出即将回收的集合,否则将真正的回收。
方法区的回收
方法区回收主要回收两部分内容:废弃的常量和不再使用的类型。常量池中常量、类、方法、字段的符号引用发生内存回收时,垃圾收集器会判断是否其他有地方还在引用,如果没有,则被系统清理掉。
一个类型是否属于”不在被使用的类“必须满足三个条件(也不一定会被真的回收):
- 该类的所有实例都已经被回收,也就是java堆中不存在该类以及任何派生子类的实例。
- 加载该类的类加载器已经被回收,(除非经过设计的可替换类加载器的场景,入OSGI等,否则很难达成)
- 该类对应的class对象没有在任何地方被引用也无法通过反射访问该类的方法。
在使用大量反射、动态代理、CGLIB等字节码框架,动态生成的JSP以及OSGI这类频繁自定类加载器的场景中,都需要jvm具有类型卸载的能力,保证方法区不会再有过大的内存压力。
垃圾收集算法
收集算法可分为引用计数式垃圾收集和追踪式垃圾收集。
分代思想:
- 弱分代:绝大多数对象都是朝生夕死。
- 强分代:熬过越多次垃圾回收的对象越难以消亡
- 跨代引用:存在相互引用的两个对象,是倾向于同时生存和消亡的,如果新生代对象存在跨代引用,由于老年代对象难以消亡,使得新生代对象在收集时得以存活,进而随着年龄的增长晋升到老年代中,这个跨代引用得以消除。(避免遍历老年代中所有的对象来确保可达性分析的正确性,在新生代建立一个全局的数据结构,这个结构把老年代划分为若干,标记出老年代的那一块内存会存在跨代引用里,发生Minor GC时,只有包含跨代引用的小块内存中的对象加入到GC Roots进行扫描。)
垃圾收集器的一致设计原则:收集器应该将java堆划分出不同的区域,再根据回收对象依据其年龄(年龄就是熬过垃圾收集过程的次数)分配到不同的区域之中存储。朝生夕灭的对象放在一块区域,每次回收时只关注如何保留少量存活而不是去标记那些大量将被回收的对象,以较低代价回收大量空间;难以消亡的对象放在一块区域,已低频次去回收。
java堆划分不同的区域后,垃圾收集器每次只回收其中一个或某些部分的区域-------因此有了 Minor(次要) GC、Major(重大) GC、Full(完全的) GC回收类型的划分。
Java堆一般划分为新生代(Young Generation)、老年代(Old Generation),新生代中,每次垃圾收集时都会发现大批对象死去,而每次回收后存活的少量对象,将会逐步晋升为老年代中存放。
- 部分收集(Partial GC):指目标不是完整收集整个java堆的垃圾收集,又分为新手代和老年代收集。
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集。又分为一个Eden区和两个等大的Survivor区。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集,目前只有CMS收集器会单独收集老年代的行为。
- 混合收集(Mixed GC):指目标时收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
收集的算法
- 标记-清除:首先标记出所有需要回收的对象,标记完后,再统一回收掉所有标记的对象,存在的问题:1.执行效率不稳定,java堆中存在大量对象,而且其中大部分对象是需要回收的,这时需要进行大量标记和清除的动作,导致标记和清除两个过程的执行效率随对象的增加而降低。2.内存碎片的问题,标记、清除会产生大量不连续的内存碎片,空间碎片太多可能会导致之后分配大对象时没有足够的内存块而提前触发垃圾收集动作。
- 标记-复制:将内存按容量划分为大小相等的两块,每次使用其中的一块,当其中一块的内存用完了,将存活的对象复制到另一块,再一次性清除空间。问题:1.如果存在大量存活的对象,则会产生大量的内存间的复制开销。对于大多数可回收的对象,这个算法只需要复制少量的存活对象,每次回收都是针对整个内存块,不用考虑空间碎片的问题。2.可用内存直接缩小一半。
- 标记-整理:标记回收的对象,再将存活的对象都向内存空间一端移动,然后直接清除掉边界以外的内存。问题:移动活对象并更新所有引用这些对象的地方都需要重新定位,而这个操作时需要全程暂停用户应用程序才能进行。
是否移动对象---移动则回收时复制,不移动则分配时复杂,从时间停顿来说,不移动停顿时间短,
根节点枚举:固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),通过OopMap的数据结构来快速查找执行上下文和全局的引用位置。所有收集器再根节点枚举这一步骤都必须暂停所有用户线程。
安全点:由于OopMap的协助下,可快速准确地完成GC Roots,同时可能导致引用关系的变化。再特定的位置通过OopMap记录这些引用的信息,这些位置就是安全点。安全点的选定以”是否具有让程序长时间执行的特征“--方法调用、循环跳转等,这些地方才会产生安全点。如何让所有线程都跑到最近的安全点,然后停顿下来,又分为两种方式:抢先式中断和主动式中断,
- 抢先式:线程的执行代码主动去配合,在垃圾收集时,系统首相中断所有的用户线程,如果发现现有用户线程中断地方不在安全点,就恢复它,然他执行到安全点后再中断。没有jvm使用这种方式。
- 主动式:垃圾收集时需要中断线程,不直接对线程操作,仅仅设置一个标志位,每个线程执行过程中不断的去主动询问这个标志,一旦发现中断标志位为真时,就在最近的安全点挂起。轮询标志和安全点重合。
垃圾收集器:
Serial收集器:单线程工作的收集器,只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停(JVM后台自动发起和完成的)所有工作线程,直到收集结束(jvm运行在客户端模式下的默认新生代收集器)。
ParNew收集器:Serial的多线程版本,同时使用多条线程进行垃圾收集,可用参数、收集算法,只有这个收集器能和CMS配合。激活CMS(-XX:+UseConcMarkSweepGC)后的默认的新生代收集器(-XX:+/-UseParNewGC 指定或禁用)。默认开启的线程数和处理器核心数相同(-XX:ParallelGCThreads参数来限制垃圾收集的线程数)。
Parallel Scavenge收集器:基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间,而paralled Scavenge收集器的目标则是达到一个可控的吞吐量---处理器运行用户程序的时间占处理器总消耗时间的比值。响应速度块,高吞吐量可以高效地利用处理器资源,尽快完成程序的运算任务。通过参数-XX:MaxGCPauseMillis控制时间,-XX:GCTimeRatio直接控制吞吐量大小。
- -XX:MaxGCPauseMillis,停顿时间短是通过牺牲吞吐量和新生代空间换取的。
- -XX:GCTimeRatio,大于0小于100的整数,垃圾收集器占总时间的比率,吞吐量的倒数,默认为99,即允许1%(1/(1+99))的垃圾收集时间。
还有就是-XX:+UseAdaptiveSizePolicy 自适应调节策略,开启后就不需要指定新生代大小(-Xmn)、Eden和Survivor的比值(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,根据虚拟机运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。
Serial Old收集器
老年代收集器,采用标记-整理算法的单线程收集器。主要意义也是客户端下使用。在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent ModeFailure时使用
Parallel Old收集器
基于标记-整理的多线程收集器,是Parallel Scavenge收集器的老年代版本。
CMS收集器
Concurrent Mark Sweep 以获取最短回收停顿时间为目标的收集器---关注服务的响应速度。基于标记-清除算法实现。运行过程:
- 初始标记(需要stop th world):标记一下GC Roots能够直接关到的对象,速度很快。
- 并发标记:GC Roots 的直接关联对象开始遍历整个对象图的过程,比较耗时,但不停顿用户线程,与垃圾收集线程并行。
- 重新标记(需要stop th world):修正并发标记期间,因用户线程继续运行而导致标记产生变动的对象的标记记录。
- 并发清除:清理删除标记阶段已经判读死亡的对象,不需要移动对象,可与用户线程并行运行。
优点:并发收集、低停顿。
缺点:1.虽然部导致用户线程停顿,但会占用一部分线程而导致应用程序变慢,降低吞吐量,CMS默认启动的回收线程数量是(处理器核数+3)/4。占用不超过25%的处理器运算资源,并随核数的增加而下降。2.无法处理浮动垃圾,有可能出现”Con-current Mode Failure“失败而导致另一次完全"Stop the World "的Full GC。在并发标记和并发处理过程中,由于用户线程还在执行,会不断的产生新对象,这一部分是无法在这次收集过程中处理的,只有等到下一次,而这一部分就是浮动垃圾。还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代满后在进行收集,必须预留一部分空间给并发收集时程序运作使用。通过参数-XX:CMSInitingOccupancyFraction 设定CMS触发的百分比,降低内存回收频率。3.由于是标记-清除,所以会产生大量内存空间碎片,过多会导致大对象无法分配到足够的内存而导致Full GC。通过 -XX:UseCMSCompactAtFullCollection(JDK9放弃了),用于在不得不进行Full GC时开启内存碎片的合并整理过程,而这个过程是需要移动对象的,所以无法是并发的。通过参数 -XX:CMSFullGCsBeforeCompaction(jdk9f废弃),在执行若干次不整理空间的Full GC之后,下一次Full GC 前会先进行碎片整理(默认是0,每次full gc都整理)
G1收集器
面向局部收集,面向堆内存任何部分来组成回收集进行回收,衡量标准不再是属于那个分代,而是那块内存的垃圾数量最多,回收效益最大,也就是G1收集器的 Mixed GC模式。
G1不在坚持固定大小以及固定数量的分代区域划分,而是把连续的堆划分为大小相等的独立区域,每一个Region都可以根据需要,扮演新生代的Eden、Survivor、或老年代空间,根据不同的角色采用不同的策略去处理。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象(看作老年代),通过参数进行设置Region的大小,-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
收集步骤:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
衡量垃圾收集器的三项重要指标:内存占用、吞吐量、延迟。
GC日志:
+PrintGC 查看GC基本信息。
+PrintGCDetails 查看GC消息信息
+PrintHeadAtGC 查看GC前后的堆、方法区可用容量的变化。
+PrintGCApplicaitonConcurrentTime -XX:+PrintGCApplicationStoppedTime 查看GC过程中用户线程并发时间和停顿时间
+PrintTenuringDisitribution 查看剩余对象年龄的分布信息
大多数情况下,对象在新生代的Eden区分配,当Eden区空间不足时,进行一次Minor GC。可通过 -XX:SurvivorRatio=8决定新生代中Eden区和一个Survivor区的空间比列为8:1。
大对象直接进入老年代,需要大量连续内存空间的java对象。在分配空间时,容易导致内存足够但就提前触发了GC,以获取足够连续的空间来安置大对象,而当复制时,也需要高额的内存复制开销。通过参数 -XX:PretrentureSizeThreshold 指定超过设定值的对象直接进入老年代,避免在Eden和两个Suivivor之间进行复制。
年龄的判断----使用分代收集管理堆内存,对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
空间分配担保------在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle PromotionFailure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次FullGC。新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
Eden 、from Survivor、to Survivor
GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
有关年轻代的JVM参数
1)-XX:NewSize和-XX:MaxNewSize -Xmn:年轻代大小
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
2)-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
3)-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1
5)-XX:NewRatio:指定老年代/新生代的堆内存比例。在hotspot虚拟机中,堆内存 = 新生代 + 老年代。如果-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆内存的1/5。在设置了-XX:MaxNewSize的情况下,-XX:NewRatio的值会被忽略