美文网首页
JVM内存分配与垃圾回收学习笔记

JVM内存分配与垃圾回收学习笔记

作者: echoSuny | 来源:发表于2020-03-26 00:54 被阅读0次

    在Java中我们new对象的时候是放在堆内存当中。当对象不再使用的时候,就需要被回收掉,否则就会一直占据着内存空间导致无法为新的对象分配空间。幸运的是Java是自带垃圾回收机制的,一般情况下不需要我们手动去回收,而是有个专门的线程,也就是GC线程来专门回收那些没有用的对象。
    GC全称是Garbage Collection,直译就是垃圾收集。GC更多的是发生在堆内存中,方法区/元空间也是有需要回收的,但不像堆内存当中那么频繁。因此堆是重点的关注对象。
    关于GC,可以从三个方面来思考一下:
    1—什么需要回收?

    public static void main(String[] args) {
            List<Object> list = new LinkedList<>();
            while (true) {
                list.add(new Object());
            }
        }
    

    运行上面的代码,经过一段时间之后,控制台就会输出如下日志:


    日志

    从上图中我们可以看到Heap,指的就是堆。Metaspace就是元空间。Heap分为PSYoungGen(新生代)和ParOldGen(老年代)。其中PSYoungGen又划分为了eden空间,from survivor空间,to survivor空间三个部分。上图中在这三个空间后面的是每个空间的大小。可以看出三个空间的比例为Eden:From:To = 8:1:1。而PSYoungGen和ParOldGen的比例大约是1:2。这些都是默认的比例,其实可以JVM中进行配置来更改大小的:
    -Xms 堆内存初始内存分配的大小
    -Xmx 堆内存可被分配空间的最大上限
    -Xmn 设置堆内存新生代的初始大小和最大
    -XX:SurvivorRatio = 8 表示eden和survivor的比值 8:1:1 (如果设置为2则比例为2:1:1,对应的大小则为:5120K:2560K:2560K)
    另外从图片中可以看到在Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded之上在疯狂的进行FullGC,可是内存还是没有被释放。这其中就包含了一个问题就是:怎么判断一个对象是垃圾?
    (1)在JVM早期的时候使用的是引用计数法。所谓引用计数就是假如a引用了b,那么就在b上记录一次,假如为1。c也引用了b,就在b上再增加一次,那么此时b的引用数就变成了2。反之失去一个引用就减去1。当引用数为0的时候,b就变成了一个可回收的对象。但引用计数存在一个很大的缺点就是相互引用,相互引用会导致无法回收。
    (2)可达性分析。可达性分析首先需要确定GCRoot。GCRoot是一些对象,包括有类静态属性的对象,常量的对象,虚拟机栈中(本地方法表)的对象,本地方法native栈中的对象。

    public class Test {
        static Object root1 = new Object(); // 静态属性的对象
        public final static Object root2 = new Object(); //常量的对象
        public  void root() {
           Object root3 = new Object(); //虚拟机栈中的对象 至少在方法执行完之前是作为gc root
        }
    }
    

    与GC有着密不可分的还有经常提到的四种引用:强引用(StrongRefrence),软引用(SoftRefrence),弱引用(WeakRefrence),虚引用(PhantomRefrence)。
    StrongRefrence:所谓强引用就是指直接通过new关键字创建的对象。只要与GC root产生关联,即时产生OOM也不会被回收。
    SoftRefrence:软引用的特点是当程序即将发生OOM的时候,会把软引用的对象给回收掉。
    WeakRefrence:弱引用指的是当GC线程回收垃圾时,如果只有弱引用存在,那么GC线程就会进行回收。
    PhantomRefrence:虚引用则很容易被回收,只是在回收的时候会发出通知。
    2—什么时间回收?
    当新生代Eden区空间不够了就会触发一次普通GC,当老年代空间不够了则会触发Full GC
    3—怎么回收?
    关于如何回收的问题需要先了解垃圾一下回收算法。
    (1)复制算法。特点是实现简单,运行高效,没有内存碎片,需要复制内存缺点是内存利用率只有一半。


    复制算法

    假设上图中的整个内存是20M,分成两份之后就是各10M。在需要回收的时候会把不可回收的对象从左边复制到右边,并把左边的整个区域格式化。反之亦然。这种复制算法是应用在PSYoungGen区。说的具体一点就是发生在From区和To区。这也就是为什么From区和To区的比例总是1:1。至于为什么在From区和To区采用复制算法是因为程序中绝大多数的对象是不需要考虑回收的,它们都是朝生夕死的。那么只需要拿出很少一部分内存采用复制算法。假设整个PSYoungGen的大小是100M,如果整个PSYoungGen采用复制算法的话,那么需要被分为两个50M。假如红色方块数量较多,那么来回复制的话效率就比较低了。按照PSYoungGen区的默认比例8:1:1,那么Eden区占据80M,From和To分别占据10M。这样复制的话也只是在From或者To区进行复制,并且多数情况也只是复制From和To区的一部分,还不到10M,那么在这么小的一块区域采用复制算法的话,效率就不会有什么影响了。
    (2)标记清除。特点是利用率百分之百,不需要复制,缺点是有内存碎片。


    标记清除算法
    具体的清除过程就是在可回收的对象上打上标记,GC回收之后就是下面的结果。但是可以看到内存不连续,有很多碎片。当需要分配一个较大的对象,例如一个需要占据5个小方块的对象时就无法分配了。
    (3)标记整理。特点是利用率百分之百,没有碎片,缺点是需要复制。
    标记整理算法
    这种算法相较于标记清除就是多了一个复制的过程。在清除的时候会把不可回收的对象整齐的排列起来。

    其次需要了解一下堆内存的分配策略:


    堆内存分配

    (1)对象优先分配在Eden区:按照上图来说,当创建一个小于8M的对象时,会首先分配在Eden区。
    (2)大对象直接进入老年代:假设Eden区已经存在了一个6M的对象A,当我再new一个3M的对象B时,此时需要的空间为6+3=9大于Eden区的大小了,From和To同样放不下,这是就会直接把B分配到老年代。也就是所谓的空间担保。
    (3)长期存活的对象将进入老年代:假设Eden区现在存在一个0.5M的对象A,此刻发生了一次普通GC,但是A没有被回收掉,那么A就会从Eden区进入From区,同时A的对象头上会存放一个age来标记自己的年龄。如果再发生一次普通GC就从From移动到To区,并且年龄加1。以后每发生一次普通GC就会在From和To中间来回的移动(因为采用的是复制算法)。假如A有幸在age=15的时候(一般来讲是15次)还没有被回收,那么就会把A放入老年代。
    (4)动态年龄判断:假设Eden区现在存在一个大小为0.7M的对象A,From区存在一个大小为0.5M,age=6的对象。此刻发生了一次普通GC且没被回收,就需要把对象A从Eden区移动到From区。但是From区的大小只有1M,放不下A和B了,A和B就会被同时放入老年区。尽管B的年龄只有6,A的年龄只有1。这种也是基于空间担保。
    最后来了解一下收集器:

    收集器
    可以看到收集器分为单线程收集器和多线程收集器。
    单线程收集器:
    Serial 新生代 复制算法
    SerialOld 老年代 标记整理算法
    单线程清除过程
    由于每次进行GC都需要暂停所有用户线程,如果垃圾回收时间过长,则会造成程序卡顿。
    多线程收集器:
    ParNew 新生代 复制算法 并行
    CMS 老年代 标记清除算法 并行 并发
    ParallelScavenge 新生代 复制算法 并行
    ParallelOld 老年代 标记整理算法 并行
    普通多线程清除过程
    和上图中的区别就是多个GC线程并行收集。清理速度肯定是快于单线程收集器的。
    CMS清理过程
    这个是CMS收集器清理的示意图。首先会做一次初始标记,然后跟随用户线程一起并发标记,随后暂停用户线程去重新标记,接着并发清理。虽然也经历了两次暂停,但相比暂停去清理的话,初始标记和重新标记暂停的时间是短的多的。另外由于CMS采用的是标记清除算法,速度快。所以CMS多应用于移动互联网。由于并发收集的时候,用户线程也在运行,所以当收集结束的时候,其他的线程可能会产生浮动垃圾,那么就需要下一次GC的时候去进行回收。

    G1 跨新生代和老年代 标记整理+化整为零
    jdk1.7才引入,采用分区回收的思维,把新生代以及新生代里的Eden,From,To和老年代全部打散,所以才可以在新生代和老年代中都是用同一个垃圾收集器。

    相关文章

      网友评论

          本文标题:JVM内存分配与垃圾回收学习笔记

          本文链接:https://www.haomeiwen.com/subject/mdnwyhtx.html