GC基础知识

C语言申请内存:malloc free

C++: new delete

Java: new ?

自动内存回收,编程上简单,系统不容易出错,手动释放内存,容易出两种类型的问题:

  1. 忘记回收
  2. 多次回收

什么是垃圾?

没有任何引用指向的一个对象或者多个对象(多个对象之间是循环引用)
浮动垃圾:这次没有回收,下次再回收

如何定义垃圾?

  1. reference count(引用计数)
    但是如果有循环引用的多个对象,是找不到的,就会发生内存泄漏。

  2. Root Searching(根可达算法)
    java的hotspot正在使用的算法。
    GC roots:根对象。包括
    一个程序运行起来以后,通过根找不到的全是垃圾。
    object o = new Object();这个new Object()一定不是垃圾。
    根对象指的是,我们现在正在跑的这个栈里面的局部变量(就是main方法中的局部变量)【线程栈变量】,以及我们从class里面引进的静态变量,还有一些常量池和JNI指针引用到的变量。
    在这里插入图片描述

常见的GC算法

  1. 标记-清除(Mark-Sweep)
    首先标记出想要回收的对象,接着统一进行清除。
    位置不连续,产生碎片
  2. 拷贝(Copying)
    “半区复制”:将内存分为两部分(1:1),每次只使用其中一块。用完一块,就将存后的对象复制到另一块上,清理掉这一块内存空间。
    不产生碎片,内存拷贝很快,浪费空间。
    “Appel式回收”:每次只使用Eden和一块Survivor空间,用完后复制到另一块Survivor空间上。
  3. 标记压缩(Mark-Compact)
    与复制算法相似,区别是先让所有存活的对象都向内存一端移动,再清理掉后面的内存空间。
    不产生碎片,但效率低,因为任何一块的挪动都需要进行线程同步。

JVM内存分代模型(用于分代垃圾回收算法)

  1. 部分垃圾回收器使用的模型

  2. 新时代 + 老年代 + 永久代(1.7)/ 元数据区(1.8) Metaspace
    · 永久代和元数据都是装Class对象的;
    · 永久代必须指定大小限制,但是元数据区可以设置,也可以不设置。无上限(受限于物理内存);
    · 字符串常量 1.7 - 永久代; 1.8 - 堆
    · MethodArea是一个逻辑概念,对应的是永久代/元数据区

  3. 新生代 = Eden + 2个suvivor区
    在这里插入图片描述
    (1)YGC回收之后,eden区大部分对象都会被回收。活的对象拷贝进survivor区。
    (2)再次YGC,eden活着的对象 +s0 -> s1
    (3)再次YGC,eden活着的对象 +s1 -> s0
    (4)年龄足够 -> 老年代 (老一点的垃圾回收器是15岁, CMS是6岁)
    (5)s区装不下 -> l老年代

  4. 老年代
    (1)顽固分子
    (2)老年代满了FGC【Full GC】 新生代老年代同时进行回收
    尽量减少产生FGC,因为使用到了标记压缩算法

    动态对象年龄判定
    年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域*TargetSurvivorRatio的时候,就从这个年龄段网上的年龄的对象进行晋升。
    例子:年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。

MinorGC = YGC
MajarGC =FGC

常见的垃圾回收器

在这里插入图片描述

Epsilon是测试使用的空的GC,调试JDK的。
Serial和Parallel可以看成是同样的种类用在不同的区域上。

  1. Serial 运行在年轻代 单线程回收【串行回收】
    垃圾回收的时候,应用程序是没办法运行的(STW,Stop the world)出现卡顿
    在这里插入图片描述

  2. Parallel Scavenge 年轻代 并行回收
    在这里插入图片描述

  3. ParNew 年轻代 配合CMS并行回收

  4. SerialOld

  5. ParallelOld

  6. ConcurrentMarkSweep(CMS) 并发的 垃圾回收和应用程序同时运行,降低STW时间(200ms内)

    前面长的甚至有几小时STW

  7. G1(10ms)

  8. ZGC (1ms PK C++)

  9. Shenandoah

  10. Epsilon

调优主要是1,2和4,5
因为1.8默认回收器是 PS+ parallelOld

CMS

承上启下垃圾回收器。第一款并发垃圾回收器。
有巨大问题:最长时间的STW就是CMS产生的。因为cms失败了,就要shang

从线程角度
在这里插入图片描述
初始标记会把根对象标出来。

错标

如果已经标记好了要清除的对象,结果在并发标记中,有的垃圾又被引用到了,这时候清除垃圾就会产生错标现象

很多算法就是为了高效率解决算法问题。重新标记就会修正错标

并发清理可能会产生浮动垃圾,就是标记好了要清除的对象,结果在这时候又出现了垃圾。这些垃圾会留到下一次清理的时候清理。

三色标记算法

黑色:自己已经标记,fields都标记完成(即接下来的遍历不会再遍历到它了,它的孩子都已经遍历完了)
灰色:自己已经标记,还没来得及标记fields
白色:没有遍历到的节点
如果灰色B>白色C的引用消失了,黑色A->白色C的引用出现了。此时就会把C当成垃圾,出现了错标。

CMS解决方案——Incremental Update

把黑色B变为灰色。(写屏障)

了解生产环境下的垃圾回收器组合

  • JVM命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
  • JVM命令参数分类
    标准:-开头 ,所有HotSpot都支持 java -version
    非标准:-X开头 Java -X
    不稳定:-XX开头
    1
    2
    3
    java -XX:+PrintCommandLineFlags  设置值(启动了哪些参数)
    java -XX:+PrintFlagsFinal 最终默认值
    java -XX:+PrintFlagsInitial 默认参数值

分配和垃圾回收大致流程

  1. 尝试在栈中分配对象,如果能分就分【逃逸分析,不能有逃逸,野指针问题;标量替换:把聚合量分解成为标量(原始数据类型)】方法结束,栈帧弹出的时候,对象顺带直接销毁,不需要垃圾回收。
  2. 如果逃逸分析失败,有引用指向它,就放到堆上。如果大对象,直接放老年代。老年代垃圾回收:FGC
  3. 假如不够大,TLAB(Thread Local Allocation Buffer,线程本地分配缓存区)一般是直接放到Eden区的。但是为啥有TLAB呢?因为一个程序开始,为许多线程分配资源的时候,一定会发生资源争用。JVM就要对这些线程进行管理、同步。所以效率偏低。为了提高效率,JVM设置了一种机制:JVM启动的每个线程都可以有私有的一块空间(放在eden区里面),以后new对象就new到自己的空间,满了再抢占。YGC最后到老年代。
    在这里插入图片描述