深入理解Java虚拟机——垃圾收集器与内存分配策略

在Java堆里面存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。

下面3点参考:https://www.cnblogs.com/parryyang/p/5748711.html
整个JVM内存总共划分为三代:新生代(Young Generation)、老年代(Old Generation)、持久代(Permanent Generation)

1、年轻代:所有新生成的对象首先都放在年轻代内存中。年轻代的目标就是尽可能快速的手机掉那些生命周期短的对象。年轻代内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior.当回收时,将Eden和Survior中还存活的对象一次性拷贝到另外一块Survior空间上,最后清理Eden和刚才用过的Survior空间。
2、年老代:在年轻代经历了N次GC后,仍然存活的对象,就会被放在老年代中。因此可以认为老年代存放的都是一些生命周期较长的对象。
3、持久代:基本固定不变,用于存放静态文件,例如Java类和方法。持久代对GC没有显著的影响。持久代可以通过-XX:MaxPermSize=<N>进行设置。

GC什么时候开始进行?

此处整理自https://blog.csdn.net/u013309822/article/details/80346913
GC经常发生的区域是堆区,堆区还可以分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区。
对象有限在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。
Full GC,发生在老年代的GC,当老年代没有足够空间时即发生Full GC,发生 Full GC一般都会有一次Minor GC。
大对象直接进入老年代,如果很长的字符串数组,虚拟机提供一个 XX:PretenureSizeThreadhold参数,令大于这个参数值的对象直接在老年代分配,避免Eden区和连个Survivor区发生大量的内存拷贝。
发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则进行一次Full GC,如果小于,则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那只会进行一次 Minor GC,如果不允许,则改为进行一次 Full GC。
后面讲到内存分配的时候会再次讲解

如何判断对象死去?可以被GC?

1.引用计数法:给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1,;引用失效时,计数器值就减1,;任何时刻计数器为0的对象就是不可能再被使用的。(判定效率高,实现也很简单,但是主流的Java虚拟机里没有选用引用计数法来管理内存,最主要的原因是它很难解决对象之间相互循环引用的问题)

public class People {

    public Object instance = null;
    
}

public class Test {

    public static void main(String[] args) {
        Person person1 = new Person();
        Person person2 = new Person();
        //相互引用
        person1.instance = person2;
        person2.instance = person1;

        person1 = null;
        person2 = null;
        System.gc();
    }

}

如果使用引用计数法的话,这里的person1和person2将不能被回收。


2.可达性分析算法:通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象GC Roots没有任何引用链相连时,则证明此对象时不可用的。
在Java中,可作为GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。JDK1.2以后,Java将引用分为强引用、软引用、弱引用、虚引用,强度以此递减。
深入理解Java虚拟机——垃圾收集器与内存分配策略
图片来自:https://www.cnblogs.com/zhiqianye/p/6204110.html

 

垃圾收集算法

图片来自https://www.cnblogs.com/parryyang/p/5748711.html
标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。(最基础的收集算法,标记和清除的效率都不高,会产生大量不连续的内存碎片)
深入理解Java虚拟机——垃圾收集器与内存分配策略
先介绍以下finalize()方法:

protected void finalize() throws Throwable { }

它是Object类的一个protected方法,子类可以覆盖该方法以是实现资源的清理工作,GC在回收对象之前会调用该方法
标记过程:要宣告一个对象死亡,至少要经历两次标记过程,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,这两种情况都视为“没有必要执行”,对象直接被回收。如果这个对象有必要执行finalize()方法,那么这个对象将会放置在一个叫F-Queue的队列中,并在稍后由虚拟机自动建立、优先级低的Finalizer线程去执行它,finalize()方法是对象逃脱死亡的最后一个机会,稍后GC将对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,就能逃脱死亡(下次GC的时候由于已经执行过一次finalize方法,就没有这个机会了,直接GC回收),如果没有,这个对象就会被GC回收了。

复制算法:(对象存活率不高的情况下,解决了效率问题)它将可用内存按容量划分为大小相等的两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,缺点时将内存缩小为原来的一半,代价太高。
深入理解Java虚拟机——垃圾收集器与内存分配策略
标记-整理算法:与标记-清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理端边界的内存。
深入理解Java虚拟机——垃圾收集器与内存分配策略
分代收集算法:根据对象存活周期的不同将内存分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法进行回收。

理解 枚举根节点、安全点、安全区域,为垃圾收集器做铺垫

