(一)JAVA虚拟机垃圾回收-时机和对象

754 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

为什么要垃圾回收

如果不限制Xmx则会对系统造成影响,Java虚拟机运行的时候限定了堆区的最大内存Xmx,实际使用的内存不能超过这个值,所以要回收一些不需要的垃圾内存。

  • Java程序不需要主动去垃圾回收
  • Java虚拟机有帮助垃圾回收的机制

什么时候回收

什么时候回收简单来说就是内存不够的时候,但是什么时候内存不够呢?

在JVM里,将堆区划分为两个区域,新生代和老年代,分别用来存放新生成的对象长期存在的对象。 于是也有了分别针对这两个区域的GC。而这两个区域的GC又是分层的,在新生代的GC后内存仍然不够才有可能触发老年代的GC

老年代的GC,又称为FullGC。分场景来说,FullGC在这些情况下会被触发:

1、发生Young GC之前进行检查,如果“老年代可用的连续内存空间” < “新生代历次Young GC后升入老年代的对象总和的平均大小”,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,此时会触发FullGC

2、当老年代没有足够空间存放对象时,会触发一次FullGC

帮助打印gc详情:java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar demo.jar &

具体的过程

Eden区域刚开始的设置并没有到达Xmx的最大,会有一个初始值,不够用的时候也会触发young gc..

gc1.png

触发开始GC回收

gc2.png 触发老年代的回收

gc4.png

什么该回收

引用计数

垃圾回收可以采用引用计数法,但是这个基本思想存在一个致命的缺点,这个缺点可能导致内存泄漏,因为有些a->b,b->a,但是别的地方都不用他们了。如果这样的垃圾一直不能回收,数据量增大的时候会造成系统奔溃

GC root可达

垃圾回收的基本思想是看是否是根可达,如果可达,那么就认为是不需要被回收的。如果不可达,那么就可以被回收了。

那什么是垃圾回收的roots呢?

image.png

总体来说,Roots就是当前的stack上,那些栈帧(是所有的frame,而不是栈顶的frame)的局部变量表里面的引用。

垃圾回收找垃圾的算计,从roots开始遍历一遍,就能找出所有的可达的节点。深度优先或者广度优先遍历都是可以的。 在节点很多的 情况下,标记的过程的效率影响垃圾回收的过程。此时整个系统都要停顿

如果是采用标记-复制的垃圾,所有可达的节点对象都复制到另外一半的区域。

具体的roots有哪些呢?主要是以下这些。

  • 虚拟机栈中引用的对象-局部变量
  • 静态成员引用的对象
  • 类成员引用的对象
  • 方法区常量引用的对象
  • 本地方法栈JNI引用的对象

虚拟机栈中引用的对象-局部变量

  • 笨方法:遍历栈里所有的变量,逐一进行类型判断,如果是 Reference引用类型,则属于 GC Roots,如果栈中的变量非常多,确实会非常的影响效率。
  • 高效方法:从外部记录下栈里那些 Reference 类型变量的类型信息,存成一个映射表 -- 这就是 OopMap 的由来

后面还会详细讲述OopMap的一些具体情况

“在解释执行时/JIT时,记录下栈上某个数据对应的数据类型,比如地址1上的”12344“值是一个堆上地址引用,数据类型为com.aaaa.aaa.AAA)

现在三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样做的。其中,HotSpot把这样的数据结构叫做 OopMap,JRockit里叫做livemap,J9里叫做GC map。”

GC 时,直接根据这个 OopMap 就可以快速实现根节点枚举了。

静态成员引用的对象、类成员引用的对象、方法区常量引用的对象

静态成员引用的对象和类成员引用的对象在方法区找。通常来说,这块的量数据小,不会影响查找的效率。

应该也是可以记录在OopMap

本地方法栈JNI引用的对象

本地方法栈是指这样的代码的栈

JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
   // 缓存String的class
   jclass jc = (*env)->FindClass(env, STRING_PATH);
}

