深入理解Java虚拟机

时间:2022-12-27 18:36:52

深入理解Java虚拟机--GC部分阅读笔记

转自我的Github

GC

GC(Garbage Collection)这个概念并不是Java语言锁特有的技术。事实上GC的历史比Java久远,Lisp就才是第一门真正实用内存动态分配和垃圾收集技术的语言。在Java语言中,程序计数器,虚拟机栈,本地方法栈三个区域会随着线程的生命周期结束而得到清理,但是Java堆是动态分配的,垃圾处理器主要关注这部分内存。抽象来说GC完成的事情:

  • 哪些内存需要回收
  • 如何回收

下面就按照上面三点来仔细说一下Java GC。

哪些内存需要回收

当一个对象再也没有引用可以引用到,那么我们就说它的生命周期可以结束了,那么如何去判断一个对象是否是已经无用了呢。

引用计数法

给引用加一个引用计数器,当计数器的数值为0就表示没有其它引用可以引用到这个对象,那么当然这个对象就可以安全的被垃圾回收器所回收。引用计数器实现简单,在大部分情况下它都是一个不错的算法,COM,FlashPlayer,Python等都使用了引用计数法金星内存管理。但是它有一个问题,就是很难解决对象之间的循环引用问题。这就类似于图论中的非连通有向图中的连通分量。

可达性分析算法

类似图论中的可达性,以引用为边,以对象为节点。算法的基本思想就是从一系列称为GC Roots的对象作为起始点,从这些节点向下搜索,如果一个节点不可达,则证明这个对象是不可用的,可以被安全的回收掉。在Java语言中,可以作为GC Roots的对象包括以下几种:

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

四种类型的引用

再JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用,这四种引用的强度以此减弱。

  • 强引用就是类似”Object obj = new Object()”这类的引用。只要强引用存在,垃圾收集器就不会回收调被引用的对象。
  • 软引用是用来描述一些还有用但并非必须的对象。对于软引用关联的对象,再系统将要发生内存溢出之前会把这些对象列进回收范围之中进行二次回收,如果这次还没有足够的内存才会抛出内存溢出异常。详情见SoftReference。
  • 弱引用是用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够都会回收被弱引用关联的对象。详情见WeakReference。
  • 虚引用不影响对象的生存周期没有影响,为一个对象设置虚引用关联的唯一目的就是能在对象被收集的时候收到一个系统通知,详情见PhantomReference。

finalize()方法

在可达性分析中不可达的对象也并不一定就会被回收。要真正的回收对象,至少要经历两次标记过程:如果一个对象被分析为不可达,那么它会被第一次标记,并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当一个对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这种情况视为没有必要执行。 如果这个对象有需要执行的finalize()方法,那么这个对象会被放置在一个叫F-Queue的队列中,并在稍后由一个虚拟机建立的,低优先级的Finalizer线程去执行它。但是要注意的是这里的执行只是表示虚拟机会去调用它,但是并不保证等待它执行完成。稍后虚拟机会对F-Queue中的对象进行第二次标记,如果这次标记的时候对象还是不可达,那么它将会被回收。

永久代的回收

方法区(永久代)的垃圾回收的收集效率比较低,但是还是需要垃圾回收的。永久代的垃圾回收主要包括两部分的内容:废弃的常量和无用的类。 废弃常量的判断方法比较简单,只要没有引用可以访问这个常量。对于无用的类的条件就要苛刻很多,一个类需要同时满足以下三个条件才算是无用的类:

  • 该类所有的实例都已经被回收,Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

所以在大量使用反射,动态代理这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能。

##如何回收

垃圾收集算法

  • 标记清除算法:算法分标记和清除两个阶段,先标记出所有需要回收的对象,在标记完成后统一回收。不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片。
  • 复制算法:为了解决效率问题,它将可用内存按照容量划分成大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将存活的对象复制到领一块上面,再把使用过的内存空间一次性清理掉。不足:空间代价太高。 但是由于新生代中的对象大部分的生命周期很短,所以并不需要按照1:1的比例分配空间优化的方法是将内存分为一块较大Eden的空间和两块较小的Servivor空间,每次使用Eden和其中一块Survivor。每次回收将存活的对象复制到另一个Servivor空间中。HotSpot虚拟机默认的Eden和Survivor的比例大小是8:1 。
  • 标记整理算法:根据老年代的特点,标记整理算法被提出了。它将所有的存活对象往一端移动,然后直接清理掉边界以外的内存。
  • 分代收集算法:根据对象的存活周期将内存分为不同的几块,根据各个年代使用适当的收集算法。新生代的对象生存周期短,可以采用复制算法;老年代对象存活率高,没有额外空间对它进行分配担保,所以必须使用标记清理或者标记整理算法进行回收。

