java虚拟机:一篇让你了解垃圾收集策略和垃圾收集核心知识

什么是垃圾收集?

在我们所看到的很多讲解JVM书籍知识的文章中,总会看到如下一句话:java和C++之间有一堵由内存动态分配和垃圾收集技术围成的“高墙”,墙外的人想进来,墙里的人想出去。感觉这句话挺有意思的,使用C++的程序员在管理内存方便有着至高无上的权利,内存的分配和释放都是由程序员直接控制的,如果分配了内存,但是忘记释放了,就会很容易出现内存溢出的问题,C++程序员饱受其折磨。而使用Java的程序员呢,就不太关注对象的分配和释放问题了,但是呢,当出现内存溢出后,很难定位问题,这时候Java程序员就会想到我们能直接和内存打交道就好了,不然依赖Jvm进行内存分配,我都不知道其具体做了什么,是怎样做的。

话不多说,在讲解下面只是之前,大家思考几个问题:

  • 哪些内存需要回收?
    需要回收的内存,肯定是我们认为其被垃圾所占有着,我们就可以进行回收了,但是呢,如何认为哪些是垃圾呢?如果不小心回收了不是垃圾的内存,可能会导致程序的奔溃的。正如我们生活中,也会产生垃圾,比如我就不太喜欢打扫房间,屋里有很多无用的东西堆放着,占用了有限空间的很多地方,此时我买了一个东西,但是我发现房间里没有地方放了,此时我就火急火燎的将一些我认为是垃圾的东西全扔掉,但是坑爹了,我不小心将我的社保卡也扔了,完蛋了!!!!所以呢,清理垃圾吗,一定要标记好,且一定要定时清理,不然垃圾多了,不仅标记起来困难,而且清理时间长,导致清理的那段时间我啥事做不了,本来很急的事只能干等着了。。。
  • 什么时候回收?
    这个不用多说了,你感觉垃圾所占用的空间已经影响到你的生活了,那肯定就得回收了。想Java中的Eden区快满了,就会进行Young GC操作。而Old区快满了,就会触发Full GC操作。
  • 如何回收?
    还是拿生活举例:
    方法一:我早上起来看见家里垃圾挺多的,我就在出门时,直接将垃圾拿走了(此时屋里还会挺乱的,因为不是垃圾的东西分布的很散)。对应于标记-清除算法,标记垃圾,清理垃圾。
    方法二:我早上起来看见家里的基本一大半全都是垃圾(注:生活是多么不卫生呀)。我就直接将不是垃圾的东西拿出来,让后放到一个角落里,然后大刀阔虎将其余空间的东西全都给扔掉。对应于标记-整理算法,标记不是垃圾的东西
    方法三:我现在租的房子大嘛,所以我任性点,我使用房子的时候,只使用一半区域,当这半区域很乱后,我就把这半区域有用的东西移到另一半空闲的区域,然后将原来的一半区域全都清掉。对应于复制算法,将不是垃圾的东西复制到另一个空闲区域。

标记对象已死的两种策略

引用计数算法

这个算法比较好理解,对于每一个对象,都会有一个计数器与之关联,每当有一个新的引用过来,计数器就会加1,目前这种算法在很多地方都有用到,但是在Java中并不是采用这种算法,而是采用可达性分析算法。为啥不采用引用计数法呢?书中是这样说的:“很难解决对象之间相互循环引用的问题”,“很难”表示还是有办法的,但是呢,解决起来很困难,哈哈哈,既然这样,咱们就去了解下可达性分析算法吧。

可达性分析算法

可达性分析法就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点搜索,当一个对象没有和任何一个GC Roots相连时,则证明此对象是不可用的。所以呢,一定要知道哪些是GC Roots,在Java中,作为GC Roots的对象包括如下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象;
    对于上面所说的几种作为GC Roots的对象,仔细思考下也很好理解啦。比如第一种,既然在虚拟机栈中,那么在一个栈帧中对应的方法调用肯定要用到这个对象了,我还要用,你如果给我回收了,那还有没有天理啦?第二种和第三种呢,静态变量是和Class对象关联的,Class对象还在呢,你总不能把静态变量关联的对象回收掉吧?对于常量呢,就放到常量池里啦,对于每个Class对象,也就独此一份,作为GC Roots也不为过吧?第四种和第一种类似,只是对应于Native方法而已。

“引用”这个词大家都很很懂吧?对于一个对象,我们要使用它,当然要引用它了,而判断一个对象是否存活也和“引用”有关,之前对于一个对象,要么引用它,要么不引用它,但是当出现垃圾回收时,如果引用只是是和否的关系,很影响垃圾动态回收的灵活性,所以在Java中对引用又进一步细粒度的划分了,分为如下四种:

  1. 强引用(Strong Reference):通过关键字new进行的引用就是强引用,只要强引用存在,垃圾收集器就永远不会收集被引用的对象;
  2. 软引用(Soft Reference):当在内存溢出之前,会进行第二次垃圾回收,把这些软引用的对象作为第二次垃圾回收的目标范围之中。
  3. 弱引用(Weak Reference):被引用关联的对象只能存活到下一次垃圾收集发生之前。
  4. 虚引用(Phantom Reference):一个对象是否有虚引用的存在,完全不会影响其垃圾收集,设置虚引用的唯一目的是当这个对象被垃圾收集时会收到一个系统通知。

下面是我自己写的一些测试用例,通过配置-Xmx -Xmn -Xms等参数,观察垃圾收集的情况,会不会将对应的软引用、弱引用等对象进行回收。

import java.lang.ref.PhantomReference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

/**
 * @Author: jiangcw
 * @Version 1.0
 */
public class Test {

    private Test reference;

