文章采集调用(java内存溢出和内存泄漏的区别,你知道吗?)

优采云 发布时间: 2021-09-01 14:10

  文章采集调用(java内存溢出和内存泄漏的区别,你知道吗?)

  评论

  上一篇博客写了JVM的内存溢出问题,比较了内存​​溢出和内存泄漏的区别,然后做了虚拟机栈的OOM和SOF,方法区和运行时常量池的OOM,以及堆的OOM相关实验验证,在实验过程中,发现java8对方法区perm gen进行了改进,即metaspace替换了perm gen(java7将字符串常量池移到了堆中)。最后提出两个问题,关于使用stringbuilder导致堆空间OOM的实验现象,目前还没有解决,期待与读者一起探讨。

  之前的博文大致完成了java运行时数据区的基本内容。本篇博客进入JVM的垃圾回收部分。这部分预计会写在两个博客中。本篇博客将学习上述算法的对象生存判断算法、垃圾回收算法以及JVM实现。

  物体生存判断算法

  我在百度最上面的小书里看了一篇关于对象生存判断算法的解释,解释的太好了。 . 我不会太尴尬写的,我会在本节末尾链接到它。这里我硬着头皮按照文章的思路写出来! (在此向文章的作者致以崇高的敬意,先盗图。)

  先介绍一下大致流程:

  

  对象生存判断算法流程图

  首先进行可达性分析,对不可达对象进行两次标记/筛选过程。如果任何标记/筛选阶段未能“自救”,该对象将被回收。

  判断方法

  目前,引用计数和可达性分析在编程语言中被普遍使用。

  1.引用计数方法

  虽然主流JVM中没有使用引用计数的方法,但在Python、游戏脚本语言等领域也有广泛的应用。是经典的内存管理算法。

  优点:实现简单,判断效率较高

  缺点:无法解决对象循环引用的问题

  例如以下代码:

  public class ReferenceCountingGC {

private ReferenceCountingGC instance = null;

private static final int _1MB = 1024*1024;

private byte[] bigsize = new byte[2*_1MB];//占用内存,方便查看GC,因为每个对象都有它

public static void main(String[] args) {

ReferenceCountingGC objA = new ReferenceCountingGC();

ReferenceCountingGC objB = new ReferenceCountingGC();

//相互引用

objA.instance = objB;

objB.instance = objA;

//置为null

objA = null;

objB = null;

//想要触发GC

System.gc();

}

}

  这里是GC日志:

  [GC (System.gc()) [PSYoungGen: 9876K->1292K(36864K)] 9876K->1300K(121856K), 0.0794454 secs] [Times: user=0.16 sys=0.00, real=0.08 secs]

