真的可惜,四面阿里,结果我被JVM垃圾回收机制与 OOM异常卡住了

前言

为什么需要垃圾回收

  • 首先我们来聊聊为什么会需要垃圾回收,假设我们不进行垃圾回收会造成什么后果,我们举一个简单的例子
  • 我们住在一个房子里面,我们每天都在里面生活,然后垃圾都丢在房子里面,又不打扫,最后房子都是垃圾 我们是不是就没法住下去了。
  • 所以JVM垃圾回收机制也是一样的,当我们创建的对象占据堆空间要满了的的时候我们就对他进行垃圾回收,注意java的垃圾回收是不定时的,c语言的是需要去调用垃圾回收方法
  • 刚刚也说到 上面举的例子也说到 假设一个房子都被垃圾堆满了 那么我们没法住人了 那么我们是不是会告诉别人这个房子没法住人了 而java也是如此当我们堆空间满了的时候 此时它就会抛出异常OutOfMemoryError(简称OOM)

什么地方需要进行垃圾回收

刚刚我们说了为什么要回收垃圾,和什么是OOM那么我们下面就给大家介绍,我们JVM中什么地方需要进行垃圾回收。

垃圾回收要考虑的点

**1)是否会产生垃圾
2)哪些内存需要回收
3)什么时候回收
4)如何对他进行回收**

程序计数器
jvm中唯一 一个不需要垃圾回收的地方。
栈 本地方法栈
这个地方会因为栈帧存满了导致内存溢出,所以需要垃圾回收
方法区(元空间)
这个地方也需要进行垃圾回收

这个地方是我们垃圾回收最频繁的地方,我们几乎我们所有的对象都存储在堆中,也是我们今天要着重讲的地方

堆GC

堆,可能大家都不陌生,可是好像又距离我们很远,今天它来了

从上面的图我们可以看出 ,我们的堆空间被主要被划分为了二块区域 ,新生代 ,老年代 java堆是我们JVM中管理区域最大的一块, java堆是一个线程共享的区域 ,在虚拟机启动时创建 ,几乎所有的对象都在此分配, java虚拟机规范中有过描述,所有的实例对象以及数组都在堆中进行分配内存, 但目前因为JTI编译器的发展,和逃逸分析技术的逐渐完善,在堆中分配对象也不是那么的绝对了。
堆的内存在物理上可以是不连续的,但是在逻辑上是即可。

新生代

  • 我们对象的创建到结束,几乎是"朝生夕死"的一个过程差不多90%的对象都在新生代被回收了,所以新生代的gc也是发生最为频繁的一个区域。新生代产生gc我们称为Y-GC
  • 每产生一次y-gc我们对象的年龄就加一岁,直到15岁后进入老年代。当然这是正常情况,那么有没有特殊情况勒当然有

空间担保

  • 当我们创建的对象大于Eden的时候,此时怎么办,此时他会先产生一次Y-GC如果还是无法存储下新创建的对象,那么我们就会通过空间担保策略进入老年代。
  • 还有一种情况,对象创建也会直接进入老年代,当我们的Surivivor区满了的时候,此时它不会主动产生gc只会依赖于Eden,但我们的对象又不能被抛弃,所以它也被分配到了老年代
  • 当然我们实际开发工作中需要尽量的去避免这种情况的诞生

动态年龄

  • 什么是动态年龄勒,这是堆中的另一个,担保策略了,它会去判断我们Surivivor的区中,相同年龄的对象大于Surivivor区一半的时候,那么他就会判定此时这些对象已经能够很好的存活了,所以他们就集体被丢到老年代了

对象如何分配内存

老年代

  • 我们老年代存放的都是一些老对象了,大对象,都是存活时间较长的对象这里一般很少产生FGC这里一旦产生FGC那么所产生GC的耗时将会是YGC的10倍时耗,而我们老年代快要存满时进入了一个对象,这时会产生一次FGC如果GC结束后,还是无法存放对象的话此时就会报OOM异常。

垃圾回收算法

分代算法

分代算法,其实也就是将我们堆空间划分为了一个个不同的区域,新生代,老年代,不让它回收的时候对整个堆进行一个回收。减少GC所停顿的时间,我们称之为STW (Stop The World),假设我们堆整个堆进行垃圾回收,是不是每次都需要去把整个堆的垃圾标记一次,非常的那么用户线程停止的时间就非常长,你想一下,假如你的电脑每使用1个小时就卡10秒,那么你是不是非常操蛋。

标记清除

算法执行过程

堆空间垃圾清理前

垃圾清除后

算法介绍
标记清除是最开始jvm选择的一种垃圾回收算法,这个算法就和他的名字一样,分为标记,和清除二个过程,首先他会标记所有需要回收的对象,标记结束后对标记的对象进行一个垃圾回收。
缺点
这种方式会有什么缺点呢!它会导致内存空间的浪费,产生大佬不连续的内存碎片,当我们需要一个连续的内存空间存放大对象的时候,因为连续的内存空间不够,导致我们不得又产生一次GC,提高了我们GC产生的频率。

标记整理

算法执行过程

算法介绍
标记整理,的执行过程,于标记清除相反,标记清除是标记需要回收的对象,而标记整理却是标记存活的对象,然后把他们全部向一段进行位移,然后清除端边界以外的所有对象。
适用范围
老年代垃圾回收

复制算法

算法执行过程

算法介绍
复制算法也是在标记清除上的一个改进,它弥补了标记清除出现大量不连续内存碎片的缺点。它将一个可用的内存空间划分为大小相等的二块区域,每次只使用其中一块区域,当这块区域用完了就把存活的对象放到,另一块空着的区域区,然后把自己清除干净,变成一块空着的区域。这样就解决了内存碎片的问题
缺点
那么这样的算法是不是太过于苛刻了,每次都需要一块空着的区域用于存放对象,牺牲掉了大量的内存。
适用范围
新生代Surivivor区域

堆中对象内存的分配策略

指针碰撞

这种分配方式其实是复制算法,标记整理中的携带的一种对象分配策略,我们如何区分什么是用过的,什么是没用过的,这时我们通过一个指针,作为一个分界点指示器,那所需要分配的内存,就仅仅是把指示器指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为"指针碰撞"(Bump the Pointer)。

空闲列表

空闲是标记清除中对对象分配的一个策略,因为标记清除中我们的内存划分的随机的,已使用内存和未使用内存相互交错,那么我们如何把他们关联起来,虚拟机针对这种交错的内存维护了一个列表,记录哪些内存块是可用的,在分配的时候找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为"空闲列表"(Free List)。

OOM异常

其实OOM在上面介绍了堆内存的划分和收集过程中,大家也应该对它有了一定的认识了,OOM异常是发生在老年代Old中的一个异常,当我们老年代中无法在存放对象的时候,就会报OOM内存溢出异常

public class HeapOomError {
    public static void main(String[] args) {
        List<byte[]> list =new ArrayList<>();
        int i=0;
        while (true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
            list.add(new byte[5 * 1024 * 1024]);
            //System.out.println("count is:"+(++i));
        }
    }
}

设置堆空间的大小

最后我们得到的结果如下

总结

总而言之我们需要的优化的GC的损耗和避免内存溢出的出现,从而提高我用户良好使用体验。

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

阅读 149

推荐阅读
架构人生
用户专栏

454 人关注
212 篇文章
专栏主页