HotSpot虚拟机实现

OopMap

记录哪些地方存在对象引用,在JIT编译的过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用(OOP)普通对象引用。

安全点

在特定的地方记录OOP,即安全点,程序执行时并非在所有的地方都能停下进行GC,只有达到安全点才能暂停。安全点的选取时间基本上以“是否具有让程序长时间执行的特征“选择的,所以”长时间执行“的最明显特征就是指令序列的复用,例如方法调用,循环跳转,异常跳转等。 抢先式中断:把所有线程中断,如果发现有线程不在安全点上,就恢复线程,让它跑到安全点去。 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅设置一个标志,各个线程主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点事重合的,另外再加上创建对象需要分配内存的地方。

安全区

扩展的安全点,解决线程被挂起的情况下的GC问题,因为挂起的线程不会引起对象引用的变化,所以在安全区内的线程对GC不会有影响。

垃圾收集器

深入理解Java虚拟机

Serial收集器

单线程的垃圾收集器,简单高效,缺点是会STW(Stop The World),现在用于client模式的JVM。

ParNew收集器

多线程的Serial收集器,只有CMS收集器可以与之配合工作.

Parallel收集器

新生代收集器,以控制吞吐量为目的,而不是短停顿时间,更适合后端cpu计算比例较大的应用,也叫吞吐量优先收集器。

Serial Old收集器

Serial收集器的老年代版本,用于Client模式,如果在Server模式下有两大用途:1.与Parallel收集器搭配使用, 2.作为CMS收集器的预备案,在Concurrent Mode Failure时使用。

ParallelOld收集器

Parallel收集器的老年代版本。

CMS收集器(ConcurrentMarkSweep)

以获取最短回收停顿时间为目的的收集器,集中应用在互联网站或者BS系统的服务端上。

收集步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中,初始标记和重新标记仍然需要STW。初始标记只是标记一下GCRoots能直接关联到的对象,速度很快,并发标记是进行GC Roots Tracing的过程,重新标记是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

缺点:1.对CPU资源敏感,多线程在CPU负载高的情况下表现会下降。2.无法处理浮动垃圾,即在标记过程之后出现的垃圾。所以在程序运行时没有足够的内存就会出发Serial Old收集。3.标记清除算法,会产生内存碎片。

G1收集器

优点:

  • 并行并发
  • 分代收集
  • 空间整合:标记整理算法
  • 可预测的停顿

整个Java堆划分成多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但是新生代和老年代不是物理隔离的了。

之所以可以建立可预测的停顿时间模型,因为它能有计划的避免在整个java堆重进行全区域的垃圾收集。G1根据各个Region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许收集时间,优先回收价值最大的Region(这也是Garbage-First名称的由来)。

Region之间的对象引用以及其他收集器中新生代和老年代之间的对象引用,虚拟机都是使用Reembered Set来避免全表扫描的。G1中每个Region都有一个与之对应的Remembred Set。虚拟机发现程序在对Reference类型的数据进行操作时,会产生一个WriteBarrier暂时中断写操作,检查Reference的引用对象是否处于不同的区域之中,如果是,就通过CardTable把相关引用信息记录到被引用对象所属的RemenberedSet之中。

步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

垃圾收集器常用参数

深入理解Java虚拟机

内存分配策略

  • 对象优先在新生代Eden区中分配,当Eden区中没有空间时,触发MinorGC
  • 大对象直接进入老年代
  • 长期存活的对象进入老年代
  • 动态对象年龄判定:如果在Survivor空间中相同年龄的所有对象大小的综合大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代
  • 分配空间担保:MinorGC时Survivor无法容纳回收后的对象,则直接进入老年代。计算存活对象大小取晋升老年代的对象大小的平均值。