垃圾回收机制

2020-03-04  本文已影响0人  瀚海网虫

1. 谁需要GC

栈:不需要 ,不是共享的对象
堆:需要
方法区/元空间:也需要。 元空间(独立于JVM的内存空间,只受限于系统本身的内存)

2. GC 的一般参数

-Xms 堆区内存初始内存分配的大小
-Xmx 堆区内存可被分配的最大上限
打印GC详情
-XX:+PrintGCDetails
当堆内存空间溢出时输出堆的内存快照
-XX:+HeapDumpOnOutOfMemoryError
堆中参数配置:
新生代大小: -Xmn20m 表示新生代大小20m(初始和最大)

-XX:SurvivorRatio=8 表示Eden和Survivor(2)的比值,
缺省为8 表示 Eden:From:To= 8:1:1
2 Eden:From:To= 2:1:1

3. GC如何判断对象的存活

可达性分析:
在Java, 可作为GC Roots的对象包括:

方法区: 类静态属性的对象;
方法区: 常量的对象;
虚拟机栈(本地变量表)中的对象.
本地方法栈JNI(Native方法)中的对象。

public class GCRootTest {
    Object o = new Object();
    static Object gcRootTest1 = new Object();
    final static Object getGcRootTest2 = new Object();
    public static void main(String[] args) {
        Object obj1 = gcRootTest1;  //可达  = 在对象中是引用,传递右侧对象的地址
        Object obj2 = obj1;         //可达
        Object obj3 = obj2;         //可达
    }
    public void test() {
        //不可达 (方法运行完后回收)
        Object obj4 = o;
        Object obj5 = obj4;
        Object obj6 = obj5;
    }
    //本地变量表中引用的对象
    public void stack() {
        Object obj7 = new Object();  //new 出对象,属于虚拟机栈中本地变量表的对象
        Object obj8 = obj7;
        // obj8 在方法(运行完)出栈前,都是可达的。 (跟test方法,似乎也没有区别)
    }

4. 引用类型

    public static void main(String[] args) {
        Man man = new Man("peter");
        SoftReference<Man> sorfMan = new SoftReference<>(man);
        man = null;
        System.out.println(sorfMan.get());
        System.gc();
        System.out.println("-------gc ------");
        System.out.println(sorfMan.get());
        List<byte[]> list = new LinkedList<>();
        try {
            for (int i = 0; i < 1000; i ++) {
                System.out.println("----------" + sorfMan.get());
                list.add(new byte[1024 * 1024]);
            }
        } catch (Throwable e) {
            System.out.println("Exception ------" + e.getMessage() + ",  sorfMan  " + sorfMan.get());
        }
    }
Man name peter
[GC (System.gc()) [PSYoungGen: 1723K->496K(2560K)] 1723K->576K(9728K), 0.0009803 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 80K->472K(7168K)] 576K->472K(9728K), [Metaspace: 3150K->3150K(1056768K)], 0.0049414 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
-------gc ------
Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
[GC (Allocation Failure) --[PSYoungGen: 1162K->1162K(2560K)] 7779K->7779K(9728K), 0.0007110 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1162K->1032K(2560K)] [ParOldGen: 6616K->6603K(7168K)] 7779K->7636K(9728K), [Metaspace: 3237K->3237K(1056768K)], 0.0048426 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) --[PSYoungGen: 1032K->1032K(2560K)] 7636K->7636K(9728K), 0.0004065 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 1032K->1031K(2560K)] [ParOldGen: 6603K->6587K(7168K)] 7636K->7618K(9728K), [Metaspace: 3237K->3237K(1056768K)], 0.0047505 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid53056.hprof ...
Heap dump file created [8589024 bytes in 0.017 secs]
Exception ------Java heap space,  sorfMan  null
Heap
 PSYoungGen      total 2560K, used 1167K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 2048K, 56% used [0x00000007bfd00000,0x00000007bfe23c50,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 7168K, used 6587K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
  object space 7168K, 91% used [0x00000007bf600000,0x00000007bfc6ec08,0x00000007bfd00000)
 Metaspace       used 3286K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0
    public static void main(String[] args) {
        Man man = new Man("peter");
        WeakReference<Man> weakMan = new WeakReference<>(man);
        man = null;
        System.out.println(weakMan.get());
        System.gc();
        System.out.println("-------gc ------");
        System.out.println(weakMan.get());
    }
Man name peter
-------gc ------
null
Process finished with exit code 0

5. 回收算法

5. 1复制算法(Copying)

image.png

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

注意这里还是涉及到了内存复制,如果每次不能回收的内存较多,这样效率会降低很多。

  1. 空间担保(Handle Promotion)
    新生代内存不够时,会放老年代,即老年代是新生代的空间担保。
  2. 内存分配比例(8:1:1)
    新生代大部分对象,会很快会回收掉,”朝生夕死“。复制算法,会预留两个交换区,分别是 From Survivor ,To Survivor ,比例基本为1:1。占用大概20%空间,相应的Eden 占用80%。

5. 2 标记-清除算法(Mark-Sweep)

image.png

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

5. 3 标记-整理算法(Mark-Compact)

image.png

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

5.4 堆内存分配策略对象优先在Eden分配

对象优先在Eden分配,如果说Eden内存空间不足,就会发生Minor GC。

大对象:大对象直接进入老年代,需要大量连续内存空间的Java对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。
-XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。

长期存活的对象:将进入老年代,默认15岁(反复复制15次,每次年龄+1)
-XX:MaxTenuringThreshold调整。

动态对象年龄判定:为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。

6. JVM的具体策略 --- 分代收集

image.png image.png image.png

命令行参数查看:jps -v


image.png

并行:垃圾收集的多线程的同时进行。
并发:垃圾收集的多线程和应用的多线程同时进行。

注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)
垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

细节

初始标记-短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记-和用户的应用程序同时进行,进行GC RootsTracing的过程
重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
-XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用CMS
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

G1


image.png

-XX:+UseG1GC

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

内存布局:在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
ν 新生代GC
回收Eden区和survivor区,回收后,所有eden区被清空,存在一个survivor区保存了部分数据。老年代区域会增多,因为部分新生代的对象会晋升到老年代。
ν 并发标记周期
初始标记:短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,产生一个全局停顿,都伴随有一次新生代的GC。
根区域扫描:扫描survivor区可以直接到达的老年代区域。
并发标记阶段:扫描和查找整个堆的存活对象,并标记。
重新标记:会产生全局停顿,对并发标记阶段的结果进行修正。
独占清理:会产生全局停顿,对GC回收比例进行排序,供混合收集阶段使用
并发清理:识别并清理完全空闲的区域,并发进行
ν 混合收集
对含有垃圾比例较高的Region进行回收。
G1当出现内存不足的的情况,也可能进行的FullGC回收。

G1中重要的参数:
-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。
-XX:ParallerGCThreads:设置GC的工作线程数量Stop The World现象
GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。

7 举个栗子

ArrayList 源码中,remove 方法,会调用fastRemove 方法

 private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

注意上面的 // clear to let GC do its work
数组中存储的元素,属于GC Roots的 对象 。remove 方法执行完,不会被GC 回收,所以这里手动释放了。

上一篇 下一篇

猜你喜欢

热点阅读