垃圾收集器与内存分配策略

垃圾收集器与内存分配策略

线程私有的程序计数器、虚拟机栈和本地方法栈的内存大小在编译期间可知,生命周期是随着线程或者方法的开始而开始,结束而结束。
而线程共享的堆和方法区不一样,只有在程序执行期间才知道需要创建多少个对象以及一个方法不同分支的内存分配。
  • 问题:
    • 哪些内存需要回收
    • 什么时候回收
    • 怎么回收
  1. java引用:JDK1.2之后,java对引用的概念进行了扩充,分为强引用、软引用、弱引用和虚引用四种,引用强度逐渐减弱。
    • 强引用:程序代码中普遍存在的,比如Object a = new Object()。当强引用还存在时,垃圾回收器永远不会回收被引用的对象。
    • 软引用:用来描述一些有用但非必须的对象。在系统将要发生内存溢出异常时,将这些对象列入回收范围进行第二次回收,如果本次回收还是没有足够的内存时,才会抛出内存溢出异常。java.lang.ref.SoftReference,适合用来实现缓存:比如网页缓存、图片缓存等。
    • 弱引用:描述非必需对象,被弱引用的对象只能生存到下一次垃圾收集发生之前。WeakReference
    • 虚引用:最弱的一种引用关系,又称幽灵引用或者幻影引用。一个对象是否存在虚引用,完全不会对其生存时间造成影响,也无法通过一个虚引用来获得一个对象实例。该引用存在的唯一目的是在这个对象被垃圾收集器回收时收到一个系统通知。PhantomReference
      注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

对象是否存活的判断:

  1. 引用计数法:每个对象维护一个引用计数器,当有一个地方引用它时,计数器加1,如果引用失效时,计数器减1.任何时刻当计数器为0的对象是不可能再被使用的。
    优点:实现简单,判定效率高。
    缺点:无法解决对象之间相互引用的循环问题。
    虚拟机并没有用该方法来判断你对象是否存活。
  2. 可达性分析算法:通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则这个对象是不可用的,被判为可回收对象。
    • GC Roots可以理解为由堆外指向堆内的引用, 一般而言,包括:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。
      可达性分析算法中的不可达对象会被标记并筛选是否需要执行finalize()方法,如果需要执行,则将该对象置入F-Queue队列中,等待一个由虚拟机自动建立的低优先级的finalizer线程去执行。finalize()方法是对象避免被GC的唯一一次机会,因为一个对象的finalize()方法置多被系统调用一次。
    • 然后GC会对F-Queue队列中的对象进行二次标记,如果还是有引用链就从即将回收的集合中移除,否则会被GC。
    • 一个对象没有覆盖finalize()方法或者该方法已经被虚拟机调用过,则被判断为不需要执行该方法。
    • finalize()方法的运行代价高,不确定性大,无法保证各个对象的调用顺序。

方法区回收

