JVM-堆和回收

208 阅读7分钟

JVM-堆和回收

堆的划分

堆又因为对象分代年龄可以被分为三个区域,分别为新生代,老年代,永久代。

  • 新生代:当一个对象被new出来,使用完后被回收,这就是一个新生代对象
  • 老年代:当一个对象经过n次回收后依旧没有被清理,便会被移到老年代,这就是老年代对象。
  • 永久代:即之前讲过的方法区,用于存放已被类加载子系统加载过的类的信息。

对象回收

对象回收方法
  1. 引用计数法:给对象添加一个引用计数器,每当这个对象被引用,引用计数器加1,当引用失效的时候,引用计数器就减1,为0的时候被回收,缺点就是无法处理循环引用

  2. 可达性分析法:通过一系列的‘GC roots’的对象为起始点,从这些节点开始往下搜索,走过的路径称为‘引用链’,当一个gcroot无法到达一个对象的时候,这个对象会被判定不可用然后被回收。(注意,这里的回收会经历两个阶段,具体请参考finilize笔记)

    可做为GC roots引用的对象有

    • 虚拟机栈中栈帧中局部变量表中引用的对象(方法中的局部变量引用的对象)
    • 方法区类静态属性引用的对象(静态变量引用的对象)
    • 常量池中常量引用的对象
    • 本地方法栈中引用的对象

    (类的实例变量不能作为GC roots直接引用的对象)

    关于引用

    引用分为强引用,软引用,弱引用,虚引用。一般new对象的引用就是强引用,软引用指的是在内存溢出之前才会被考虑回收的引用,弱引用只能生存到下一次垃圾回收前,虚引用的存在对一个对象没有任何影响,它的作用主要是对象被回收了可以提供一个消息通知。

对象回收后的垃圾收集算法
  • 标记-清除算法

    标记清除算法是将需要清除的对象进行标记,标记完后将其清除,这样做的缺点是会产生内存碎片,并且效率过低。

  • 复制算法

    复制算法是将整个内存区域分为一块eden区域和两块survivor区域,将eden区域和其中一块survivor区域中的没被回收的对象放入另一块survivor区域,然后将那两块区域全部清空,优点是效率高,不会有内存碎片,缺点是总会有一块内存区域是空的,eden与survivor比例划分为8:1,如果当内存不够的时候,新生代内存区域会对老年代内存区域进行内存担保来获取一部分内存。

  • 标记-清除-压缩算法

    与之前的标记清除算法一样,只是多了清楚后将存活下来的对象进行整理这一步操作,这样的优点是不会有内存碎片,但缺点是效率更低了。

  • 分代收集算法

    分代收集算法并没有什么创新,只是在不同内存区域他会根据该内存区域的特点来选用标记清除算法还是复制算法。

如何安全发起对象回收

选用为GC root的节点一般在执行上下文(虚拟机栈栈帧的局部变量表)和全局引用(方法区)中,在对象回收的过程中是不能出现对象引用关系发生变化的情况(finalize除外),这样就必须暂停所有的线程来进行对象回收,如果一个不漏的去检查完执行上下文和全局引用的位置,这样会导致长时间停顿,所以虚拟机必须很快知道对象引用的位置。 所以Hotspot采用一种数据结构——OopMap来解决这个问题。在类加载完成的时候,虚拟机将对象的什么偏移量有什么对象计算出来,在JIT编译过程中在特定的位置记录下栈和寄存器中哪些位置是引用。这样一来GC在扫描的时候就可以直接得到这些引用的信息,从而减少GC的停顿时间 。 OopMap数据结构可以说为GC的扫描减少了不少的时间,但是随之而来的还有一个问题,如果每条指令都生成对应的OopMap,那么想必需要大量的额外空间,GC的空间成本将十分巨大,就是何时生成对应OopMap成为当前面临的问题。 之前提到的选用“特定的位置”就叫做安全点,安全点选用的特征是具有能让程序长时间执行,比如方法调用,循环体末尾等。选用好安全点后,运行中的线程通过抢占式或者主动式来被中断,抢断式就是先将所有线程进行中断,当发现有的线程并没有在安全点的时候,将其运行安全点再中断。主动式中断是在每个线程的安全点设置一个标志,当到达此处的时候会被挂起。没有运行中的线程它自身会有一个“安全区域”的标记,当这个线程要运行的时候会检查虚拟机是否完成了GC过程,完成了便可以开始执行,否则就继续等待。 (oopMap避免了虚拟机全盘扫描寻找GC roots而是在JIT编译时期就记录下了虚拟机栈和方法区等区域里哪里存储的是引用,这样提高了效率,并防止为了给每条指令都生成oopMap他只在特定位置生成,特定位置的特征是“能使程序长时间运行”,到达特定位置的方式有抢占式和主动式。)

垃圾收集器

Serial收集器(单线程,新生代)

serial收集器在新生代使用复制算法回收,在老年代使用标记整理算法回收。适用于client模式

Parnew收集器(多线程,新生代)

serial收集器的多线程版本。无法与CMS配合,CMS主要负责老年代垃圾回收工作。

Parallel Scavenge收集器(多线程,新生代)

与Parnew收集器负责的区域一样,但是注重点不一样,parnew收集器注重的是尽可能缩短停顿时间。Parallel Scanvenge收集器注重的尽可能提高吞吐量。吞吐量=程序代码运行时间/(程序代码运行时间+垃圾收集时间)

Serial Old收集器(单线程,老年代)
Parallel Old收集器(多线程,老年代)
CMS收集器(多线程,老年代)

CMS收集器是注重尽可能短的停顿时间的收集器,重视服务的响应速度,concurrent mark sweep,方法使用的是标记清除算法。整个垃圾回收过程分为以下几个步骤

  1. 初始标记(需进入safe point停顿所有线程进行标记,标记GC Root关联的对象,时间短)
  2. 并发标记(与用户线程并发进行,不影响线程的运行,时间长)
  3. 重新标记(需进入safe point停顿所有线程进行标记,时间短)
  4. 并发清除

这样做能够达到低停顿的目的,但CMS的缺点如下

  1. CMS对CPU资源很敏感,CMS整个垃圾回收过程有两个步骤都是并发执行的,占cpu资源很多,这样会导致虽然低停顿但是吞吐量会很低的情况,况且当CPU资源很少的时候,会使应用程序变慢。
  2. CMS无法处理浮动垃圾,CMS在清楚阶段是并发清除的,在清除过程中可能还会产生垃圾,CMS无法集中处理掉他们,只能留在下一次垃圾回收来处理。
  3. CMS使用的算法是标记清除算法,前面已经说过标记清除算法会造成内存碎片,可以设置参数让他进行标记清除整理算法,但这样就不是并发了,需要停顿线程。

(仅用于记录平时学习个人心得,如有错误恳请指教)