java垃圾回收浅析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kswkly/article/details/79705459

垃圾回收(Garbage Collection),乍一看,垃圾收集应该处理顾名思义 - 查找并扔掉垃圾。事实上,它恰恰相反。垃圾收集正在追踪所有仍在使用的对象,并将其余标记为垃圾,然后释放垃圾占用的内存,当然了,java有垃圾自动回收机制,所以我们基本上不需要去关心内存分配和垃圾回收的问题,不过了解一下还是挺好的。
一、什么是垃圾?
如果一个对象没有任何引用与之关联那么它就会被认定为是垃圾。那如何确定一个对象有没有被引用呢?我们有两种方法:
1、引用计数法,给对象添加一个引用计数器,当它被引用时,计数器加一,当引用失效时计数器减一,当任何时候计数器为都0时,判断该对象为垃圾。这种方法简单易懂,但是缺点也很明显,就是不能解决循环引用,例如:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.object = object2;
        object2.object = object1;

        object1 = null;
        object2 = null;
    }
}

class MyObject{
    public Object object = null;
}

上面的代码中object1和object2相互引用,形成了一个环路,即使两者都是null,但由于它们的引用数不为0 ,所以它们不会被回收。java并没有采用这种方式,不过Python用的是,不知道它具体是怎么解决循环引用的。java用的是下面这种。
2、根搜索(GC Root Tracing),它通过一系列的“GC Roots”对象作为起点进行搜索,搜索所走过的路径叫做引用链,若果一个对象没有任何路径到达GC Roots,那么就说这个对象是不可达的,不够即使是不可达的对象也不能马上就判断其为垃圾,至少要经过过两次标记,且两次都不可达才能将其判断为垃圾。这其中还有一个问题就是那些对象可以作为“GC Roots”,如下:

  • 虚拟机栈(里面的本地变量表)中的引用的对象
  • 方法区中的类的静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)的引用的对象
    现在垃圾找到了,那么如何回收呢?主要有以下几种方法
    1、标记删除(Mark-Sweep)
    步骤很简单,首先标记出所有需要被回收的对象,然后删除它们,就是回收这些对象所占的内存。虽然简单易行,效率也高,但是它的缺点也很明显:容易产生内存碎片。如下图:
    标记删除
    2、标记复制
    标记复制的原理也很简单,首先它将内存分为大小相同的两部分,每次使用其中的一部分,当这一部分内存用完时,就将其中还存活着的对象复制到另一块内存上,然后将这部分内存清空。这样一来就解决了标记删除所产生的内存碎片问题,但是它的缺点也很明显:能使用的内存只有一半。如下图:
    这里写图片描述
    3、标记整理(Mark-Compact)
    标记整理能够解决前面两种算法的缺点,它在标记清理出垃圾对象后,会把所有活着的对象都向内存一段移动,并更新引用其对象的指针,这样一来就不会产生内存碎片了,但是由于每次都要对或者的对象进行移动,所以它的效率比“标记清理”要低。
    这里写图片描述
    4、分代收集(Generational Collection)
    java所采用的一种很经典的垃圾回收算法,最新的JDK9完全已经完全删除了它,而采用G1算法。为什么会提出分代收集呢?因为研究人员观察到,应用程序内的大部分对象分为两类:一类是很快不被使用的;另一类是会存活一段时间的。如下图:
    这里写图片描述
    所以分代收集的主要思想就是:根据对象生命周期的长短把内存分为若干个不同的区域。如下图:
    这里写图片描述
    其中新生代存放刚刚创建和那些存活时间比较短的对象,老年代存放存活时间较长的对象。新生代又分为Eden、survivor1、survivor2三个区,两个servivor互为From和To逻辑区域,当其中一个为From时另一个就为To,这些区的大小也是不一样的,Eden更大,因为IBM的专门研究表明新生代中的对象98%都是很快就会死的。当进行回收时将Eden和From survivor中还存活着的对象拷贝到另一块To survivor中,并清空Eden和From survivor,清空后把From变成To,To变成From。如果在拷贝的过程中To survivor满了,会将其存在老年代中。如下图:
    这里写图片描述
    在JDK8之前,还有一块区域叫做permgen(永久代)的特殊空间,它是用来存放类的元数据的,但是它实际上用于给Java开发人员带来很多麻烦,因为很难预测所有需要的空间。由于预测元数据是一项非常麻烦的工作,因此在Java 8中移除了永久代以支持Metaspace。它位于本机内存中,不会干扰常规堆对象。默认情况下,Metaspace大小仅受Java进程可用的本机内存量的限制。当然你也可以自行设置。
    几个不同的GC
    Minor GC:就是对年轻代的清理,发生频率高,该过程会让应用程序的线程暂停,如下图:
    这里写图片描述
    Major GC:就是对老年代的清理,频率低,不会让应用程序的线程暂停
    Full GC:就是对整个堆的清理,包括年轻代和年老代,如下图:
    这里写图片描述
    不过不必在意这些称呼,你应该关注的是GC是否停止了所有的应用程序线程,或者是否能够与应用程序线程同时进行。
    java垃圾收集器的演变历程
    1、串行收集(Serial)
    在JDK的早期版本中,JVM仅能使用serial收集器,它的特点就是:它只会使用一个CPU或者一个线程去进行垃圾回收,而且当进行垃圾回收时,其他线程必需停止,直到它完成垃圾回收后才能运行。
    2、并行收集(parallel)
    顾名思义,并行收集就是能采用多个线程进行垃圾回收,这样可以充分利用CPU多核的特性,提高垃圾收集的效率,降低垃圾收集的时间。
    3、CMS收集
    CMS在Minor GC(就是对新生代进行垃圾回收)时会暂停所有应用线程,并采用多线程的方式进行垃圾回收。在Full GC时(就是对老年代进行垃圾回收)不会暂停其他应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。
    4、G1收集
    garbage first垃圾优先收集,意思就是首先收集包含垃圾数最多的区域。G1的关键设计目标之一是通过垃圾收集来预测和配置垃圾回收时应用程序线程暂停持续的时间。事实上,Garbage-First是一个软实时垃圾收集器,这意味着您可以为其设置特定的性能目标。您可以在任何给定的y毫秒时间范围内请求停止stop-the-world(垃圾回收时应用程序线程暂停持续的时间)不超过x毫秒。为了实现这一点,G1采用了新的堆划分方式。首先,堆不必分成连续的年轻一代和老一代。相反,堆被拆分成一个个可容纳对象的小堆区域,通常有2048个。每个区域可能是Eden,也可能是survivor或者old。所有Eden和survivor区都叫做年轻代,所有old取都是老年代,如下图:
    这里写图片描述
    这样做的好处就是避免一次收集整个堆,而是只收集那些有垃圾最多的区域(G1会在并发阶段估计每个区域包含的数据量,进而确定哪些区域垃圾多,需要收集)。
    其实在G1中,还有一种特殊的区域,是用来存放巨型对象的(占用的空间超过了分区容量50%以上)。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
    G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。
    1、young GC
    当Eden区被填满时,应用程序线程停止,将Eden区中还活着的对象复制到survivor区,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
    2、mix GC
    在进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

猜你喜欢

转载自blog.csdn.net/kswkly/article/details/79705459