使用堆外内存的好处

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,
    所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减
    少回收停顿对于应用的影响。
  • 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内
    存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存
    数据,都建议存储到堆外内存。

堆内内存(on-heap memory)

对外内存和堆内内存是相对的概念,其中堆内内存使我们平常工作中接触比较多的,我们在JVM参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值。

堆内内存=新生代+老年代+持久代

在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。

常见的垃圾回收算法:

  • 引用计数器法(Reference Counting)
  • 标记清除法(Mark-Sweep)
  • 复制算法(Coping)
  • 标记压缩法(Mark-Compact)
  • 分代算法(Generational Collecting)
  • 分区算法(Region)

堆外内存(off-heap memory)

堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。Java中常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用。DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作,下面介绍构造方法

DirectByteBuffer(int cap) {                 

        super(-1, 0, cap, cap);
        //内存是否按页分配对齐
        boolean pa = VM.isDirectMemoryPageAligned();
        //获取每页内存大小
        int ps = Bits.pageSize();
        //分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        //用Bits类保存总分配内存(按页分配)的大小和实际内存的大小
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
           //在堆外内存的基地址,指定内存大小
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        //计算堆外内存的基地址
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

在Cleaner 内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的 线程对象(Runnable),回收操作是发生在 Cleaner 的 clean() 方法中。

DirectBuffer使用注意事项

java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。

Cleaner VS Finalize

  • Cleaner
  • Cleaner继承自PhantomReference,它本质上仍然是一个Reference。所以它的处理方法与WeakReference,SoftReference十分相似。仍然是由GC标记,Reference Handler线程处理的。
  • Cleaner本身不带有清理逻辑,所有的逻辑都封装在thunk中,因此thunk是怎么实现的才是最关键的。
  • Cleaner中的next和prev是private的,一定要记住这一点,不要和Reference的成员变量next 混淆了。它们不是一回事。这个next, prev是双向链表,而Reference的next则是由JVM维护的。
  • finalize
  • finalize方法定义在Object中
  • 在这个方法中可以释放各种资源。
  • 如果一个定义了finalize方法的类在初始化的时候,就会调用一下这个static方法,生成一个Finalizer对象,而我们自己的对象就是这个方法中所使用的finalizee。也就是说,我们新建一个带 finalize 方法的对象,就会伴生一个 Finalizer 对象。
  • Finalizer 继承自 FinalReference,因此Finalizer也是一种Reference,所以前边Reference的处理逻辑是和Weak, Soft reference的逻辑十分相似的。
  • inalizer和Cleaner的作用也十分相似,但有一个巨大的不同在于,finalize方法里可以使object 复活,而 Cleaner 的 clean 方法中不能使得对象复活。

强引用(Strong Reference)> 软引用(Soft Reference)(弱引用(WeakReference)> PhantomReference(虚引用)

  • 强引用(Strong Reference)
Object o= new Object(); //只要o还指向Object, object就不会被回收
  • 弱引用(Weak Reference)
    当一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向的时候, 如果GC运行, 那么这个对象就会被回收.
WeakReference<Object> wr = new WeakReference<Object>(new Object());
  • SoftReference
    当系统内存不足时soft reference指向的object才会被回收. 正因为有这个特性, soft reference比weak reference更加适合做cache objects的reference. 因为它可以尽可能的retain cached objects, 减少重建他们所需的时间和消耗.
    怎样判断内存不足呢?
if (rt == REF_SOFT) {
    // For soft refs we can decide now if these are not
    // current candidates for clearing, in which case we
    // can mark through them now, rather than delaying that
    // to the reference-processing phase. Since all current
    // time-stamp policies advance the soft-ref clock only
    // at a major collection cycle, this is always currently
    // accurate.
    if (!_current_soft_ref_policy->should_clear_reference(obj, _soft_ref_timestamp_clock)) {
      return false;
    }    
  }

should_clear_reference的实现如下所示:

// The oop passed in is the SoftReference object, and not
// the object the SoftReference points to.
bool LRUMaxHeapPolicy::should_clear_reference(oop p,
                                             jlong timestamp_clock) {
  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;
  }

  return true;
}

可见,SoftReference的回收还要满足一个条件,那就是当前引用的存活时间是不是大于_max_interval,如果大于_max_interval,那它就和WeakReference一样处理,如果不大于的话,那就当成普通的强引用处理。

public class SoftReference<T> extends Reference<T> {
    // 由JVM负责更新的,记录了上一次GC发生的时间。
    static private long clock;

    // 每次调用 get 方法都会更新,记录了当前Reference最后一次被访问的时间。
    private long timestamp;

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }

    // 和super.get的逻辑最大的不同,就在于每次调用get都会把上次发生GC的时间,也就是
    // clock 更新到 timestamp 中去。
    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}

_max_interval是在哪里设定的呢?

// Capture state (of-the-VM) information needed to evaluate the policy
void LRUMaxHeapPolicy::setup() {
  size_t max_heap = MaxHeapSize;
  max_heap -= Universe::get_heap_used_at_last_gc();
  max_heap /= M;

  _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
  assert(_max_interval >= 0,"Sanity check");
}

原来是计算了一下,上一次GC以后,堆里还有多少剩余空间,然后把这些空间通过一次乘法转换成_max_interval。也就是说,max_heap越小,那么_max_interval就会越小,SoftReference就会有越大的可能性被回收。很多人肯定都看过这句话:**SoftReference只会在内存空间不够用的情况下才会被回收。**但没有人能说清楚,什么情况算是堆内存不够用。我这里把代码show给大家看了。Hotspot里关于内存不够用可是有明确的定义的哦。

另外,这里还通过这代码向大家展示了一个JVM参数:SoftRefLRUPolicyMSPerMB。这个参数调得越小,SoftReference就会越倾向于尽快回收SoftReference。

  • PhantomReference
    PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理。应用场景就是sun.misc.Cleaner
  • FinalReference
    Finalizer继承FinalReference类,FinalReference继承Reference类,对象最终会被封装为Finalizer对象,如果去查看源码会发现Finalizer的构造方法是不对外暴漏,所以我们无法自己创建Finalizer对象,FinalReference是由jvm自动封装。