Java虚拟机(一)-Java虚拟机内存分区以及垃圾回收算法

321 阅读5分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。

jvm内存分区

image.png

垃圾回收器

垃圾回收算法

引用计数算法

原理:给对象添加一个引用计数器
缺陷:不能解决循环依赖
好处:引用计数算法还是有好处的可以实时进行垃圾回收,且不用单独启动线程进行可达性分析
实际使用场景:java虚拟机没有使用,python在使用

可达性分析算法

原理:通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

image.png

可作为 GC Roots 的对象:

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

标记-清除

直接标记清除就可。

不足:效率不高、空间会产生大量碎片 image.png

标记-复制

把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。 image.png

标记-整理

再标记清除的基础上又增加了内存碎片整理算法

image.png

分代回收

分代收集理论基于以下三种假说和经验法则。

  • 弱分代假说

绝大多数对象,在第一次垃圾收集时就会被回收,按照经验法则,这个值高达百分之九十八。

  • 强分代假说

熬过越多次收集过程的对象越难以消亡。

  • 跨代引用假说

该假说认为只会存在很少的跨代引用。因为只要经过一些次数的垃圾收集,即使还存在跨代引用,新生代会变成老年代,跨代引用也就自然消失了,所以跨代引用的数量不会多。在对新生代对象进行收集时,由于可能存在老年代对象引用了该对象,那么,需要找到这些老年代对象。根据跨代引用假说,这些跨代引用的数量不会太多,相比于对老年代进行扫描,在新生代建立一个全局数据结构(记忆集),记录哪一块老年代内存会存在跨代引用,虽然维护这个数据结构,也需要少量的开销。但仍然显得更加合算。

根据分代理论,新生代的对象很大概率一次垃圾收集就会被回收,而老年代中的对象在下一次垃圾收集被回收的概率较小,正是由于这个区别,新生代和老年代的垃圾收集会使用不同的垃圾收集算法和垃圾收集器。

新生代每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。 老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用标记清除 或者标记整理算法回收。

延伸学习
什么是记忆集?

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。

如果我们不考虑效率和成本问题,我们可以用一个数组存储所有有指针指向新生代的老年代对象。但是如果这样的话我们维护成本就很好,打个比方,假如所有的老年代对象都有指针指向了新生代,那么我们需要维护整个老年代大小的记忆集,毫无疑问这种方法是不可取的。因此我们引入了卡表的数据结构

什么是卡表?

记忆集是我们针对于跨代引用问题提出的思想,而卡表则是针对于该种思想的具体实现。(可以理解为记忆集是结构,卡表是实现类)

在hotspot虚拟机中,卡表是一个字节数组,数组的每一项对应着内存中的某一块连续地址的区域,如果该区域中有引用指向了待回收区域的对象,卡表数组对应的元素将被置为1,没有则置为0;下图为卡表的一个实现,某块内存的地址向右移动9位(相当于除以512)定位到一个卡表元素,也就是说,内存中每512字节的连续区域会被定位到同一片卡表区域,如果卡表对应元素为1则代表该512个字节所在区域中有指向的指针。

本文已参与「新人创作礼」活动,一起开启掘金创作之路。