前几天看到的一篇垃圾回收--GC文章,很不错,做个摘抄

前几天看到的很不错博文,就摘抄了一些,有的图片显示不出来,我就给删掉了。有兴趣的话看原文。

1.Error

  • Error是程序无法处理的错误,表示运行应用程序中较严重的问题,是运行时JVM出现的问题。这些错误是不可检查的,因为他们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况,比如 OutOfMemoryError 和 StackOverFlowError 。
  • 运行时数据区包括两部分,由所有线程共享的数据区和线程隔离的数据区组成,只有程序计数器是不会发生OutOfMemoryError情况的区域。如果应用程序执行的是Java方法,那么这个计数器记录的就是虚拟机字节码指令的地址,如果正在执行的是Native方法,这个计数器值为空Undefined
  • 其他区域:方法区、虚拟机栈、本地方法栈和堆都是可能发生OutOfMemoryError的区域
  • 虚拟机栈:如果线程请求的栈深度大于虚拟机栈所允许的深度,将会出现StackOverFlowError异常,如果虚拟机动态扩展无法申请到足够的内存,将出现OutOfMemoryError。
  • 本地方法栈:如上
  • 堆:Java堆可以处于物理上不连续,逻辑上连续,就像我们的磁盘空间一样,如果堆中没有内存完成实例分配,并且堆无法扩展时,将会抛出OutOfMemoryError
  • 方法区:方法区无法满足内存分配需求时,将抛出OutOfMemoryError

2.JVM运行时数据区

  • 虚拟机栈:描述的是方法执行时的内存模型,线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈帧,主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈和出栈时机很明确,所以此块区域不需要GC。
  • 本地方法栈:与虚拟机栈功能非常类似,主要区别在于虚拟机栈为虚拟机执行java方法时服务,而本地方法栈为虚拟机执行本地方法时服务。此块区域也不需要GC。
  • 程序计数器:线程私有,可以看作是当前线程执行字节码的行号指示器。(为什么要记录指令地址,java虚拟机的多线程是通过线程轮流切换并分配处理器的时间来完成的,在任何一个时刻,一个处理器只会处理一个线程,如果这个线程被分配的时间片执行完了,该线程会被挂起,处理器会切换到另一个线程执行,当下次轮到被挂起的线程时,通过记录在程序计数中的行号即可知道上次执行到那里)程序计数器的主要作用是记录线程运行时的状态,方便线程被唤醒时能从上一次被挂起时的状态继续执行。程序计数器是 唯一一个没有规定任何OOM情况的区域,所以也不需要GC。
  • 本地内存:线程共享区域,java8中,本地内存(堆外内存)包含元空间和直接内存(在java8之前有个永久代的概念,实际上指的是HotSpot虚拟机上的永久代,它用永久代实现了JVM规范定义的方法区功能,主要存储类的信息、常量、静态变量、即时编译器编译后的代码等等,这部分由于是在堆中实现的,受GC管理,不过由于永久代有-XX:MaxPermSize的上限,如果将大量类信息放入永久代或将字符串放入永久代的常量区,很容易造成OOM,所以在java8将方法区的实现移到了本地内存中的元空间)所以也不需要GC。
  • 堆:堆是GC发生的区域,对象实例和数组都是在堆上分配的。

3.识别垃圾

  • 引用计数法:对象被引用一次,在它的对象头上加一次引用计数,如果对象的引用次数为0,表示没有被引用,则此对象可被回收。(循环引用问题,)
  • 可达性算法:以一系列叫做GC Root的对象以起点出发,引用他们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个节点。。。这样通过GC Root串成的一条线叫做引用链,直到所有的节点都遍历完毕,如果相关对象不在任意一个以GC Root为起点的引用链中,则这些对象会被判断为垃圾,被GC回收。
  • GC Root可以是虚拟机栈帧中的本地变量表中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法中Native方法引用的对象。(本地方法就一个java调用非java代码的接口,java通过JNI调用本地方法,本地方法以库文件的形式存放)
    (a, b 对象可回收,就一定会被回收吗?并不是,对象的 finalize 方法给了对象一次垂死挣扎的机会,当对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收)