主要是回收废弃常量和无用的类,性价比较低。而堆尤其是新生代的内存,一次垃圾回收一般可以回收70%-95%的空间。
  1. 废弃常量的回收:类似于回收java堆中的对象,比如没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,在GC时,如有必要会将该常量清理出常量池。常量池的其他类(接口)、方法、字段的符号引用与之类似。
  2. 无用的类的判断,需要同时满足以下三个条件:
    (1)该类的所有实例已经被回收,即java堆中不存在该类的任何实例。
    (2)加载该类的ClassLoader已经被回收
    (3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    满足上述三个条件的无用类可以被回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class和-XX:+TraceClassLoading、-XX:+TraceClassUnLoading来查看类加载和卸载信息。
    在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoder的场景都需要虚拟机具备类卸载功能,以保证永久代不会溢出。

垃圾收集算法

  1. 标记-清除(Mark-Sweep)算法:首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记对象。
    缺点:(1)效率不高:标记和清除的效率都不高
    (2)空间问题:会产生大量内存碎片,导致以后需要分配一个较大内存时,无法找到足够大的连续内存而不得不提前触发一次垃圾回收
  2. 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存用完之后,就将还存活着的对象复制到另一块内存中,然后将已使用过的内存一次清理。这样每次只对整个半区进行内存回收,内存分配时也不用考虑内存碎片等问题,只需要移动堆顶指针,按顺序分配内存即可。
    优点:实现简单,运行高效
    缺点:内存缩小了原来的一半。如果对象存活率较高时,复制操作会降低效率。
    现在的商业虚拟机使用该算法来回收新生代。因为新生代中的对象98%是“朝生夕死”的,即生命周期很短,并不需要按照1:1的比例来划分内存空间。所以将内存分成了一个较大的Eden区和两个小的Survivor区(8:1:1),每次使Eden区和一个Survivor区。当垃圾回收时将Eden区和一个Survivor区中的存活对象复制到另一块Survivor空间,最后清理掉Eden和之前用过的Survivor空间。
    当Suvivor空间不够使用时,需要依赖其他内存(老年代)进行分配担保。
  3. 标记-整理算法:标记过程与标记清除算法一致,但后续动作不是直接对可回收对象进行清除,而是对所有存活的对象向一端移动,然后清除端边界以外的内存。(老年代)
  4. 分代收集算法:根据对象存活周期的不同将内存分为几块,一般是新生代和老年代。然后根据各个年代特点选择适当的收集算法。新生代的对象因为生命周期比较短,所以每次都是大批量的对象死亡,只有少量存活,选择复制算法的成本比较低;而老年代的对象存活率比较高,没有额外的空间可做分配担保,而且复制成本也比较高,因此一般才选择标记清除或者标记整理算法。

HotSpot算法实现:

  1. 枚举根节点:
    (1)可作为GC roots的节点有很多,逐个检查需要消耗很多时间
    (2)可达性分析对执行时间的敏感点还体现在GC停顿上。因为在整个可行性分析期间,对象的引用关系是不能出现仍然在不断变化的情况的,因此就要求系统在该时刻看起来处于一个冻结的时间点,这也是导致GC进行时必须停顿所有Java执行线程的一个重要原因
    准确式GC:在执行系统停顿时,不需要一个不漏地检查完所有执行上下文(例如栈帧中的本地变量)和全局的引用(例如常量和静态属性),虚拟机应该是有办法直接知道哪些地方存放着对象引用的。例如:HotSpot虚拟机使用一组称为OopMap的数据结构,在类加载完成时,会将对象内什么偏移量的位置上是什么样的数据类型给计算出来,在JNI编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。
  2. 安全点:
  • 什么是安全点:OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
    • 实际上,HotSpot也的确没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
    • Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。
  • 如何选择安全点:安全点的选定是以“是否具有让程序长时间执行的特征”为标准进行选定的。
    • 因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行。所以“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
  • 在GC发生时,如何让所有线程都跑到安全点从而进行GC停顿呢?
    • 抢先式中断:不需要线程的执行代码主动配合。GC发生时,线中断所有线程,如果发现有线程未跑到安全点,恢复该线程,让其跑到安全点。(几乎没有虚拟机采取该方法暂停线程来响应垃圾回收事件)
    • 主动式中断:当GC需要中断线程时,不直接对线程进行操作,仅通过设置标志。在线程执行的过程中,由各个线程主动轮询该标志,当发现标志为true时,自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
  • 安全区域:安全点保证了程序执行时,在不太久的时间内就会遇到可以进入GC的安全点。但是对于处于sleep或者block状态的线程来说,无法响应JVM中断请求,主动走到安全点进行中断挂起。因此出现了安全区域。
    • 安全区域是指在一段代码片段中,引用关系不会发生任何变化。在这个区域中的任何位置开始GC都是安全的。当线程走到安全区域时,会标识自己已经进入安全区域状态。那么在GC时就不需要管标识为该状态的线程了。
    • 在线程要离开安全区域时,它需要检查系统是否已经完成了根节点枚举或者整个GC过程,如果已经完成,该该线程可以继续执行,如果没有,需要等待直到收到可以离开安全区域的信号为止。

垃圾收集器

从Serial收集器、Parallel收集器、CMS收集器再到G1收集器,用户线程的停顿时间在不断减少,但并没有完全消除。

  • 并行:多条垃圾收集器并行工作,但是用户的工作线程还处于挺顿状态
  • 并发:用户线程和垃圾收集器线程可以同时执行,但并不一定是并行执行,可能会交替执行。用户程序在继续执行,而垃圾收集器运行在另一个CPU上。
  1. Serial收集器(复制算法):最基本、历史最悠久的收集器。JDK1.3.1之前,新生代唯一的收集器。
    • 单线程的收集器:只有一个CPU或者一条线程来完成垃圾收集工作,并且在它工作的同时,所有的工作线程必须暂停直至垃圾收集工作完成。是虚拟机运行在Client模式下的默认新生代垃圾收集器。
    • 优点:简单高效,在单CPU环境中,不存在线程切换的开销。
    • 缺点:在用户看不到的地方暂停用户的工作线程,对很多应用来说难以接受。
  2. ParNew收集器(复制算法):serial收集器的多线程版本,除了使用多线程来进行垃圾收集外,其余行为包括控制参数、收集算法,暂停工作线程以及对象分配规则和回收策略等,与Serial收集器完全一样。
    • 是虚拟机运行在server模式下的首选新生代收集器,原因之一:它是除了Serial收集器外,唯一可以与CMS收集器配合工作的收集器。因为CMS不能与JDK1.4已经存在的Parallel Scavenge收集器搭配使用,所以JDK1.5以后,如果使用CMS作为老年代收集器,新生代的收集器只能是Serial或者Parallel收集器
    • “-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器。
    • “-XX:+UseParNewGC”:强制指定使用ParNew。
    • “-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。
  3. Parallel Scavenge收集器(复制算法):多线程的垃圾收集器。与其他收集器关注减少用户工作线程停顿时间不同的是,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
    • 吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    • 停顿时间越短越适合需要与用户进行交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效率的利用CPU时间,尽快完成程序运算,主要适合在后台运算而不需要太多交互的任务。
    • -XX:MaxGCPauseMillis:控制最大垃圾收集挺顿时间,允许的值是一个大于0的毫秒数,收集器尽可能地保证内存回收花费的时间不超过该设定值。并非越小越快,因为这是通过牺牲吞吐量和新生代内存空间换取的。
    • -XX:GCTimeRatio:直接设置吞吐量大小,一个大于0且小于100的整数。垃圾收集时间比例=1/(1+该参数),默认为99,即1%的垃圾收集时间。
    • -XX:UseAdaptiveSizePolicy”参数是一个开关,如果这个参数打开之后,虚拟机会根据当前系统运行情况收集监控信息,动态调整新生代大小(-Xmn),Eden与Survivor区的比例、老年代大小等细节参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略。该策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
  4. Serial Old收集器(标记-整理算法):Serial收集器的老年代版本,也是一个单线程收集器,主要也是给Client模式下的虚拟机使用。
    • 对于Sever模式下的虚拟机下使用:一是与JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure 时使用。
  5. Parallel Old收集器(标记-整理算法):Parallel Scavenge收集器的老年代版本,使用多线程与“标记 -整理”算法。
    • JDK1.6以后提供,因为Parallel Scavenge收集器不能与CMS收集器搭配使用,所以之前如果新生代选择Parallel Scavenge收集器,老年代只能选择Serial Old收集器。但是单线程的Serial Old收集器无法充分利用服务器多CPU的处理能力,吞吐量无法达到目的。
    • 在注重吞吐量和CPU资源敏感的场合,优先考虑Parallel Scavenge+Parallel Old收集器
  6. CMS收集器(标记清除算法):以获取最短回收停顿时间为目标的垃圾收集器
    • 垃圾回收步骤:
      1)初始标记:标记GC Roots能直接关联到的对象,速度很快。
      2)并发标记:进行GC Roots Tracing的过程
      3)重新标记:修正并发标记期间因用户程序继续进行而导致标记产生变动的那一部分对象的标记记录
      4)并发清除
      初始标记和重新标记时,还是需要暂停用户的工作线程。而耗时最长的并发标记和并发清除过程都是可以与用户线程一起工作的。
    • CMS默认启动的回收线程数是(CPU数量+3)/4,即当cpu数量在4个以上时,并发回收时垃圾收集器的线程数不少于25%的CPU资源,并随着CPU资源的增加而下降。
    • 缺点:
      1)对CPU资源非常敏感(面向并发设计的程序对CPU资源都比较敏感)。在并发阶段,虽然不会暂停用户线程,但是因为占用了一部分线程,还是会导致应用程序变慢,吞吐量降低。
      2)CMS收集器无法处理浮动垃圾,可能出现“Conturrent Mode Failure”失败而导致另一次Full GC产生。
      由于CMS并发清除阶段用户线程还在运行,伴随着程序还在产生新的垃圾,这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只能留到下次再清理,这一部分垃圾称为“浮动垃圾”。
      也正是由于在垃圾收集阶段用户线程还在运行,那么也就需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代填满之后再进行收集,需要预留一部分空间给并发收集时用户程序使用。
      可以通过“-XX:CMSInitiatingOccupancyFraction”参数设置老年代内存使用达到多少时启动收集。(JDK1.5 默认当老年代使用了68%的空间就会激活CMS收集器,JDK1.6中已经提升至92%)
      如果在CMS运行期间,预留的内存无法满足程序需要,就会出现一次“Conturrent Mode Failure”失败,这时虚拟机会启动后备预案:临时启动Serial Old收集器来重新进行老年代的垃圾收集。会增加停顿时间。
      3)由于CMS收集器是一个基于“标记-清除”算法的收集器,那么意味着收集结束会产生大量碎片,有时候往往还有很多内存未使用,可是没有一块连续的空间来分配一个对象,导致不得不提前触发一次Full GC。
      CMS收集器提供了一个“-XX:UseCMSCompactAtFullCollection”参数(默认是开启的)用于在CMS收集器顶不住要FullGC时开启内存碎片整理(内存碎片整理意味着无法并发执行不得不停顿用户线程)。
      参数“-XX:CMSFullGCsBeforeCompaction”来设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值是0,意味着每次进入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倍以上。
  7. G1收集器(标记-整理算法):面向服务端的垃圾收集器,目标追求还是低停顿
    • 特点:
      • 并发与并行:能充分利用多CPU、多内核环境的硬件优势,与用户线程并发或者并行执行
      • 分代收集:虽然不需要其他垃圾收集器的配合,但是他的内部还是采用不同的方式去管理新创建的对象和已经存活一段时间,经过多次GC仍然存活的对象的
      • 空间整合:整体上看是基于“标记整理算法”,局部上看是基于“复制算法”,但不管哪个算法,都不会产生内存碎片,回收后都会提供规整的内存。
      • 可预测的停顿:G1除了追求低停顿之外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    • G1收集器保留了新生代和老年代的概念,但与其他收集器中新生代和老年代是物理隔离概念不同的是,G1收集器将整个java堆内存划分为多个大小相等的区域Region,新生代和老年代是部分Region的集合(可能不连续)。
    • G1收集器之所以能够建立可预测的停顿时间模型,是因为他避免了在整个Java对中进行全区域的垃圾收集。他跟踪各个区域的垃圾回收价值(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First名称的由来)。
    • G1收集器的运作大致可分为:
      • 初始标记: 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
      • 并发标记: 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
      • 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
      • 筛选回收: 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
    • 避免全堆扫描——Remembered Set:Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象), 如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

