(下文内容都是针对HotSpot VM)

JVM内存结构

图示

  • 线程间共享(绿)
  • 线程间独立(蓝)

介绍

线程共享

  • 1 方法区(永生代)

    • 存储已经被加载的类信息、static变量.
    • 1.1 同时放置运行时常量,运行期间动态产生的常量也放在这
    • 参数 -XX:MaxPermSize

      SE8后改为MetaSpace

  • 2 堆
    较复杂,见下文

线程独立


  • Sun Hotspot不区分虚拟机栈和方法栈
    • 3 虚拟机栈 (Java Virtual Machine Stacks)
      • Java方法执行的内存模型,类似C++,每个方法调用都会在栈上留下局部变量表,操作数栈,动态链接,方法出口等, 调用完成就出栈
    • 4 本地方法栈 (Native Method Stacks)
      • 作用类似,区别在于为虚拟机执行native方法
  • 5 程序计数器 (Program Counter Register)
    • 记录线程切换时原来代码段的地址
    • 占空间很小
  • 6 直接内存
    • 不在JVM的范围内,就是不用经过JVM管理、映射的直接内存

GC过程

四种引用类型

2022-11-28T222315

  • 强引用(Strong Reference)

    1
    2
    Object object = new Object();
    // 只要强引用存在,垃圾回收就不会回收该对象,内存不足时会抛出OOM。
  • 软引用(Soft Reference)

    1
    2
    SoftReference<Object> softReference = new SoftReference<>(new Object());
    //定义:非必须,但仍有用的对象。内存不足时才会回收。

    further reading: 怎么定义内存不足
    Interval(该引用多少MS没访问过) = clock - timesatmp > heap_free_space_after_last_gc(MB) * SoftRefLRUPolicyMSPerMB(JVM paramrter, default value is 1000ms))
    Related Code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // https://github.com/AdoptOpenJDK/openjdk-jdk12u/blob/master/src/java.base/share/classes/java/lang/ref/SoftReference.java
    public class SoftReference<T> extends Reference<T> {
    private static long clock; //Timestamp clock, updated by the garbage collector

    private long timestamp; //Timestamp updated by each invocation of the get method.
    public T get() {
    T o = super.get();
    if (o != null && this.timestamp != clock)
    this.timestamp = clock;
    return o;
    }
    }

    //https://code.googlesource.com/edge/openjdk/+/refs/heads/jdk8u111-b09/hotspot/src/share/vm/memory/referencePolicy.cpp
    bool LRUCurrentHeapPolicy::should_clear_reference(...) {
    jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
    assert(interval >= 0, "Sanity check");
    // The interval will be zero if the ref was accessed since the last scavenge/gc.
    if(interval <= _max_interval) { return false; }
    }
  • 弱引用(Weak Reference)

    1
    2
    WeakReference<Object> weakReference = new WeakReference<>(new Object());
    //定义:发生GC时就会被回收
  • 虚引用(Phantom Reference)

    1
    2
    3
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference<Object> pReference = new PhantomReference<>(new Object(),queue);
    //不影响对象的生命周期,初始化必须有队列 只是在对象释放后,虚引用会进ReferenceQueue队列。

对于三种非强引用,一般都要结合referenceQueue使用,用于捕获哪些reference被释放了。如无必要,自己写代码请只用强引用。对该部分不理解也不影响对GC过程的理解。

垃圾标记算法

引用计数 Reference Counting Collector

python默认采用的策略

  • 优:
    • 简单,就是类似C++的shared pointer。 早期做法。
  • 缺:
    • 无法鉴别循环引用,即
    • 效率变慢,每次指针操作可能修改计数。
      1
      2
      3
      a.next = b; 
      b.next = a;
      a = b = null;

根搜索算法 Root Tracing Collector

Java和C#默认采用的策略

过程

  1. 通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
  2. 找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
  3. 重复(2)。
  4. 搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。

根集(Root Set)

就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),包含

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI(Native方法)的引用对象

标记过程

关键点: 高优先级别,全局暂停标记; 低优先级异步释放

