【转】Java垃圾回收机制

这是在知乎上看到的一个关于Java垃圾回收机制的回答,写得言简意赅,在此收藏;

原文链接:https://www.zhihu.com/question/35164211

Java和C++在内存分配和管理上有什么区别?

Java与C++之间有一堵由动态内存分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。

  • 对于从事C和C++程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权利的皇帝,也是从事最基础工作的劳动人民-----既拥有每一个对象的所有权,又担负着每一个对象从生命开始到终结的维护责任。
  • 对于Java程序员来说,虚拟机的自动内存分配机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,而且不容易出现内存泄露和内存溢出问题,看起来由虚拟机管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权利交给Java虚拟机,一旦出现内存泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误将会是一项异常艰难的工作。并且好的Java程序在编写的时候肯定要考虑GC的问题,怎样定义static对象,怎样new对象效率更高等等问题,简称面向GC的编程。

也可以说Java的内存分配管理是一种托管的方式,托管于JVM。 C++经过编译时直接编译成机器码,而Java是编译成字节码,由JVM解释执行。
C++是编译型语言,而Java兼具编译型和解释型语言的特点。

Java虚拟机规范将JVM虚拟机所管理的内存分为几部分?

  1. 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行字节码的行号指示器。是线程私有,生命周期与线程相同。
  2. Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。 Java虚拟机栈描述的是Java方法(区别于native的本地方法)执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动作链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  3. 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机所使用到的Native方法服务。
  4. 方法区 线程共享的内存区域:存储虚拟机加载的类的信息,比如版本,方法描述,字段描述,静态变量和常量;还有运行时常量池:用于存储编译好的常量和运行时常量;

有哪些方法可以判断一个对象已经可以被回收,JVM怎么判断一个对象已经消亡可以被回收?

  1. 引用计数算法
    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。Java语言没有选用引用计数法来管理内存,因为引用计数法不能很好的解决循环引用的问题。
  2. 根搜索算法
    在主流的商用语言中,都是使用根搜索算法来判定对象是否存活的。
    GC Root Tracing 算法思路就是通过一系列的名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,即从GC Roots到这个对象不可达,则证明此对象是不可用的。
    比如上图,左边的对象都是存活的,右边的都是可以回收的。

那些对象可以作为GC Roots?

  1. 虚拟机栈(栈帧中的本地变量表)中的引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中的常量引用的对象
  4. 本地方法栈中JNI(Native方法)的引用对象

HotSpot JVM (下简称JVM)的内存管理

JVM将堆分成了 二个大区 Young 和 Old 如下图:

而Young 区又分为 Eden、Servivor1、Servivor2, 两个Survivor 区相对地作为为From 和 To 逻辑区域, 当Servivor1作为 From 时 , Servivor2 就作为 To, 反之亦然关于为什么要这样区分Young(将Young区分为Eden、Servivor1、Servivor2以及相对的From和To ),这要牵涉到JVM的垃圾回收算法的讨论。

1)因为引用计数法无法解决循环引用问题,JVM并没有采用这种算法来判断对象是否存活。
2)JVM一般采用GCRoots的方法,只要从任何一个GCRoots的对象可达,就是不被回收的对象
3)判断了对象生死,怎么进行内存的清理呢?
4)标记-清除算法,先标记那些要被回收的对象,然后进行清理,简单可行,但是①标记清除效率低,因为要一个一个标记和清除②造成大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象的时候无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
5)采用复制收集算法:将可用内存按照容量分为大小相等的两块,每次只是使用其中的一块。当这一块的内存用完了,就将可用内存中存活着的对象复制到另一块上面,然后再把已使用过的内存一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序非配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半。复制算法的执行过程如下图所示:

上面的过程就解释了为什么我们的Yong内存需要分为三块,Eden,Survivor1,Survivor2以及FROM TO相对使用的用法。

因此当Eden区满的时候 GC执行,这时会将 Eden 区和 From 区中还被引用的对象会被移到 To区 ,个别大对象和部分From对象在To已满的情况下会被放到Old区,如下图:

垃圾回收算法

  • 引用计数:一个对象被引用计数器加一,取消引用计数器减一,引用计数器为0才能被回收。优点:简单。缺点:不能解决循环引用的问题,比如A引用B,B引用A,但是这两个对象没有被其他任何对象引用,属于垃圾对象,却不能回收;每次引用都会附件一个加减法,影响性能。
  • 标记清除法:分为两个阶段:标记阶段和清除阶段。标记阶段通过根节点标记所有可达对象,清除阶段清除所有不可达对象。缺点:因为清除不可达对象之后剩余的内存不连续,会产生大量内存碎片,不利于大对象的分配。
  • 复制算法:将内存空间分成相同的两块,每次只是用其中的一块,垃圾回收时,将正在使用的内存中的存活对象复制到另外一块空间,然后清除正在使用的内存空间中的所有对象,这种回收算法适用于新生代垃圾回收。优点:垃圾回收对象比较多时需要复制的对象恨少,性能较好;不会存在内存碎片。缺点:将系统内存折半。
  • 标记压缩算法:是一种老年代回收算法,在标记清除的基础上做了一些优化,首先从根节点开始标记所有不可达的对象,然后将所有可达的对象移动到内存的一端,最后清除所有不可达的对象。优点:不用将内存分为两块;不会产生内存碎片。
  • 分代算法:新生代使用复制算法,老生带使用标记清除算法或者标记压缩算法。几乎所有的垃圾回收期都区分新生代和老生带。
  • 分区算法:将整个堆空间分成很多个连续的不同的小空间,每个小空间独立使用,独立回收。为了更好的控制gc停顿时间,可以根据目标停顿时间合理地回收若干个小区间,而不是整个堆空间,从而减少gc停顿时间。