一 什么是GC,为什么要发生GC?
GC是garbage collection的缩小,简称垃圾回收。因为程序在运行过程中经常会出现内存溢出,内存泄漏的问题。
这里简单讲讲内存溢出和内存泄漏:
内存溢出:当前向内存申请的空间大于内存能提供的最大空间
比如申请栈的深度大于栈的最大深度,栈扩展的时候需要的内存不够,
这样的情况都会发出内存溢出,
内存泄漏:申请资源的时候开辟一个内存空间,资源使用完后并没有释放掉,
仍占据那一片内存,相当于内存有但是使用不了,就叫内存泄漏。一次两次泄漏没什么,
泄漏次数太多导致系统内存不够用,就很可能产生内存溢出了。
GC就是HotSpot虚拟机提供的为解决内存泄漏和内存溢出的一种机制,简称垃圾回收机制。发生GC的条件:回收区域的内存达到上限了,需要清除没有任何引用的对象。
二 JVM会在哪些地方进行回收?
垃圾回收主要发生在堆上(head区)的新生代,老年代,永久代(如果还存在的话),其次就是在方法区上。
四 垃圾回收机制的主要流程?
垃圾回收在广义来说分为新生代和老年代,大小为1:2,新生代有三个区,一个Eden区,两个survivor区,大小为8:1:1,每次发生垃圾回收就是在一个Eden区和一个survivor区,然后把存活的对象放入到另一个survivor区,如果survivor区存不下了,利用分配担保机制,把survivor存不下的对象放入老年代。老年代还会存储占用内存较大的大对象,长期存活的对象也会进入老年代中。当老生代的内存也满了,新生代对象再次进入老生代时会报OOM异常(内存溢出)。新生代垃圾回收频率高,速度块;方法区一般称为永久代,永久代中的对象一般不会被回收,永久代主要废弃常量和无用类。当定义String abc这个常量,就会被放入运行时常量池,当没有对像对此进行引用,则当作废弃常量回收。如何判断无用类:
(1) 该类的所有实例都已经被回收,java堆中不存在类的任何实例
(2)加载类的classloader已经被回收
(3)该类对象的java.lang.class对象没有在其他其他地方引用,无法在任何地方通过反射访问类的方法。
-
发生GC的时机
大多数情况下,对象在新生代进行分配,主要分配在Eden区和一块survivor区,当Eden区满了后就会发生一次minor gc,回收新生代空间,然后把存活的对象放入另一块survivor区,如果另一块survivor放不下,借助依赖担保机制进入老年代。当老年代空间满了,会触发major gc或full gc,只要老年代的连续空间大小大于新生代对象总大小就进行minor gc,否则进行full gc。 -
major gc,full gc
我的理解是,major gc收集的是老年代区域,full gc是收集整个堆空间。
当再一次minor gc后晋升的空间大于老年代剩余的最大连续空间时触发一次major gc或full gc,这里我有一个问题(minor gc触发major gc还是full gc?)
当perm gen(永久代),如果存在的话,内存不够用时会触发full gc,对整个堆进行一个收集。
然而,我们如何判断哪些对象是存活的,哪些是死亡的呢?
这里采用的是引用计数法和可达性分析法来判断
-
引用计数法:
为每一个对象建立一个引用计数器,每当有一个地方引用,引用计数器+1,引用失效,引用计数器-1,最后回收的对象就是引用计数器为0的对象。但是存在两个对象间循环引用的问题。 -
可达性算法:
从一些能够作为gc roots的节点开始,把包含所有对象的节点走一遍,称为一条链路。如果有的节点没有通向roots节点的链路,则称该节点是不可达的。回收的对象就是这些不可达的节点。
GC Roots节点:
虚拟机栈中的对象,
本地方法栈中的对象,
方法区中的类静态属性引用的对象,
方法区常量引用对象
当我们判断哪些对象是可回收的,就可以上主菜了(如何进行回收,采用何种算法)
可以采用以下算法进行回收,标记清除/标记整理法,复制算法。
-
标记清除/标记整理法:
首先标记出来要回收的对象,标记完之后对标记的对象进行回收。但是标记清除会产生大量的内存碎片,
标记整理就是在标记清除的基础上通过消除内存碎片来改进的算法,每次清除后,如果有内存碎片则向左移动来消除碎片。 -
复制算法
每次采用两块内存,一次只收集其中一块的内存区域,首先把第一块的存活对象复制到第二块内存,再把第一块的死亡对象释放掉,下一次回收第二块的内存,复制存活对象到第一块去。主要发生在堆中的新生代中,堆eden区和一块survivor区进行收集,把存活对象放入第二块survivor中去。 -
分代收集算法:
把内存分为新生代和老年代,对新生代采用复制算法,而老年代没有多余内存区域,没有分配担保机制,只能采用标记清除或标记整理算法。
以上便是四种垃圾回收的算法,说到这里可能差不多了,但是我们还是要明确一下,垃圾回收这个工作是谁来做的呢?垃圾回收器? 没错,就是它干的。
垃圾回收器主要有两种,CMS收集器和G1收集器
- CMS收集器
cms收集器以最短停顿时间为目标,是基于标记清除算法的收集器,主要回收步骤:
(1) 初始标记:标记gc roots能直接关联的对象,速度很快
(2) 并发标记:进行gc roots tracing过程,标记roots不可达的对象,相对耗时
(3)再次标记:修正并发标记期间因用户程序运行而产生的标记改变,速度快
(4) 并发清除:对标记的对象统一进行处理,比较耗时
初始标记和再次标记是stop the world,停止一切用户线程
并发标记和并发清除是与用户线程并发执行的
缺点:
(1)对cpu资源敏感,因为cms收集过程会占用用户线程,如果用户线程对cpu的资源要求较高,会降低用户程序的效率
(2)无法处理浮动垃圾又会导致另一次的full gc的出现:由于清除过程是与其他线程并发执行,在清除过程中可能产生大的内存对象,但是标记已经完成,只能等待下一次gc回收。当内存空间不足时,又会开始新的gc。
(3)因为采用的是标记清除法,会产生内存碎片。
- G1收集器
回收过程与CMS大致一样,采用的都是可达性算法进行标记。
但是G1有着独特的特点:
(1) 并发与并行:垃圾回收线程能与用户线程并发执行
(2) 采用的是标记整理算法,不会产生内存碎片
(3) 分代收集:能根据对象的存活时间采用不同的收集算法
(4) 可预测的停顿:除了追求低停顿外,还能建立可预测的时间停顿模型,便于监控
二者的区别:
(1)cms会产生内存碎片,g1不会产生内存碎片
(2) cms不能建立可预测的时间停顿模型,g1可以
讲到这里,我们把垃圾回收的主要区域,大致的回收过程,如何判断对象是否可回收,回收算法,常见垃圾收集器都讲了一轮,让大家知道了在虚拟机的对象如何管理和回收来保证内存的正常可用。但是假如一个对象刚进入虚拟机,它是如何分配的呢,这就是jvm的内存分配策略呢。
- 内存分配策略
新对象直接在新生代的eden区进行分配,如果eden区满了,触发一次minor gc。将eden区和一块survivor区的存活对象放入另一块survivor对象中,如果survivor区域的空间存储不下所有复制过来的存活对象,就会凭借依赖担保机制借助老年代的空间进行存储。
如果新对象是一个大对象,即size>-XX:PretenureSizeThreshold,就会直接存入老年代。
这就是jvm的内存分配策略了,是不是感觉特别简单。试着想想,利用我们现在掌握的知识,能否在虚拟机的层面优化我们的程序。
现在有了垃圾回收机制,成为解决内存溢出的有效办法,但是频繁的gc,会占用大量cpu资源,导致程序的效率降低,频率较低的gc,会导致内存泄露问题。只有适当的回收频率既有效的提高内存利用率,又不会消耗过多的cpu资源。
最后,我们再讨论几个问题。
(1) 分代收集如何判断对象年龄:
jvm会给每个对象一个年龄计数器,当一个对象进行一次minor gc后还存活,并且能够被survivor空间容纳的,就让该对象的年龄+1,当一个对象的年龄达到15后,说明该对象的存活时间很长,就会让他进入老年代中。
(2)为什么新生代中要有survivor区:
为了避免多次发生full gc。如果新生代中没有survivor区,eden区的对象经历minor gc后,存活对象会大量进入老年代,导致老年代被快速填满,从而发生full gc,因为full gc比minor gc回收的区域大,所有耗时比较长。
(3)为什么要有两块survivor区:
如果只有一块suvivor区,当eden区满的时候,发生一次minor gc,然后把存活的对象放入另一块survivor区,但是在下一次的gc时,survivor可能会产生内存碎片,但如果是采用两个survivor区,可以把eden区域和其中一块survivor存活对象通过复制算法存入另一块survivor,然后回收eden区和survivor区。
貌似这样就可以解决问题了,但是宝宝心中有个疑问,为什么在一块survivor中利用标记整理算法来消除内存碎片,是因为这个方式比复制更费时???算了,下次找个面试官问问吧。
好的,今天的分享就到这里了,感觉还行吧!!