[Full GC (System.gc()) [PSYoungGen: 1292K->0K(36864K)] [ParOldGen: 8K->1188K(84992K)] 1300K->1188K(121856K), [Metaspace: 4725K->4725K(1056768K)], 0.0098699 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

Heap

PSYoungGen total 36864K, used 317K [0x00000000d6c00000, 0x00000000d9500000, 0x0000000100000000)

eden space 31744K, 1% used [0x00000000d6c00000,0x00000000d6c4f738,0x00000000d8b00000)

from space 5120K, 0% used [0x00000000d8b00000,0x00000000d8b00000,0x00000000d9000000)

to space 5120K, 0% used [0x00000000d9000000,0x00000000d9000000,0x00000000d9500000)

ParOldGen total 84992K, used 1188K [0x0000000084400000, 0x0000000089700000, 0x00000000d6c00000)

object space 84992K, 1% used [0x0000000084400000,0x0000000084529058,0x0000000089700000)

Metaspace used 4732K, capacity 4930K, committed 5248K, reserved 1056768K

class space used 510K, capacity 561K, committed 640K, reserved 1048576K

  可以看到,经过一次GC,9876→1292,堆中的对象被回收了。这从侧面说明Hotspot虚拟机不是采用的引用计数方式。

  2.可访问性分析算法

  可达性分析的两个重要概念:GC 根和引用链。

  GC 根

  gc 根是引用链的根节点。可以作为gc根的对象分为以下几类:

  1.虚拟机栈本地变量表中引用的对象

2.本地方法栈中JNI(java native interface)引用的对象

3.方法区的常量、类静态属性引用的对象

  参考链

  从gc root开始,向下搜索,搜索遍历的路径称为引用链。

  如下图:

  

  参考链

  可访问性分析

  如果一个对象和 gc 根没有通过任何引用链连接,那么这个对象就被认为是不可达的。无法到达的对象即将进入标记/筛选阶段。

  标记/筛选阶段

  1.第一次打标/筛选过程

  简单来说,第一个标记过程就是看对象是否需要执行finalize方法。如有必要,进入下一个标记阶段,如果不需要执行,则结束标记阶段,基本确定该对象需要回收。

  是否需要执行finalize方法判断依据

  1.如果该对象对应的类中没有覆盖finalize方法,则说明没有必要执行

2.如果在之前,该对象已经执行过一次finalize方法了,则说明没有必要执行(因为finalize方法只能执行一次)

  2.第二次打标/筛选过程

  在第一次标记/筛选过程之后,认为需要执行finalize方法的对象被放置在F-QUEUE中,JVM会自动创建一个低优先级线程来执行F-QUEUE中的finalize方法。这里的“执行”并不一定意味着必须完全执行。因为如果在finalize方法中存在无限循环,是不是要等到执行完毕?队列将被阻塞!

  判断主体是否活着的依据:

  在finalize方法中,该对象又重新连接到了gc roots的引用链上

  实验

  package gctest;

public class FinalizeEscapeGC {

private static FinalizeEscapeGC instance = null;//instance是gc root

@Override

protected void finalize() throws Throwable {

super.finalize();

FinalizeEscapeGC.instance = this;

System.out.println("I save mylife!");

}

public void isAlive() {

System.out.println("I'm alive!");

}

public static void main(String[] args) {

instance = new FinalizeEscapeGC();

instance = null;//对象和gc roots之间失去连接

System.gc();

//第一次拯救过程

try {

Thread.sleep(500L);

} catch (Exception e) {

e.printStackTrace();

}

try {

instance.isAlive();//判断对象是否存活,若存活,则不会是空指针

} catch (NullPointerException e) {

System.out.println("nullpointerException happens");

}

instance = null;//对象和gc roots之间失去连接

System.gc();

//第二次拯救过程,就是把第一次的代码重复一遍

try {

Thread.sleep(500L);

} catch (Exception e) {

e.printStackTrace();

}

try {

instance.isAlive();//判断对象是否存活,若存活,则instance不会是空指针

} catch (NullPointerException e) {

System.out.println("NullPointerException happens");

}

}

}

  

  运行结果

  可以看出对象开始自救一次,然后自救失败,失去与实例的连接,实例为空。

  参考简书文章超LINK

  简书关于对象生存判断算法的超链接

  回收方法区

  方法区也存在于GC中。主要是回收过时的常量和无用的类。

  Abandoned constants:不再使用的常量(指常量池中的字面量和符号引用),比如一个“ABC”存放在常量池中,但没有在任何地方使用,被判断为是一个过时的常量。

  无用类:

  1.该类的实例对象都被回收

2.加载该类的类加载器被回收

3.该类的java.lang.Class对象没有在任何地方引用到,无法通过反射访问该类中的方法

  本节总结

  一旦一个对象被unreachable分析确定为unreachable,它必须经过两次标记过程的重复测试才能成功自救,并且只能通过finalize方法自救一次。

  垃圾采集算法

  共有三种采集算法和一种采集思路。即标记清除算法、标记排序算法、复制算法、分代采集思想。

  1.tag-clearing 算法

  这个算法很容易理解,就是将确定为死的内存回收,什么都不做。

  优点:简单

  缺点:会形成大量的空间碎片,导致新创建的对象没有完整的内存分配,会提前启动full GC。

  

  标签去除算法

  2.tag-sorting 算法

  标签排序算法是在标签清除算法的基础上改进的。死对象的内存恢复后,将存活的对象移动到内存空间的一端,解决了空间碎片化的问题。

  优点:不会产生空间碎片,阻止Full GC提前进行

  缺点:执行速度比去除标记慢

  

  标签排序算法

  3.复制算法

  复制算法,简单来说就是你现在有两个空间,一个 A 用来存储对象,另一个 B 是空的。回收死对象后,将A中幸存的对象复制到B,然后清空A的内存,这样就形成了一个存放对象的空间和一个空的空间。

  优点:不存在空间碎片问题,空间分配效率很高

  缺点:牺牲了一部分内存空间作为空内存空间。

  

  复制算法

  4.子代采集思路

  分代回收的思想是将堆内存分为老年代和新生代,针对不同的区域实现不同的垃圾回收算法,让内存的分配和回收更加高效!例如,老年适合使用标记去除算法和标记排序算法。新生代被进一步划分为Eden区,从survivor区到survivor区执行复制算法。

  JVM中GC算法的对象生存判断与实现

  1.Enumeration 根节点

  在进行可达性分析时,需要根节点信息和引用链信息,需要两个需求:STW(stop the world)和引用信息

  STW:可达性分析具有时间敏感性。虽然无法再分析,但参考关系仍在变化。因此,GC期间必须停止所有Java线程,称为stop the world。

  引用信息:要建立引用关系,需要知道当前内存中哪些对象是引用。传统的方法是直接扫描整个内存来获取参考信息。 Hotspot 使用 oopMap 方法。

  1.记录栈、寄存器等区域中哪些位置是GC管理的指针

2.一段代码内可以有多处使用oopMap,但不是每条指令都会使用oopMap,也就是安全点,safepoint将一段代码分为好几段

3.oopMap的作用域也只在它所在的那一段里

  2.安全点

  GC只有在超出安全点的情况下才能执行。安全点一般在“程序可以长时间执行的地方”,其特点是指令序列的复用。 (我一直不明白为什么我在看书的时候会选择这个,以后再说)

  1.循环跳转

2.方法调用

3.异常跳转

  你为什么选择这种方式?

  1.安全点选择太少,每次GC间隔太长,安全点选择太多,GC执行太频繁。

  2.Imagine 如果在 A 和 B 两个安全点之间有循环跳转、异常跳转等上面提到的代码段,这段代码的执行时间很长。 . . . ,甚至无限循环,难道你不想在B安全点执行GC直到天老吗? (也就是GC之间的间隔太长了)

  如何使所有线程安全?

  GC 是整个 JVM。 JVM中会有很多线程在执行,那么如何保证GC期间所有线程都处于安全点呢?

  分为抢占式中断和主动式中断

  抢先中断

  特点:不照顾线程感受

过程:GC时先中断所有的线程,然后让那些没有到安全点的线程自己再跑到安全点

使用:现在已经没有使用抢先式中断的了

  主动中断

  特点:照顾线程感受,让它自己去吧

过程:设置一个GC标志,线程执行到安全点或创建对象分配内存时,主动去轮询这个标志,为真时就主动中断自己(这个时候是安全点,中断就中断呗)

使用:大家都说好

  3.安全区

  上面考虑了多线程,但没有考虑线程在执行过程中可能会休眠或阻塞。如果等待它的sleep结束或者CPU时间片的分配,它又会变老!于是引出了安全区的概念

  定义:安全区域是指这一段代码中的引用关系不会发生变化

线程在安全区域行为本质上是一个握手过程

过程:

1.线程A进入safe region,设置一个标志Ready flag.

2.GC如果在线程A处于safe region的时间内进行,由于ready flag的存在,不再检查

线程A

3.线程A将要离开safe region时,轮询GC设置的标志,若为真表示GC还没有执行完,则线程A中断自己,保证自己不离开safe region。若此时GC已经执行完毕,则A顺利离开region。

  总结

  本篇博客学习了对象生存判断算法和垃圾回收算法。后面会学习垃圾采集器,对象的内存分配和回收策略,以及JVM对上述算法的实现。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线