深入理解Java虚拟机----(三)内存分配策略和垃圾收集器

时间:2022-12-27 12:34:17
垃圾回收:     垃圾回收面临着三个问题:回收什么、什么时候回收、怎么回收。     哪些对象已经不再被需要了,就需要被回收。
  • 引用计数法:教科书式解释,每个对象维护对它的引用的个数。但是主流虚拟机不适用,因为难解决循环引用。
  • 可达性分析算法:主流适用的算法。虚拟机选定一些GCRoot对象,如果一个对象和GCRoot对象间没有引用链,则这个对象是无用的、可以回收的。可以作为GCRoot的有:
    • 虚拟机栈中引用的对象
    • 方法区中,类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中引用的对象
    这两种方法都和引用相关。JDK1.2对引用进行了扩充:
  • 强引用:代码常用的引用,存在就不会被回收
  • 软引用:有用但非必须的引用。如果快OOM了,会把这部分对象进行回收。SoftReference
  • 弱引用:非必须引用。只能活到下一次回收之前。WeakReference
  • 虚引用:不会对生存时间有影响。唯一作用是可以在对象被回收时受到通知。PhantomReference
    可达性分析过后,如果一个对象和GCRoot间没有可达的引用链了,它并不是立即被回收。首先要看它有没有finalize()方法,有的话被没被执行过。如果有而且没执行过,把它放到一个F-Queue队列,虚拟机一个单独的线程区调用对象的finalize方法。但是并不保证一定执行完。对象可以在这个方法中救自己--给自己一个引用。如果没救成功,则就被回收了。救成功的对象,再次被可达性分析筛选为可回收时,不会再执行fianlize,因为只会被执行一次!finalize方法代价高、不确定性大,非常不建议使用
    方法区----常说的HotSport中的永久代,也可以被回收,但是效果通常不如堆好。而且堆内对象不使用了一定会回收,而方法区是可以被回收,是有差别的!字面常量等比较容易处理,没有引用就可以被回收。而类回收条件酒比较严苛:
  • 类的所有实例对象已经被回收
  • 加载类的ClassLoader已经被回收
  • 没有对该类对象的引用
    可以使用-Xnoclassgc控制。大量使用反射、动态代理、动态JSP等的系统,建议开启,防止方法区溢出。
    垃圾收集算法:
  • 标记清除:基础算法。标记的方法上面说过了。这个算法的缺点是会产生不连续的内存碎片,导致明明有很多内存剩余,但由于新对象太大,没有足够的连续空间而引发又一次的GC。
  • 复制算法:将空间划分为相等的两块区域,当前的满了,就把存活的对象复制到第二块块,整齐排列,然后在第二块开始为新对象分配内存。好处是解决了标记清除的碎片问题,但代价太大----内存缩小一半。商业虚拟机绝大部分采用这种算法回收新生代,但新生代一般98%的对象都是很快死掉,所以不用1:1的分配,而是将新生代分为一块eden和两块survivor,每次把eden和一块survivor的存活对象复制到另一块,回收剩下的。默认比例eden:suvivor = 8:1,所以这种方法只有10%的空间被“浪费”。但是,我们并不能保证存活下来的对象永远少于新生代10%的大小,如果不够了,需要依赖老年代分配担保。
  • 标记整理算法:像老年代这种区域,对象的存活率较高复制算法的效率就会比较低。而且不想浪费50%就要分配担保,所以老年代一般不使用复制算法。标记整理是标记后,将存活对象移动到一端,将剩下的内存清理。
  • 分代收集算法:分代只是将整个内存区域划分为几个小区域,可以在每个区域上选择各自的收集算法。例如新生代,大多短命,采用复制算法。老年代存活率高,没有额外空间担保,采用标记清除或标记整理。
HotSport实现:
    虚拟机实现必须保证高效、准确。HotSport在下面几个方面的实现方法:
  • 枚举根节点:虚拟机必须对一个一致性快照分析才能准确,不能一边分析一边还在变,所以枚举根节点要暂停所有线程(Stop-The-World)。如果挨个的分析方法区和栈中的引用,会非常耗时,HotSport用一个叫做OopMap的结构记录了这些信息,从而可以直接快速的拿到数据。
  • 安全点SafePoint:OopMap的状态随时都会改变,不可能为每条指令都生成OopMap,所以定义了安全点的概念。只有在安全点才能生成OopMap,才能暂停线程进行回收。安全点选择原则是方法调用、循环跳转、异常跳转等具有让程序长时间执行的特征的指令。
  • 安全区域:安全点的方案,在线程不分配cpu时间的情况下,如sleep,就无法走到安全点暂停,GC就被卡住了,这时需要安全区域解决。安全区域是指在一个区域内,引用关系不会发生变化,任意位置开始GC都是可以的。离开安全区域时,需要等GC枚举根节点结束。