注意点

  • 标记只能在静态内存结构上运行,JVM需要首先达到safepoint,然后标记过程全局暂停Stop the World(STW),
  • 执行(暂停)时间是看存活对象的个数,与堆大小无关,对象大小无关。
    • 至于待释放对象个数,一般不相关,只有标记-清理算法 时间复杂度和待释放对象个数有关。

safepoint 是一个big topic,以后有空再另外写一篇吧。

从标记到删除

至少需要两次标记

  1. 是否需要finalize的标记: 根未搜索、第一次标记、到且有必要执行finalize的对象打上标记放入F-Queue等待释放队列。

    • 未执行过finalize
    • finalize未被重载
  2. 虚拟机自动建立的、低优先级的Finalizer线程去执行对象的finalize()方法

  3. 对象执行finalize()如果仍旧没有被引用,就开始执行回收流程

为什么需要finalize()而不直接回收

因为不是所有内存都是JVM自己管理的,比如开辟的直接内存,就需要自己显式的申请和删除

回收算法

标记-清除 mark-sweep

标记后直接在内存中原地清除对象

  • 优:
    • 对象不需要移动
    • 只操作不存活对象,存活对象多也不影响效率
  • 劣:
    • 标记清除效率低,因为需要维护一个空闲表
    • 清除产生大量不连续的内存碎片
  • 适合:
    • 希望暂停时间尽可能短的
    • 存活比例高的

标记-整理 mark-compact

不直接对回收对象进行清理,而是所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  • 优:
    • 整理后无碎片=>内存分配更加快速, 空闲大小和位置便于统计
  • 劣:
    • 因为要移动存活的变量,和重新引用赋值, 就需要执行暂停,使得GC时间延长
  • 适合:
    • 存活比例高
    • 回收不频繁

停止-拷贝 copying

拿空间换时间,将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面)。
只有对象区与空闲区的切换过程中,程序暂停执行。

  • 优:
    • 无碎片同mark-compact
    • 标记和复制可以同时执行
    • 每次只对一块内存进行回收,高效
    • 实现简单
  • 劣:
    • 额外空间,空间利用率降低
    • 也需要暂停
  • 适合:
    • 存活比例少
    • 内存申请次数多
    • 回收不频繁

其他

Adaptive 算法:垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

所有new创建的对象都在堆中分配

分代示意图

对象分配原则

  • 对象优先在Eden分配。
  • 大对象直接进入老年代。
  • S1满时,S1所有数据进入老年代。
  • 长期存活(年龄超过15)的对象将进入老年代: 每次Minor GC使得Survivor区内的对象年龄+1

Minor GC 和 Full GC

  • 新生、年轻代 Young Generation: 对象死亡率高,少量存活, 适合停止-拷贝
    • Eden区 80%: 大部分对象在此生成
      • 一般满时触发 Minor GC(Scavenge GC) : 从Eden停止-拷贝到Survivor0,直到Eden清空位置
    • Survivor0 10%
      • 满时 交换S0和S1,(本质就是就是把数据从0转到1),完成后S0和Eden都为空
    • Survivor1 10%
      • 满时将数据转移到 老年代
  • 年老: 对象存活率高,适合 标记-整理,如果希望暂停时间减少,也可考虑标记-删除
    • 一般满时 触发 Full GC : 新生代、老年代、永生代都进行回收
  • 永生: 不怎么需要管理
    • 写满时也会触发 Full GC

这个永生在JAVA SE8后被移除,改为MetaSpace,毕竟从意义上就是记录了程序的元信息。
各代各部分比例都可以通过参数调整
另外 Full GC可以通过System.gc()显示调用;

一些经验

减少GC开销的技巧

  • 没经验不要显式调用System.gc(),因为增加了暂停次数
  • 减少临时对象的使用
  • 对象不用时最好显式置为Null,手动悬空,加快GC
  • 使用StringBuffer,而不用String来累加字符串
  • 能用基本类型如Int,Long,就不用Integer,Long对象
  • 尽量少用静态对象变量: 静态变量属于全局变量,不会被GC回收,它们会一直占用内存
  • 散对象创建或删除的时间: 集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,也是类似的。

Java 可能出现内存泄露的情况

即使根搜索看起来天衣无缝,依旧存在问题,如下:

  1. 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  2. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

GC性能调优 和 主流GC对比分析

建议看我的下一篇

Reference