浅析Java垃圾回收机制

简介

        在Java中,程序员不必像C++程序员那样需要自己手动的去释放一个对象的内存,而是由虚拟机自行执行。因为呀,Java开发人员认为:内存处理是编程人员最容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC(垃圾回收) 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。

值得注意的是,Java 语言并没有提供释放已分配内存的显式操作方法,全看虚拟机的心情。

如何实现GC的呢?

原因是在JVM中有一个低优先级的垃圾回收线程,在正常情况下是不会执行的,只有当虚拟机空闲或当前堆内存空间不足时,才会触发执行,执行的方式是:扫描那些没有任何引用的对象,并将它们都添加到要回收的集合中,进行回收。

需要我们注意的首先是条件:

        - 虚拟机空闲

        - 当前堆内存不足

这两个条件是或的关系,也就是说只满足一个条件就可以执行。而它执行的对象则是那些没有任何引用的对象,也就是说,在程序中不能通过任一条语句跳转并使用该对象。那么它对于程序来说就是死对象,对于虚拟机来说就是垃圾。

GC的优点

它是Java语言最显著的特点,使得Java程序员在编写程序时不再考虑内存管理的问题。由于GC机制,Java中的对象不再有“作用域”这个概念,只有引用的对象才有“作用域”。

垃圾回收机制有效的防止了内存泄漏,可以有效地使用可使用的内存。

垃圾回收器通常作为一个单独的低优先级的线程运行,在不可预知的情况下对内存堆中已经死亡或很久没有用过的对象进行清除和回收。(来骗,来偷袭)

浅析Java垃圾回收机制_第1张图片

 

GC的基本原理

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小及使用情况。通常,GC采用有向图的形式记录和管理堆(heap)中的所有对象。通过该方式来确定哪些对象是“可达的”,哪些对象是“不可达的”。

当GC确认某些对象是“不可达”时,也就是上文所说的没有任何引用的对象,GC就有责任回收这些对象所对应的内存空间。此处就要先讲一下Java是如何判断对象是否可以回收的:

判断对象是否可以回收的方法

一般有两种方法进行判断:

        - 引用计数法:给每个已创建的对象增加一个引用计数的属性,有引用该对象时,计数器++;

                                引用被释放时,计数器- -;当计数器为0时,说明该对象可以回收。

                                缺点:不能解决循环引用的问题;

        - 可达性分析算法:从 GC Roots 开始向下搜索,搜索过程中走过的路径被称为“引用链”。

                                       当一个对象到 GC Roots没有任何一个引用链与之相连时,证明该对象

                                       可以回收。

这时屏幕前的彦祖们又有一个新的疑问了:这么牛的功能,那我能不能为了优化系统占用,在我需要时让它马上执行,或者是主动通知虚拟机进行垃圾回收?

当然可以,程序员可以手动执行System.gc(),通知GC运行。但是,Java语言规范并不保证GC一定执行

上文一直在说垃圾回收要先找到可以回收的对象,那么对于收集垃圾回收对象有没有对其进行优化以提高效率的方式呢?

当然是有的,提高效率最有效的方式就是算法!GC也不例外。

垃圾回收 / 收集算法

截至目前,JVM共采用5种算法来对GC机制进行优化。它们各有优劣,各适用于不同的场景及对象。在介绍之前,为避免大家看完这部分之后还是有些懵,不了解分代垃圾回收中所说的“新生代”、“老年代”、“永久代”的概念。在此先说明一下垃圾回收机制的分区:

分代垃圾回收分区

分代回收器有两个分区:老年代和新生代,新生代默认的空间占总空间的1/3,老年代的默认占比是 2/3。

新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  1. 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  2. 清空 Eden 和 From Survivor 分区;
  3. From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老年代。

大对象也会直接进入老年代。

老年代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

介绍完毕,让我们回到算法的内容:

标记-清除算法:标记无用对象,然后进行清除回收。

缺点:效率不高,无法清除垃圾碎片。

 

复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

缺点:内存使用率不高,只有原来的一半。

 

标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

优点:解决了标记-清理算法存在的内存碎片问题。

缺点:仍需要进行局部对象移动,一定程度上降低了效率。

 

分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

 

分代收集算法

当前虚拟机采用的方法

分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代

为什么这么做?

比如新生代中,每次收集都有大量的对象死亡,所以选择“标记-复制”的算法,只需要付出少量的复制成本就可以完成每次的垃圾回收;

而老年代的对象存活时间高,可以选择“标记-清除”或者是“标记-整理”算法进行垃圾回收。

介绍完垃圾回收算法后,想必大家已经对其回收过程有了一个基本了解。如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾回收器

用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。

Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互响应要求不高的场景;

Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,

Parallel Scavenge收集器的老年代版本;

CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

小结

新生代回收器:Serial、ParNew、Parallel Scavenge

老年代回收器:Serial Old、Parallel Old、CMS

整堆回收器:G1

 

新生代垃圾回收器一般采用的是复制算法

优点是效率高,缺点是内存利用率低;

 

老年代回收器一般采用的是标记-整理算法进行垃圾回收。

大家都知道,除了新生代和老年代的概念外,JVM还有一个“永久代”的概念。那永久代会发生垃圾回收吗?

答案是不会。垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。

如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

(Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

你可能感兴趣的:(java,jvm,开发语言)