    private byte[] b = new byte[1024 * 1024];

    private SoftReference<byte[]> softReference;

    private WeakReference<byte[]> weakReference;

    private PhantomReference<byte[]> phantomReference;

    private  final  int x = 2;

    public static void main(String[] args) throws Exception {
        Thread.sleep(60000);
        Test testA = new Test();
        testA.softReference = new SoftReference<>(new byte[1024 * 1024]);
        testA.weakReference = new WeakReference<>(new byte[1024 * 1024]);
        testA.phantomReference = new PhantomReference<>(new byte[1024 * 1024], null);
        System.gc();
        byte[] b = new byte[1024 * 1024 * 4];
        Test testB = new Test();
        testA.reference = testB;
        testB.reference = testA;
        System.out.println(testA.softReference.get() == null);
        System.out.println(testA.weakReference.get() == null);
        System.out.println(testA.phantomReference.get() == null);
        testA = null;
        testB = null;

        System.gc();
    }
}

备注:对象进行可达性分析后,如果发现一个对象A不可达,并不会立刻进行回收,而是可以通过覆盖finalize()方法进行自我拯救,如果一个对象覆盖了finalize方法且一次没有执行过,就会将对象A放到一个F-Queue队列中,通过Finalizer线程自动执行finalize()方法,如果执行完后,又和GC Roots建立了联系,则对象A就没必要进行回收了。
方法区也会进行内存回收的,对于无用的常量和无用的类。如果没有任务地方使用常量,就可以进行回收。对于回收类比较苛刻,如果一个类的所以对象都被回收且加载该类的ClassLoader已被回收且该类对应的Class对象没有在任务地方引用,无法在任何地方通过反射访问该类的方法,则此类可以被回收。

垃圾收集算法

  1. 标记-清除算法
    容易产生内存碎片,标记和清除过程都比较慢
  2. 复制算法
    会浪费一半的内存空间,但速度比较快
  3. 标记-整理算法
    复制操作导致效率比较低,但是呢,不会浪费额外的空间
  4. 分代算法
    根据对象存活周期将内存分块,针对不同的快,使用不同的垃圾收集算法

备注:GC Roots的枚举是快速的,是通过一组称为OopMap的数据结构来达到此目的,会存在一个对象的确定的偏移量的位置是什么类型的数据。
线程只有进入安全点(SafePoint)或者安全区域(SafeRegion)才能真正进行垃圾回收。

垃圾收集器

下图是新生代和老年代垃圾收集器的搭配策略,并不是任务两种垃圾收集器都是可以搭配的。
generation
1、Serial收集器(新生代收集器)
单线程收集器,进行垃圾收集时需要暂停所有的工作线程,作用于年轻代的垃圾收集,采用复制算法
2、ParNew收集器(新生代收集器)
是Serial收集器的多线程版本,除了采用多条线程进行垃圾收集之外,其余的行为和Serial收集器一模一样,采用复制算法
3、Parallel Scanvange收集器(新生代收集器)
(1)达到可控制的吞吐量,即时CG的总时间尽可能小,吞吐量=运行用户代码时间/(运行代码时间+垃圾收集时间)
(2)有自适应调节策略,可以配置-XX:+UseAdaptiveSizePolicy参数,其他细节缠住不用配置,可以根据性能监控参数动态调整
采用复制算法
4、Serial Old收集器(与PS MarkSweep实现非常类似,可以认为一样)
(1)是Serial收集器的老年代版本,也是一个单线程收集器
(2)可以与Parallel Scavenge配合使用,同时可以作为CMS收集器的后备预案
采用标记整理算法
5、Parallel Old收集器
(1)是Parallel Scanvenge收集器的老年代版本
(2)只能和Parallel Scanvenge配合使用
采用标记整理算法
6、CMS收集器(Concurrent Mark Sweep)
(1)以获取最短回收停顿时间为目标的收集器
(2)基于标记-清除算法实现的
(3)四个主要步骤:初始标记、并发标记、重新标记、并发清除
初始标记和重新标记阶段需要“Stop the world”,初始标记:标记一下GC Roots能直接关联到的对象;并发标记:进行GC Roots Tracing过程;重新标记:为了修正并发标记阶段用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;
(4)三个主要缺点:CMS收集器对CPU资源非常敏感;无法处理浮动垃圾(标记过程之后产生的垃圾);因为是基于标记-清除算法,以产生大量空间碎片
采用标记清除算法
7、G1收集器
(1)主要特点:并行和并发、分代收集、空间整合、可预测的停顿
(2)主要步骤:初始标记、并发标记、最终标记、筛选回收

内存分配策略和回收策略

1、对象优先在Eden分配
都Eden区剩余的连续空间无法支持新对象的分配时,就触发Minor GC
2、大对象直接进入老年代
配置-XX:PretenureSizeThreshold参数,控制当对象的大小大于多少时,可以令大于这个值的对象直接进入老年代,主要是为了防止Eden去和两个Survior区之间进行大量的内存复制。
3、内存分配和回收过程
初始化是,首先对象先进入Eden区,当Eden区满后,将对象复制到S0区,此时Eden区为空,对象再次过来时进入Eden区,当Eden区满后,将Eden区和S0区存活的对象复制到S1,区,同时交换S1和S0的角色。当复制过程S1区满后,过早将一些对象复制到老年代,当老年代也满后,触发Full GC。
4、长期存活的对象进入老年代
通过参数-XX:MaxTenuringThreshold来设置对象熬过来了几次Minor GC后直接进入老年代
5、空间分配担保
就是通过老年代进行空间担保,老年代存放的并不一定是长期存活的对象,有可能由于过早提升导致一些对象进入老年代。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值