深入学习Java虚拟机学习笔记-垃圾收集器与内存分配策略

时间:2022-12-28 08:27:10

1. 判断对象已死的方法

1.1 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它,就加1。缺点是很难解决对象之间的相互循环引用。

1.2 可达性算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的。

    能作为GC Roots的对象包括:栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中引用的对象。

1.3 引用(reference)

    侠义定义:如果reference类型的数据中存储的数值代表另一个块内存的起始地址,就叫引用。

    广义定义:当内存空间还够,则能保留在内存中;当内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。

    引用分为4种:

        强引用:new 产生的对象。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

        软引用(SoftReference):  SoftReference sfRefer = new SoftReference (obj ); 

        如果有一个对象具有软引用。在内存空间足够的情况下,除非内存空间接近临界值、jvm即将抛出oom的时候,垃圾回收器才会将该引用对象进行回收,避免了系统内存溢出的情况。(前提也是对象指向不为空)因此,SoftReference 引用对象非常适合实现内存敏感的缓存,例如加载图片的时候,bitmap缓存机制。

        弱引用(WeakReference): WeakReference weakRefer = new WeakReference(obj );
        当垃圾回收器扫描到弱引用的对象的时候,不管内存空间是否足够,都会直接被垃圾回收器回收。不过也不用特别担心,垃圾回收器是一个优先级比较低的现场,因此不一定很快可以发现弱引用的对象。

        对比纯Java环境,对于面向移动终端的Android系统,对于缓存机制比较敏感,以及对于内存管理更加严格。软引用(SoftReference)适合应用在需要cache的场景,一般面向实现内存敏感的缓存;弱引用(WeakReference)则是适用在某些场景为了无法防止被回收的规范性映射,它优先级最低,一般与引用队列联合使用。而且,谷歌不推荐使用软引用。

        虚引用(PhantomReference)
        与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
        虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
        ReferenceQueue queue = new ReferenceQueue ();
        PhantomReference pr = new PhantomReference (object, queue);

       程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

1.4 finalize()函数

       要宣告一个对象死亡,需要经历两次标记过程:第一次是发现没有与GC Roots相连,将对象标记,并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过,都被视为“没有必要执行”。如果判断有必要执行,就将对象放置到一个F-Queue队列中,稍后有一个由虚拟机自动建立、低优先级的Finalizer线程去执行它(仅触发执行,不会等待运行结束,避免finalize执行慢,导致gc系统奔溃)。finalize函数是对象逃脱死亡的最后一次救命机会。第二次标记该对象时,它将被移出“即将回收”的集合。

        finalize是java刚诞生时对c/c++程序员的妥协,它运算代价高昂,不确定性大,无法保证各个对象的执行顺序,因此建议永远不要使用该函数,可以使用try-finally机制来替代。

1.5 回收方法区

        永久代垃圾回收的主要内容:废弃常量和无用的类。判定一个常量是否是废弃常量很简单,但判断一个类是否是无用的类,需要满足3个条件:
        a. 该类所有实例已被回收
        b. 加载该类的ClassLoader已被回收

        c. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

        满足以上3个条件,仅说明无用类可以被回收,但也不一定。是否对无用类回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。

        大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP、OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。


2. 垃圾收集算法

2.1 标记-清除算法:
    标记阶段:标记出所有需要回收的对象(再分为两次标记,只有第二次标记,才算标记成功)
    清除阶段:标记完成后统一回收被标记过的对象
    标记-清除是最基础的收集算法,其它算法基本是对该算法的优化。

    缺点是效率不高,空间问题也大,会产生大量的内存碎片。

2.2 复制算法(用于新生代)
    将内存划分为大小相等的2块内存,每次只用一块内存,当这块内存用完了,将还活的移到另一块内存。这种方法效率高,但空间利用率只有50%。

    现有的商业虚拟机对此做了进一步优化,IBM研究表明,新生代98%是朝生夕死,所以不需要1:1来划分内存。而是将内存的一块较大的Eden空间和两块较小的Suvivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还活着的对象一次性地复制到另一块Survivor中,最好清理掉那两块。HotSpot默认Eden:Survivor的比例是8:1,所以整个新生代的空间利用率是90%。但是没办法保证这一定够用,当Survivor不够了,需要依赖老年代进行分配担保。

2.3 标记-整理算法(老年代)
    复制收集算法面对存活率较高的对象,就需要做很多复制工作,效率就会变低。因此这个算法不使用老年代。

    标记-整理算法,标记过程跟标记-清除一样,但整理阶段不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

2.4 分代收集算法

    新生代使用复制收集算法,老年代用标记-清除或者标记-整理算法

   2.5 安全点(SafePoint)
    安全点Safe Point:safepoint 安全点顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定(the thread’s representation of it’s Java machine state is well described),比如记录OopMap的状态,从而确定GC Root的信息,使JVM可以安全的进行一些操作,比如开始GC。
    在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
    实际上,HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint)程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。 
    这些特定的位置主要在: 
    a. 循环的末尾 
    b. 方法临返回前 / 调用方法的call指令后 
    c. 可能抛异常的位置 

    对于Sefepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension),其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
    而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。下面代码清单3-4中的test指令是HotSpot生成的轮询指令,当需要暂停线程时,虚拟机把0x160100的内存页设置为不可读,线程执行到test指令时就会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待,这样一条汇编指令便完成安全点轮询和触发线程中断。

    2.6 安全区域(SafeRegion)
    safepoint只能处理正在运行的线程,它们可以主动运行到safepoint。而一些Sleep或者被blocked的线程不能主动运行到safepoint。这些线程也需要在GC的时候被标记检查,JVM引入了safe region的概念。

    safe region是指一块区域,这块区域中的引用都不会被修改,比如线程被阻塞了,那么它的线程堆栈中的引用是不会被修改的,JVM可以安全地进行标记。线程进入到safe region的时候先标识自己进入了safe region,等它被唤醒准备离开safe region的时候,先检查能否离开,如果GC已经完成,那么可以离开,否则就在safe region呆在。这可以理解,因为如果GC还没完成,那么这些在safe region中的线程也是被stop the world所影响的线程的一部分,如果让他们可以正常执行了,可能会影响标记的结果。

3. 内存分配与回收策略

     3.1 对象优先在Eden区分配
    新生代GC(Minor GC):指发生在新生代的垃圾收集动作。
    老年代GC(Major GC/Full GC):老年代的GC
   

    3.2 大对象直接进入老年代,虚拟机提供了-XX:PretenureSizeThreshold参数,令大于这个参数值得对象直接在老年代分配。

    3.3 长期存活的对象将进入老年代。虚拟机给每个对象定义了一个对象年龄。如果对象在Eden出生并经过第一次Minor GC后仍然存活,将被移动到Survivor区中,并且年龄加1。它的年龄增加到一定程度(默认15岁),将被移动到老年代中。可以通过-XX:MaxTenuringThreshold设置。

    3.4 动态对象年龄判定为了适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须到达限制才能晋升老年代。如果Survivor空间的相同年龄的所有对象之和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

    3.5 空间分配担保。在发生Minor GC之前,检查老年代最大可用的连续空间是否大于所有对象的总空间,如果是,则Minor GC可以确保是安全的。如果不成立,则会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次Minor GC,如果小于,或者不允许担保失败,那这时改为要进行一次full GC。