Java虚拟机垃圾回收

一、内存回收的关注区域

JMM章节中介绍了Java虚拟机内存模型的几个区域,对于程序计数器、虚拟机栈和本地方法栈都是线程私有的,伴随着线程由生到灭,这几个区域的内存分配这回收都有一定确定性(因为所占内存大小基本是编译可知的),因此,内存回收主要的关注对象是堆和方法区。我们只有在程序运行时才能确定会创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器主要关注的是这部分内存。

二、对象可被回收的判断标准

  1. 引用计数法

    思想:就是对每个对象添加一个引用计数器,使用该算法的微软的com技术等

    弊端:这种方式存在一个问题就是循环引用,导致内存泄露
  2. 可达性分析法

    思想:就是通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。

    Java语言中,可以作为GC Roots对象包括下面几种:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中(Native方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  1. 引用相关知识

    一般意义上讲,引用就代表了一个对象的内存地址或者存有没有对象地址的句柄,但是这种定义导致一个对象只存在引用和未被引用两种状态。而在实际应用中,一个对象需要更多的状态,来提高效率和优化性能。因此,Java引用分为4种:
  • 强引用:代码中最常见的 Object obj = new Object();只要引用还存在,垃圾收集器永远不会回收被引用的对象。
  • 软引用:用来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会将这些对象列进可回收范围之中进行第二次回收,如果这次回收还是没有足够的内存,那么才会抛出内存溢出异常。通过SoftReference来实现。
  • 弱引用:也是用来描述非必须的对象。弱引用只能生存到下一次垃圾收集之前。当垃圾收集器开始工作时,无论内存是否足够,都会回收这部分内存。通过WeakReference来实现。
  • 虚引用:一个对象是否有虚引用,完全不会对其生存周期构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾收集器回收时能够收到一个系统通知。通过PhantomReference来实现。
  1. 对象是否直接死亡

    对于可达性分析算法中不可达的对象,也不是“非死不可”,要真正宣告对象死亡,必须经历两次标记过程。如果可达性分析不可达时,对象会被第一次标记并且进行一次筛选,筛选标准是对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机都将这两种情况视为没有必要执行。如果这个对象被判定位必要执行finalize()方法,则会被放在一个F-Queue队列中,并且会在稍后由一个虚拟机自建的Finalize线程去执行它。一个对象的finalize()方法只会被执行一次,一个对象可以在finalize()完成一次自救,并且只能自救一次。

三、垃圾回收

  1. 回收方法区

    方法区的回收对象主要是废弃常量和无用的类。对于一个字符串常量“hello”,如果当前系统中没有任何一个String对象叫做“hello”,那么就认为没有任何对象引用常量池中的“hello”常量,这时该常量就是废弃常量。无用的类的判断标准则比较复杂,需要满足一下条件:
  • 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方引用,无法在任何地方通过反射来访问该类的方法。

    满足上面三个条件的无用类可以被回收,但是并不是必然回收。还需要看虚拟机参数设置
  1. 垃圾收集算法
  • 标记-清除算法

    思想:首先标记出所偶遇需要回收的对象,标记完成之后进行统一回收。

    弊端:标记和清除的效率很低,另外就是会产生大量不连续的碎片,碎片过多可能会导致后续对象分配内存时,无法找到适合内存空间而进一步促发下一次垃圾收集动作
  • 复制算法

    将可用内存一份为二,每次使用其中一块,当一块内存使用完之后,将还存活的对象复制到另外一块上,然后将使用的这一块给一次清理掉。
    弊端:将可用内存空间一半空闲,代价过高

    实际使用中大多数对象都朝生夕灭,所以优化方法就是讲内存分配默认Eden、和两块相等的Survivor空间8:1:1,每次垃圾回收时将还存活的对象复制到空闲的那一块survivor中,这样每次就只会浪费10%的内存空间。当然,当survivor不够保存剩余存活的对象时,需要到老年代担保,也就是当超出时,将对象放到老年代。
  • 标记-整理算法

    标记过程和标记-清除算法一样,只是在清除时,会将所有存活的对象移动到一端,然后直接清理端外界的的内存。
  1. 垃圾收集器
  • Serial收集器

    这个收集器是一个单线程的收集器,这里单线程的意思是当GC线程开始工作时,其他的用户线程都必须暂停,当到达安全点之后,GC线程新生代中采用复制算法,老年代中采用标记-整理算法进行垃圾回收

    弊端:需要暂停,非常影响体验

    优势:简单高效,不会有线程交互的开销,如果停顿时间控制得当,不要频繁发生,则还可以接受,一般用于运行在Client模式的虚拟机
  • ParNew收集器

    多线程版本的Serial收集器。使用多线程来进行垃圾回收。许多运行在server模式的虚拟机首选新生代收集器,原因是,除了Serial以外,只有它可以与CMS收集器配合使用。
  • Parallel Scavenge收集器

    CMS等收集器的关注点是如何减少垃圾回收时,用户线程的等待时间,而Parallel Scavenge收集器则是达到一个可控制的吞吐量 吞吐量 = 执行用户代码时间/(执行用户代码时间+垃圾收集时间)。停顿时间短适合交互性比较强,需要良好的响应速度来提升用户体验,而高吞吐量则可以高效的利用cpu时间,尽快完成程序的运算任务。
  • Serial Old收集器

    Serial收集器的老年代版本,使用标记-整理方法,一个是与Parallel Scavenge配合使用,第二点就是作为CMS的后备方案
  • Parallel Old收集器

    Parallel Scavenge的老年代版本,它出现之前,新生代的收集器Parallel只能和Serial Old配合使用
  • CMS收集器

    这种收集器主要获取最短回收停顿时间为目标,主要用于互联网站等系统的服务端。标记-清除算法实现。其垃圾收集的过程分为四步:

    初始标记:主要标记GC root能够直接关联的对象,时间短

    并发标记:GC root tracing的过程,时间长,但是与用户线程并发执行

    重新标记:修正并发标记过程中因用户程序继续运用而产生变动的那一部分对象,时间短

    并发清除:时间长,与用户线程并发执行

    缺点:对cpu资源非常敏感,因为并发执行阶段会占用线程执行的cpu资源,当cpu较少时会导致系统吞吐量很低。无法处理浮动的垃圾,可能会出现(concurrent mode failure)失败而导致另一次full gc的出现,在并发清除阶段用户线程还会生产垃圾。还有一个缺点就是标记-清除,会产生大量空间碎片。
  • G1收集器
    是一款面向服务端的垃圾收集器,特点:并发和并行,分代收集,空间整合,整体上看像标记-整理,局部像复制,可预测的停顿。它将整个堆分为多个大小相等的区域,虽然保留了新生代和老年代的概念,但是不再是隔离的了。跟踪每个区域垃圾堆积的大小,每次回收时,优先回收价值最大的region。

    垃圾收集的步骤:

    初始标记:

    并发标记:

    最终标记:

    筛选回收:

    四个过程和cms垃圾收集器很类似,最后一步不同在于,g1是挑选最具有回收的价值的区域开始回收

4.GC 日志

关键点:GC、Full GC、回收前的容量、回收后的容量、总的容量、回收的区域、回收时间a

你可能感兴趣的:(Java虚拟机垃圾回收)