深入理解java虚拟机之自动内存管理机制(二)

垃圾收集算法

    java中的内存是交给虚拟机管理的。要实现垃圾回收,必须考虑如下三个问题:

    1. 哪些内存需要回收?

    2. 什么时候回收?

    3. 怎么回收?

    对于第一点,往大了来说,是堆和方法区的内存需要回收。往具体了来说,是堆中哪些对象的内存可以回收了?方法区中哪些类的信息的内存可以回收了?要解答这两点问题,必须要有算法能够判断
  哪些对象已“死”,哪些类的信息不再需要。

    对于第二点,则要在性能与效率中做好兼顾,不能过于频繁影响程序性能,也不能太少让收集器太闲,积累太多垃圾。

    对于第三点,则要求我们想出算法,实现收集器。然而,一般来说,需要根据不同类型对象的特点使用不同的收集算法,比较通用的分类方法是将对象分为新生代与老年代,不同的代使用不同的算法
  可以有效提升收集效率。

(一)对象已死吗?

  判断对象是否已经死亡,有两种方法。

  一、引用计数算法

      给对象添加一个引用计数器,每当有一个地方引用它时,计数器就+1,引用失效就-1。该方法简单高效,但是解决不了对象之间相互循环引用的问题。

  二、可达性算法分析

      该算法是java虚拟机主流算法。基本思路是,通过一系列的称为GC Roots的对象作为起始节点,然后通过这些节点向下搜索,走过的路径称为引用链,如果某个对象到GC Roots没有引用链,

    就可以说该对象已死。

      那么哪些对象可以作为GC Roots呢?

      1. 虚拟机栈中引用的对象

      2. 方法区中类静态属性引用的对象

      3. 方法区中常量引用的对象

      4. Native方法中引用的对象

  三、再谈引用

    引用的意思就是这块内存里存储着别的内存的地址。java对引用进行了扩充。

    1. 强引用,显式的创建对象就是强引用。

    2. 软引用,在虚拟机将要发生内存溢出的情况时,会回收该区域。

    3. 弱引用,引用的对象只能存活到下一次垃圾收集之前。

    4. 虚引用。

(二)方法区中的常量与类已死吗?

  常量容易判断,类则要求很高。需要满足三个条件。

  1. 该类所有实例都被回收,即java堆中不存在该类对象。

  2. 加载该类的classloader已经被回收。

  3. 该类对应的class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  

(三)垃圾搜集算法

  垃圾搜集算法主要有三种,其中一种适用于新生代,另外两种适用于老年代。所谓的新生代就是刚创建的对象,老年代就是在多次垃圾收集中存活下来的对象。

  一、复制算法

    因为在java程序中,许多对象都是“朝生夕死”,因此会有大量的垃圾存在。将内存区域逻辑上划分为两部分,每次分配对象使用其中一块,另外一块在垃圾收集时用来存放存活下来的对象,这样可以
  减少内存碎片的产生。通常将新生代可用内存空间划分为3部分,最大的是Eden,然后有两个相同大小的Survivor。每次分配对象,就使用Eden和其中一块Survivor。当垃圾收集时,把存活的对象复制到
  另外一块Survivor上。

  二、标记-清除算法

    分为两个步骤,分别是标记和清除。缺点有两个,其一,标记和清除两个过程效率都不高;其二,会产生大量的内存碎片。

  三、标记-整理算法

    将存活的对象往一端移动。可以解决内存碎片问题。

(四)HotSpot的算法实现

  一、枚举根节点

    hotspot采用的是可达性分析算法去判断对象的存活。在进行可达性分析的时候,必须要stw(stop the world),即必须停止所有的java执行线程。hotspot中用到了OopMap的数据结构去存储哪些地方
  存放着对象的引用。在类加载的时候,jvm就把对象内多少偏移量有引用计算出来,存放在对象的类型信息里。所以从对象开始向外的扫描可以是准确的。每个被JIT编译过后的方法也会在一些特定的位置
  
记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了。这些特定的位置主要在: 
  1、循环的末尾 
  2、方法临返回前 / 调用方法的call指令后 
  3、可能抛异常的位置 
  这种位置被称为“安全点”(safepoint)

  

  二、安全点

    之所以要选择一些特定的位置来记录OopMap,是因为如果对每条指令(的位置)都记录OopMap的话,这些记录就会比较大,那么空间开销会显得不值得。选用一些比较关键的点来记录就能有效的缩
  小需要记录的数据量,但仍然能达到区分引用的目的。在这些安全点位置,线程的状态都已经可以被确定了,里面的引用关系也已经基本确定,这时进行OopMap的记录是比较好的。

    OopMap的记录在编译时在安全点的位置就开始记录,GC在运行时在安全点的位置要开始GC,那么该如何中断线程去GC呢?这里分为两种中断方式,分别是抢先式中断和主动式中断。

    抢先式中断是等发起GC时,停止所有线程,当发现有线程没到安全点时就恢复执行,几乎不使用这种方法。

    主动式中断是当需要GC时,不主动中断线程,而是简单设置一个标志,线程执行到安全点位置和创建对象需要分配内存时自己主动去轮询这个标志,标志为真就将自己挂起。

  三、安全区域

    安全区域是对安全点的补充。安全点是在Java程序执行过程中GC的一个节点,而程序不执行时,即sleep或者blocked时,就出问题了。安全区域则是专门解决这个问题的。
    安全区域是指在一段代码中,引用关系不会发生变化。当进入安全区域时,jvm发起GC就不用管这个线程了。当线程要离开区域时,必须要检查是否已经完成根节点枚举,完成,则可以安全离开,未完成
  则必须停下等待,直至收到可以安全离开的信号为止。

猜你喜欢

转载自www.cnblogs.com/one-code/p/10062384.html