一对一视频网站建设,腾讯云 安装wordpress,wordpress 加入搜索,网站正在建设中...目录
一、GC的作用
申请变量的时机销毁变量的时机
内存泄漏
内存溢出(oom)
垃圾回收的劣势
二、GC的工作过程
回收垃圾的过程
第一阶段#xff1a;找垃圾/判定垃圾 方案1#xff1a;基于引用计数(非Java语言)
引用计数方式的缺陷
方案2#xff1a;可达性分析…目录
一、GC的作用
申请变量的时机销毁变量的时机
内存泄漏
内存溢出(oom)
垃圾回收的劣势
二、GC的工作过程
回收垃圾的过程
第一阶段找垃圾/判定垃圾 方案1基于引用计数(非Java语言)
引用计数方式的缺陷
方案2可达性分析(基于Java语言)
GCRoots是哪些变量(3类)
第二阶段回收垃圾(释放内存)
策略1标记-清除策略 策略1存在问题分析:内存碎片)
策略2复制算法
复制算法存在问题分析
策略3标记——整理
分代回收
三、垃圾收集器有哪些
第一类Serial收集器Serial Old收集器(串行收集)
第二类ParNew收集器Parallel Old收集器Parallel Scavenge收集器(并发收集)
第三类CMS收集器
步骤1找到GCRoots(会引发STW)
步骤2并发标记
步骤3重新标记(会引发STW)
步骤4回收内存
第四类G1收集器 一、GC的作用 GC:全称是Garbage Clean(垃圾回收)。我们平时写代码的时候经常会申请内存。例如
创建变量、new对象、加载类... 但是由于内存空间是有限的因此就需要有借有还。 如下代码就是申请了两个变量一个a另外一个是object
int a3;Object objnew Object() 申请变量的时机销毁变量的时机 申请一个变量申请内存的时机是确定的。就是new或者int a...这种。但是这个变量什么时候不需要使用了那这个时期就不确定了。 例如内存释放得偏早如果还想要使用obj对象但是如果这一个对象被回收了那这样就显得不合理了。 又或者内存的释放比较偏迟对象一直占着坑位。 对于内存什么时候被释放这个问题 不同的语言有不同的处理方式。 对于C语言程序没有提供垃圾回收机制。因此当内存需要释放的时候必须由程序员手动进行释放(调用free函数)因此就会引入一个臭名昭著的问题那就是内存泄漏。 内存泄漏 如果申请的内存越来越多那么就意味着可用的内存越来越少最终无内存可用了。这种现象就叫做内存泄漏。 虽然垃圾回收可以让开发的程序员专注于设计业务上面的代码无需关心内存泄露的问题但是仍然有一定的劣势。 提到内存泄露那么我们再谈一下一个和它容易混淆的概念——内存溢出。但是和上面讨论的话题没有关系 内存溢出(oom)
程序在申请内存的时候没有足够的内存提供给申请者使用。这种现象就被称为内存溢出。
例如给一个int类型空间的大小却存储一个long类型的数据这样就会导致内存溢出。 垃圾回收的劣势 1、引入了额外的开销消耗资源更多了 2、可能会影响程序的流畅运行垃圾回收经常会出现STW(stop the work)问题。 二、GC的工作过程
回收的是什么样的对象
在上一篇文章当中我们提到了JVM的内存区域划分主要分为4个部分 程序计数器、栈、堆、方法区。 对于栈区只要方法返回之后就会自动从栈上面消失了不需要GC。
对于堆区就很需要GC了因为堆区当中存放的大量都是new出来的对象。
我们来画一张图描述一下根据内存使用与否的图 因此需要回收的对象都是一些没有使用但是同时也占用着内存的对象。 回收垃圾的过程
垃圾回收的过程分为两大阶段
第一阶段找垃圾/判定垃圾 方案1基于引用计数(非Java语言) 针对每一个对象都引入一小块的内存保存这一个对象有多少个引用指向它。 例如(此时有两个引用都指向new Test()对象)
//t1引用指向new Test()对象
Test t1new Test();
//t2引用指向new Test()对象
Test t2t1; 那么此时在new Test()当中就会有一个引用计数器显示指向这个对象的引用个数为2。 那么当引用计数为0的时候也就意味着此时没有引用指向这个对象了需要GC对于这一个对象进行回收操作。 什么时候引用计数为0呢下面举一个例子 private static void func2() {//让t1指向new Test1()对象Test1 t1new Test1();//让t2指向new Test1()对象Test1 t2t1;} 在一个方法当中两个引用(t1,t2)同时指向了new Test1()对象。 当调用func2()方法的时候t1和t2引用会保存在func2()方法的栈帧上面。两个引用同时指向了堆上面的new Test1()对象。 当func2()调用结束之后会从栈帧上面消失那么t1和t2引用也会随之消失。 那么也就意味着new Test2()这一个对象没有引用指向它了认为它是一个垃圾也就会被回收。 引用计数方式的缺陷
缺点1空间利用率比较低 每一个new的对象都必须要搭配一个计数器来记录几个引用。引用计数器的大小为4个字节但是如果一个对象除了引用计数器以外的部分本身也就只有4个字节大小那么就意味着比较浪费空间。 缺点2会有循环引用的问题
下面来举一个例子说明一下什么是循环引用问题:
首先创建一个Test类在内部有一个属性就是Test tnull
然后在测试类当中创建这一个类的实例对象 class Test {Test t null;
}/*** author 25043*/
public class Test2 {public static void main(String[] args) {Test t1 new Test();Test t2 new Test();}
}
到这一步的时候来画一个引用——对象的指向图
然后接下来执行下面的代码 public static void main(String[] args) {//对象1Test t1 new Test();//对象2Test t2 new Test();t1.t t2;t2.t t1;} 到了这一步再画一下引用指向的图:(把t2引用指向的对象赋给了对象1的t属性、把t1引用指向的对象赋给了对象2的t属性) 到这一步的时候 对象1有两个引用指向(t1、t2.t) 对象2有两个引用指向(t2、t1.t)。 接下来令t1null,t2null。
public static void main(String[] args) {//对象1Test t1 new Test();//对象2Test t2 new Test();t1.t t2;t2.t t1;t1 null;t2 null;}
那么此时可以认为
t1的指向为null并且t2的指向为null。 那么对应的指向对象1的引用减少了一个只剩下(t2.t) 同时指向对象2的引用也减少了一个只剩下(t1.t) 两个对象的引用计数器各自减少为1。 由于引用计数不为0也就是两个对象互相引用。那么这两个对象无法被回收。但是外部的引用又无法访问这两个对象。因此这两个对象就永远无法被回收,也永远无法被使用。这样也就出现了内存泄漏。 由于上述的两个缺点因此引入了方案2(基于Java语言的解决方案基于可达性分析) 方案2可达性分析(基于Java语言) 通过一个额外的线程定期地针对整个内存空间的对象进行扫描。 有一些起始的位置(称为GCroots)然后类似于深度优先搜索的方式把可以访问到的对象都标记一遍。那么带有标记的对象就是可达的。没有被标记的对象那就是垃圾。这样就很好地解决了对象不可达的问题。避免了两个对象相互引用、但是没有外部引用指向的问题 尽管可达性分析方法可以有效解决引用循环的问题但是如果一个程序当中的对象特别多那么也一定会造成比较大的性能损耗因为整个搜索的过程也是比较消耗时间的。 GCRoots是哪些变量(3类) 第一类栈上的局部变量 第二类常量池当中的引用指向的变量 第三类方法区当中的静态成员指向的对象。 第二阶段回收垃圾(释放内存) 回收垃圾主要分为三种策略 策略1标记-清除策略 策略2复制算法 策略3标记-整理策略 下面将分别介绍这三种策略 策略1标记-清除策略 标记就是可达性分析的过程。例如在一次搜索当中发现了以下几个部分是垃圾。清除就是直接释放内存。 策略1存在问题分析:内存碎片) 此时如果直接释放虽然内存的确还给了操作系统了但是内存还是离散的也就不是连续的这样带来的问题就是内存碎片影响程序的运行效率。 策略2复制算法
为了解决内存碎片引入的复制算法。如下图把内存一分为二
然后把正常的对象(没有被标记为垃圾的对象)的拷贝到令一半。
最后把左侧的空前全部释放掉。此时内存碎片问题就迎刃而解了。 复制算法存在问题分析
复制算法有效解决了上述的内存碎片问题但是仍然有以下的两个问题没有解决 问题1内存空间利用率低只能利用一半的空间。 问题2开销大。如果垃圾比较少那么这种搬运得不偿失。 策略3标记——整理
这个过程就是把正常的对象没有被标记为垃圾的往前搬运。最后释放掉最后面的内存。
下图当中灰色部分的为垃圾。 但是这个拷贝也是有开销的。 上述的3种方案虽然可以解决问题但是都有缺陷。因此实际上JVM当中会结合多种方案一起来实现并不是采用单一的策略。这种方式就是分代回收。 分代回收 分代回收其实就是针对对象进行分类根据对象的年龄进行回收。 对象的年龄每熬过GC的一轮扫描没有被回收那么对象的年龄就1岁。这个年龄存储在对象头当中。 大致是这样的一个过程 存储对象的内存区域大致就被分为了两部分新生代和老年代 在新生代当中分为了两部分伊甸区和幸存区。一共有2个幸存区 步骤1对于刚刚产生的对象都会被存放在伊甸区。 步骤2如果熬过一轮GC那么就会被拷贝到幸存区,(应用了复制算法)。但是大部分对象都熬不过一轮的GC。 步骤3在后续的几轮GC当中幸存区的对象就在两个幸存区当中来回拷贝。此处也是采用了复制算法来淘汰掉一些对象。 步骤4经过了多轮的GC后如果一个对象还是没有被淘汰那么就会被放入老年代。此时就认为这个对象存活的可能性就比较大了。对于老年代的对象来说GC扫描的次数就远远低于新生代了。同时老年代当中采用的就是标记——整理的方式来回收。 但是有一种特殊的情况就是当一个对象特别大也就是占用内存比较多的时候无需经过多轮GC的扫描就可以直接进入老年代了因为回收这一类的对象比较消耗性能。 三、垃圾收集器有哪些
第一类Serial收集器Serial Old收集器(串行收集) 这两个垃圾收集器是串行收集的。那么也就意味着在垃圾的扫描和释放的时候其他的业务线程都需要停止工作。这种方式扫描得慢、释放得慢、也产生了严重的STW。 第二类ParNew收集器Parallel Old收集器Parallel Scavenge收集器(并发收集) 这三个收集器、引入了多线程的方式来进行回收也就是并发收集。并不影响业务线程执行业务代码。 第三类CMS收集器
执行步骤
步骤1找到GCRoots(会引发STW)
找到GCRoots但是会引起短暂的STW。 步骤2并发标记
和业务线程一起执行。 步骤3重新标记(会引发STW) 步骤4回收内存
这个步骤也是和业务线程一起执行的。 第四类G1收集器
把整个内存分成了很多个小的区域(Region)
给这些Region进行了不同的标记。有一些region存放新生的对象有一些存放老年代的对象。
然后一次扫若干个Region但不是全部扫完