垃圾收集器:     垃圾收集器是上面介绍的垃圾收集算法的具体实现。在HotsSport中包含以下几种:
    深入理解Java虚拟机----(三)内存分配策略和垃圾收集器深入理解Java虚拟机----(三)内存分配策略和垃圾收集器     先解释:并行是指多个收集线程并行,并发是收集线程和用户线程同时执行。     这7中作用于不同的代,连线表示可以配合使用。
  • Serial:最基本的单线程收集器。而且会暂停所有线程直到收集结束。Serial作用于新生代用复制算法。它有弊端,但是是现在client模式下的默认新生代收集器。
  • ParNew:就是Serial的多线程并行版本。除了多线程,其他与Serial一模一样,复制算法,暂停所有线程。是新生代收集器的首选,可以与CMS配合使用。
  • Parallel Seavenge:也是作用于新生代复制算法的实现。区别是追求的是吞吐量,可以参数设置最大停顿时间和吞吐量。收集器会收集系统信息,自动调节各区域大小、比例等来优化,达到设定的目标。
  • SerialOld:是Serial的老年代版本,单线程,标记整理算法。主要用于client模式。另一个用途是作为CMS的后备方案。
  • Parallel Old:Parallel Seavenge的老年代版本,标记整算法。
  • CMS:追求最短停顿时间,老年代,标记清除算法。分为4个步骤:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记需要stop the world。初始标记只是标记GCRoot直接关联的对象,非常快。并发标记是GCRoot的tracing过程。重新标记是为了修正并发标记期间,用户线程继续运行导致的变动,也很迅速。第一步和第三部都很快,所以用户线程停顿时间很短,这就是CMS的目的。它很优秀:并发、停顿短。但是也有缺点:
    • 并发阶段和用户线程并行,会占用cpu资源,影响用户程序,总吞吐量降低。
    • 并发期间程序会需要空间给新对象,所以不能等100%满了再开始回收。这个比例需要控制。太小导致浪费,太大可能会并发阶段不够了,而且用备用的SrialOld,等待时间很长。
    • 因为是用的标记清除算法,会有碎片,如果无法找到足够的连续空间,会因为一次full gc。可以参数设置在要full gc时,合并整理。这个整理操作无法并发,因此停顿时间会变长。还有一个参数设置多少次的不整理的full gc后,跟随一次带整理的full gc。
  • G1收集器:
    • 缩短stop the world时间
    • 不用配合其他算法就能对整个堆管理,但可以用不同的方法管理不同的对象区域。
    • 总体看是标记整理,局部看是复制算法。所以不会产生碎片。
    • 与CMS相比,不知追求低停顿,同时建立可预测停顿时间的模型。
          G1将堆划分为多个相同大小的区域,新生代和老年代的概念仍被保留,但是只是一系列区域的集合而已。它将每个区域估算回收价值,在用户允许的停顿时间内,尽可能的回收价值高的区域。它的步骤和CMS非常类似:
  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
          但是回收阶段不是并发的,因为暂停用户线程能大幅提高回收效率。如果追求低停顿、可预测停顿,G1可以尝试。如果追求吞吐量,没有什么好处。

GC日志:     深入理解Java虚拟机----(三)内存分配策略和垃圾收集器     开头数字式虚拟机运行时长,单位秒。中括号然后跟着本次gc类型:GC或Full GC。再中括号,跟着回收区域。然后是已使用内存->回收后已使用内存(区域总内存)。然后跟着区域回收用时,单位秒。然后中括号外是堆的总内存情况和总用时。
常用参数: 深入理解Java虚拟机----(三)内存分配策略和垃圾收集器深入理解Java虚拟机----(三)内存分配策略和垃圾收集器


内存分配策略:
  • 在eden区分配内存,也是大多数情况
  • 在老年代直接分配,因为分配担保机制。
  • 新生代熬过了一定次数的Minor GC,年龄足够大的就进入老年代。默认年龄阀值为15,可配置。
  • 并不是永远炒锅阀值才进入老年代。当新生代内同一年龄的对象大小总和超过了Survivor区的一半,年龄大于等于该年龄的对象,直接进入老年代。
  • 分配担保:使用复制收集算法时,可能某次minor gc存活对象很多,survivor不足以承受,就需要老年代分配担保。所以在进行Minor GC前,虚拟机会检查老年代剩余空间是否大于新生代对象大小综合。如果大于则安全;如果小于,看参数设置是否允许冒险,如果允许冒险,会比较以往每次从新生代到老年代的对象大小的平均值和当前老年代的空余空间大小,如果剩余空间大,则允许此次Minor GC,如果剩余空间小或者不允许冒险,则Full GC。如果Minor后,老年代空间还是不够用,则还要FullGC。虽然如果冒险失败了,绕了很大很大的圈子,但是一般情况下,还是设置允许冒险,避免FullGC的频繁发生。这个开关是HandlePromotionFailure。