内存分配与回收策略

	对象主要分配在新生代的Eden区,如果启动了本地线程分配缓冲,则优先分配在线程的TLAB上,少数情况下直接分配在老年代。

分配规则主要取决于当前使用的垃圾收集器组合以及虚拟机中与内存相关的参数设置。

  1. 对象优先在Eden区分配,如果Eden区没有足够的空间进行分配时,将发起一次Minior GC
  2. 大对象直接进入老年代:需要大量连续内存空间的Java对象,比如很长的字符串和数组。
    -XX:+PretenureSizeThreshold参数,内存大于这个设置值对象直接在老年代分配,以避免在新生代的Eden区和Survivor之间发生大量的内存复制。注意:该参数只对Serial和ParNew收集器有效,必须设置该参数的时候可以i考虑ParNew+CMS收集器。
  3. 长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄的计数器,每进行一次Minior GC的对象,年龄加1。当年龄大于一定程度(默认15,也可以通过-XX:+MaxTenuringThreshold设置),对象就会被晋升到老年代。
  4. 动态对象年龄判断:如果在Survivor空间中,相同年龄的对象的内存占用总和已经大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。
  5. 空间分配担保:
    在进行一次Minor GC之前,虚拟机会check老年代最大可用的连续空间大小是否大于新生代所有对象的总和:
    * 如果是,就说明此次Minor GC是安全的,可以进行
    * 如果不是,虚拟机会去check参数-XX:HandlePromotionFailure的值是否允许担保失败: 如果是,虚拟机会去check老年代最大可用的连续空间大小是否大于历次晋升到老年代对象的平均大小:
    如果是,就会尝试进行一次Minor GC;失败后会进行一次Full GC。如果不是,则直接进行一次 Full GC。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值