4.垃圾回收算法

  • 标记-清除算法:先根据可达性算法标记出相应的可回收对象,然后对可回收对象进行回收。虽然不需要做移动数据的操作,但是会产生内存碎片。
  • 标记-整理算法:在标记-清除算法的基础上添加了一个整理的过程,即将所有的存活对象都往一端移动,紧邻排列,再清理掉另一端的所有区域。虽然解决了内存碎片,但是每一次GC都要频繁的移动存活对象,效率低。
  • 复制算法:把堆等分成两块区域,A、B,区域A负责分配对象,区域B不分配,对区域A使用标记-清除算法把所有能回收的对象全部回收,然后把区域A中存活的对象都复制到区域B,最后把A区域的对象全部清理掉释放空间。虽然解决了内存碎片,但是空间只可用一半,还需要频繁移动存活对象,效率低。
  • 分代收集算法:根据对象存活周期的不同把堆分为新生代和老年代,java8之前还有个永生代,默认比例为1:2。新生代又分为Eden区、From Survivor(S0)区和To Survivor(S1)区,三者的比例为8:1:1,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,把新生代发生的GC叫做Minor GC,老年代发生的GC叫做Full GC。
    由于大部分对象在很短的时间内都会被回收,对象一般分配在Eden区,当Eden区将满时,出发Minor GC。经过Minor GC后只会有少部分对象会存活,他们会被移到S0区(因为在Eden区出发的Minor GC把大部分对象都回收了,只留下少量存活的对象,此时他们移到S0或S1绰绰有余,所以Eden区域远大于S0和S1),同时对象年龄加一,对象的年龄就是发生Minor GC的次数,最后把Eden区的对象全部清理释放空间。
    当触发下一次Minor GC时,会把Eden区的存活对象和S0中的存活对象一起移到S1。Eden和S0的对象存活年龄加一,同时清空Eden和S0的空间

    img

    若再触发下一次Minor GC,则重复上一步,只不过是从Eden、S1区将存活对象复制到S0区,每次垃圾回收,S0、S1角色互换,都是将Eden,S0(S1)的存活对象移动到S1(S0)。在Eden区的垃圾回收采用的是复制算法。
    当对象的年龄达到了我们设定的阈值,则会从S0(S1)晋升到老年代

    img

    当某个对象分配需要大量的连续内存时,此对象的创建不会分配到Eden区,会直接分配到老年代,因为如果把大对象分配到Eden区,Minor GC后再移到S0、S1会有很大开销,也很快会占满S0、S1区。
    当S0(S1)区相同年龄的对象大小之和大于S0(S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。
    空间分配担保:在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。
    Stop The World:如果老年代满了,会触发Full GC,Full GC会同时回收新生代和老年代,即对整个堆进行GC,他会导致StopTheWorld(STW),STW就是在GC期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。所以把新生代设置成 Eden, S0,S1区或者给对象设置年龄阈值或者默认把新生代与老年代的空间大小设置成 1:2 都是为了尽可能地避免对象过早地进入老年代,尽可能晚地触发 Full GC。由于 Full GC(或Minor GC) 会影响性能,所以我们要在一个合适的时间点发起 GC,这个时间点被称为 Safe Point。
    老年代进行的GC一般采用的是标记-整理算法来进行回收。

5.垃圾回收器

  • Serial收集器:Serial 收集器是工作在新生代的,单线程的垃圾收集器,单线程意味着它只会使用一个 CPU 或一个收集线程来完成垃圾回收,不仅如此,还记得我们上文提到的 STW 了吗,它在进行垃圾收集时,其他用户线程会暂停,直到垃圾收集结束,也就是说在 GC 期间,此时的应用不可用。看起来单线程垃圾收集器不太实用,不过我们需要知道的任何技术的使用都不能脱离场景,在 Client 模式下,它简单有效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 单线程模式无需与其他线程交互,减少了开销,专心做 GC 能将其单线程的优势发挥到极致,另外在用户的桌面应用场景,分配给虚拟机的内存一般不会很大,收集几十甚至一两百兆(仅是新生代的内存,桌面应用基本不会再大了),STW 时间可以控制在一百多毫秒内,只要不是频繁发生,这点停顿是可以接受的,所以对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器。
  • ParNew收集器:ParNew收集器是Serial收集器的多线程版本,其他像收集算法,STW,对象分配规则,回收策略与Serial收集器一样,底层代码也共用了很多。ParNew主要工作在Server模式,除了Serial收集器,只有它可以与CMS收集器配合工作。
  • Paraller Scavenge收集器:Parallel Scavenge 收集器也是一个使用复制算法多线程,工作于新生代的垃圾收集器,CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。
  • Serial Old收集器:Serial Old 是工作于老年代的单线程收集器,此收集器的主要意义在于给 Client 模式下的虚拟机使用

    img

  • Paraller Old收集器:Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,两者组合示意图如下,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标。

    img

  • CMS收集器:CMS 收集器是以实现最短 STW 时间为目标的收集器,很重视服务的响应速度。我们之前说老年代主要用标记整理法,而 CMS 虽然工作于老年代,但采用的是标记清除法,主要有以下四个步骤:初始标记–并发标记–重新标记–并发清除。从图中可以的看到初始标记和重新标记两个阶段会发生 STW,造成用户线程挂起,不过初始标记仅标记 GC Roots 能关联的对象,速度很快,并发标记是进行 GC Roots Tracing 的过程,重新标记是为了修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这一阶段停顿时间一般比初始标记阶段稍长,但远比并发标记时间短。
  • G1收集器:G1 收集器是面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器。G1在STW上建立了可预测的停顿时间模型,G1会将停顿时间控制在用户设定的停顿时间以内。因为G1各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的 Region,每个Region占有一块连续的虚拟内存地址。
    G1的工作步骤:初始标记–并发标记–最终标记–筛选回收。G1 从整体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的

奉上原文链接:https://blog.csdn.net/weixin_41385912/article/details/104090594

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值