枚举根节点:
可作为GC Roots的节点主要分布在全局性的引用(例如常量和或类静态变量)和执行上下文(例如栈帧中的本地变量表)中,在进行可达性分析时,不可以出现在分析过程中,对象引用关系还在不断地变化这样的情况,所以GC进行时,必须停顿所有Java执行线程,枚举根节点时也是必须要停顿的。
安全点:
安全点是在程序执行期间的所有GC Roots已知并且所有堆对象的内容一致的点,当线程运行到安全点时,JVM可以安全地进行操作。在HotSpot中,安全点的位置主要在:1.方法返回之前;2.调用某个方法之后;3.抛出异常的位置;4.循环的末尾。为什么把这些位置she设置为安全点呢?主要目的是避免程序长时间无法进入safepoint,JVM在做GC之前要等所有的应用线程进入到安全点后VM线程才能分派GC任务,如果有线程一直没有进入安全点,就会导致GC时JVM停顿时间延长。
VM参数:-XX:+PrintGCApplicationStoppedTime 可以打印出系统停顿的时间,如果存在停顿时间特别长的情况,大概率原因是当发生GC时,有线程迟迟进入不到安全点,导致其他已经在安全点停止的线程也一直等待,这里需要分析业务代码中是否存在有界的大循环逻辑,可能在JIT优化时,这些循环操作没有插入safepoint检查。(JIT编译器:即时编译器)

Total time for which application threads were stopped: 0.0000638 seconds, Stopping threads took: 0.0000279 seconds
Total time for which application threads were stopped: 0.0000619 seconds, Stopping threads took: 0.0000261 seconds
Total time for which application threads were stopped: 0.0000804 seconds, Stopping threads took: 0.0000189 seconds
Total time for which application threads were stopped: 0.0000706 seconds, Stopping threads took: 0.0000185 seconds
Total time for which application threads were stopped: 0.0002907 seconds, Stopping threads took: 0.0001170 seconds
Total time for which application threads were stopped: 0.0001155 seconds, Stopping threads took: 0.0000238 seconds
Total time for which application threads were stopped: 0.0001465 seconds, Stopping threads took: 0.0000506 seconds
Total time for which application threads were stopped: 0.0000910 seconds, Stopping threads took: 0.0000449 seconds
Total time for which application threads were stopped: 0.0076313 seconds, Stopping threads took: 0.0000287 seconds

安全区域:
如果GC时,某个线程处于不执行状态呢?比如线程处于sleep状态或者blocked状态,这时候线程无法响应JVM的中断请求,走到安全的地方去挂起,对于这种情况就需要“安全区域”来解决。安全区域可以看成扩展了的安全点,在这个区域中任何地方GC都是安全的。当线程执行到安全区域中的代码时,首先标识自己进入到了安全区域,在这段时间里,JVM要发起GC时,根据这个标识,就不用管这部分线程了,当线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程),如果完成了,线程继续执行,否则它就必须等待,直到收到可以安全地离开安全区域的信号为止。
 

发生GC时,如何中断线程?

抢先式中断:在GC发生时,首先把所有线程全部中断,如果线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上(现在几乎没有虚拟机采用抢先式中断来暂停线程从而相应GC事件)
主动式中断:GC需要中断线程的时候,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志位true时就自己中孤单挂起,轮询标志的地方和安全点是重合的(不会一直去轮询),另外加上创建对象需要分配内存的地方也会有轮询。

 

垃圾收集器

1.Serial收集器

Serial收集器是最基本、最早的收集器,JDK1.3.1之前是虚拟机新生代收集器的唯一选择。这个收集器是一个单线程的收集器,它只会使用一个CPU、一条收集线程去完成垃圾收集工作,并且在垃圾收集过程中,必须暂停其他所有工作线程,直到它收集结束。直到现在,它依然是虚拟机运行在Client模式下的默认新生代收集器,优点:简单、高效,对于限定单个CPU的环境,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

2.ParNew收集器

其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为完全一样,它是许多运行在Server模式下的虚拟机中首选的新生代收集器,除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。ParNew收集器在单CPU的环境下绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术(一个物理CPU分成两个逻辑CPU,模拟双核心)实现的两个CPU环境中都不能百分之百地保证超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,可以使用 -XX:ParallelGCThreads参数来限制垃圾收集的线程数。

3.Parallel Scavenge收集器

新生代收集器,使用复制算法,并行的多线程收集器。Parallel Scavenge收集器关注的不是尽可能缩短用户线程的GC停顿时间,而是达到一个可控制的吞吐量,吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间),如果虚拟机总共运行了100分钟,其中垃圾收集1分钟,那么吞吐量就是99%。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数。
MaxGCPauseMills允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值,不过需要注意的是:不是如果把这个参数设置得小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换去的(例:系统把新生代调小一点,收集300MB新生代肯定比收集500MB快吧,这也直接导致了垃圾收集发生得更频繁一些,原来10s收集一次,每次停顿100ms,现在变成5s收集一次,每次停顿70ms,停顿时间确实下降了,但是吞吐量也降下来了)。
GCTimeRatio参数的值是一个 >0 , <100的整数,也就是垃圾收集时间占总时间的比率。

未完待续。。。。

 

 

 

 

 

 

博客内容整理来自《深入理解Java虚拟机 JVM高级特性与最佳实践》