虚拟机假死问题-Stop the world?

在生产环境中,经常遇到虚拟机假死的现象,或者长时间停顿的现象。比如下面这种情况

# /usr/local/TencentKona-8.0.5-282-fiber/bin/jstat -gc 27 1000 10
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777431.0  141440.0 133421.2 18048.0 16699.9    119    3.127 10491 11432.048 11435.176
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777430.7  141440.0 133421.2 18048.0 16699.9    119    3.127 10492 11433.227 11436.354
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777430.7  141440.0 133421.2 18048.0 16699.9    119    3.127 10492 11433.227 11436.354
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777429.6  141440.0 133421.2 18048.0 16699.9    119    3.127 10493 11434.366 11437.493
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777431.2  141440.0 133421.2 18048.0 16699.9    119    3.127 10494 11435.499 11438.627
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777433.1  141440.0 133421.2 18048.0 16699.9    119    3.127 10495 11436.614 11439.741
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777435.5  141440.0 133421.2 18048.0 16699.9    119    3.127 10496 11437.832 11440.959
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777436.6  141440.0 133421.2 18048.0 16699.9    119    3.127 10497 11438.941 11442.069
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777436.1  141440.0 133421.2 18048.0 16699.9    119    3.127 10498 11440.023 11443.151
465920.0 386048.0  0.0    0.0   497664.0 497664.0 2796544.0  2777435.4  141440.0 133421.2 18048.0 16699.9    119    3.127 10499 11441.089 11444.217

统计的间隔时间是1000ms,统计10次,

发现值10s的时间里有9s多的时间在进行full gc,一共gc了9次,平均每次gc的时间大概是1s。

这有可能是标记阶段(找root和可达的节点)在停止所有的线程

OopMap is a structure that records where object references (OOPs) are located on the Java stack. Its primary purpose is to find GC roots on Java stacks and to update the references whenever objects are moved within the Heap.

OopMap

认为OopMap的全称是Ordinary Object pointer map。

如下图所示:

gc5.png

首先在类加载完的时候,虚拟机就把对象内什么偏移量上是什么类型的数据计算出来,包括静态对象和常量引用的对象。 另外虚拟机在运行时,也会在特定的位置记录下栈和寄存器中哪些位置是引用。 这样,GC在扫描的时候就比较方便的找到roots.

忽略这里寄存器,考虑栈的和类的情况就行

主动式中断

虚拟机不直接中断线程,而是在内存设置标志位,当线程检查到标志位被设置了,则运行至safePoint时主动中断 safePoint一般出现在:

  1. 循环体的结尾
  2. 方法返回之前
  3. 调用方法的call之后
  4. 抛出异常的位置

这些位置保证线程不会长时间运行导致无法到达safePoint,避免其他线程都停顿等待本线程。

小总结

如果像本章开头那个情况垃圾会送不掉,GC太频繁(发现值10s的时间里有9s多的时间在进行full gc,一共gc了9次,平均每次gc的时间大概是1s,回收不了内存也就分配不了新内存),所有线程都几乎准备跑到安全点停顿,则会造成Stop the world,我们看到的现象就是虚拟机线程停顿、处于假死的状态。

编程经验

从上面的分析我们先可以学到, 变量声明和使用的地方不要相隔太远,一般不推荐超过三行,这也是有道理的。 如果相隔的距离太长,这个变量就在内存里会占据的时间更久,垃圾回收能清理的内存就又变小了。

总结

总结来说,本文主要讲述了垃圾回收的时机和对象。 查漏补缺,都了解到了这些?

  • Java程序不需要主动去垃圾回收
  • Java虚拟机有帮助垃圾回收的机制
  • 分配不够的时候分代回收

Gcroots有哪些呢?

  • 虚拟机栈中引用的对象-局部变量
  • 静态成员引用的对象
  • 类成员引用的对象
  • 方法区常量引用的对象
  • 本地方法栈JNI引用的对象 了解引起虚拟机假死的罪魁祸首stop the world!