JVM第十天-GC-GC基础知识

什么是垃圾?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

当一个对象,没有任何引用指向它,这个对象就可以被称作垃圾。

JAVA和C++内存管理的对比

在这里插入图片描述

怎么定位垃圾?

定位垃圾的算法有两种:

1、引用计数算法(ReferenceCount)

也就是对象身上有个计数器,记录着有几个对象引用着它。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

当计数器为0的时候,这个对象就被视为一个垃圾了。

不能解决的问题

循环引用

在这里插入图片描述
引用计数,不能解决这种,一堆垃圾,没有外部指向它们,但它们互相引用着各自,导致计数永远不为0,这一团垃圾就永远不能被回收。

2、根可达算法(RootSearching)

在这里插入图片描述

就是说,从一堆“”对象,从它们为起源,开始向着它们自身内部所引用的对象依次向下去搜寻下去,如果哪个对象不能顺着这条线被连接上,那么这个对象就将被视为垃圾。
那什么样的对象是“”对象呢?
1、main方法栈帧内的所有变量。(也就是JVM栈)
2、方法区里存放的静态变量
3、运行时常量池里的东西
4、native方法栈帧里的变量(也就是本地方法栈)
5、Class对象

通过这种算法,“垃圾块”的问题就可以被解决了。

GC回收算法

找到了垃圾,那具体我要使用什么方式来回收这些垃圾呢?
一般来讲,GC回收算法有三种:
在这里插入图片描述

1、Mark-Sweep(标记清除)

在这里插入图片描述

算法的思想是,一次性先标记可以被回收的垃圾对象,然后再对这些被标记的对象进行清理。

这个过程大致是这样的:
在这里插入图片描述

通过GC Roots(根对象们),向下一直找,能连上的就不是垃圾,标记为不可回收。
然后再对那些GC Roots这根线连不上的那些对象,进行标记为可回收垃圾对象,进行回收。

适用场景

算法比较简单,适合存活对象比较多的情况下,效率会比较高。(可想而知,eden区的回收对象每次都比较多,就不太适合这种)

弊端

它需要进行两遍扫描,效率是偏低的。
并且比较容易产生碎片。

2、Copying(拷贝)

在这里插入图片描述
将内存一分为二。实际可用内存只有一半。
在垃圾回收的时候,只需要找到那些不是垃圾的存活对象,一次性将它们都拷贝到另外一块内存中,当前这块内存全部清理掉。
在这里插入图片描述

适用场景

它只扫描一次,效率比标记清除有所提高,且不会产生内存碎片。
适用于存活对象较少的情况(eden区的对象垃圾回收就比较合适)

弊端

内存一分为二,只有一半可用,会造成空间的浪费。
并且每次垃圾回收都会全量拷贝对象到另外一块内存,涉及到移动复制对象,需要调整对象的引用。

3、Mark-Compact(标记压缩)

在这里插入图片描述

和标记清除有点像,但不同的是,它会在对标记为垃圾的对象清理之后,有一个将剩余存活对象进行整理向前移动汇总的动作,这就避免了碎片化的产生。
在这里插入图片描述

优点

不会产生碎片,方便对象分配内存
不会产生内存减半的问题

弊端

它需要像标记清除一样,进行扫描两次。
并且额外的整理的动作,还会涉及到对象的移动。
因此这种算法效率是偏低的。

JVM内存分代模型

部分垃圾回收器使用的模型

除Epsilon 、ZGC、 Shenandoah之外的GC垃圾回收器都是使用逻辑分代模型
G1是逻辑分代,物理不分代。
除此之外不仅逻辑分代,而且物理分代。

堆内存逻辑分区

网上大多数说法都是新老年代的比例是1:3
在这里插入图片描述

不过经过笔者测试,发现在jdk1.8中,新老年代区的比例是1:2
在这里插入图片描述
堆内存分为主要分为两块区域:

1、新生代区

存放新生对象。
新生代这块区域,又细分了几块区域:
1块eden区,2块survivor区,它们的内存占比是8:1:1
eden区满了触发YGC(对新生代区域进行清理)。
采用的垃圾回收算法一般都是复制算法

2、老年代区

存放顽固老不死对象。
新生代和老年代的内存占比是1:3
old区满了触发FGC(同时对新生代和老年代进行清理)
采用的垃圾回收算法可能是标记整理,也可能是标记清除(根据垃圾回收器的实现而定)

NON HEAP-方法区(永久代/元空间)

除了上面的堆内分布之外,还有一块区域,叫做方法区,里面存储的就是Class,常量等的信息,方法区是一种规范。
JVM规范有意将这块区域划分为非堆空间,但在jdk1.8以前这是逻辑意义上的,实际的存储位置还是在堆上。在jdk1.8以前,方法区的实现就是永久代,永久代依旧是堆上分配的一块空间,但几乎不会被GC回收(或者说很苛刻)。
从jdk1.8开始,这块空间正式从堆中被分离出来,方法区的实现是元空间。

永久代和元空间的区别

永久代必须指定大小限制 。(还是占用的堆空间)
元数据可以设置,也可以不设置,无上限(受限于物理内存)

方法区和字符串

.在JDK1.7以前,字符串常量是存放在方法区中的。
在JDK 1.7后,字符串常量池被从方法区里剥离出来,在 Java 堆(Heap)中单独开辟了一块区域存放运行时字符串常量池。

一个对象的生命周期

在这里插入图片描述

一个对象被new了出来
在这里插入图片描述

首先尝试分配到栈内
在这里插入图片描述

栈内放不下了,将被分配到了eden区

在这里插入图片描述

在经历一次年轻代的垃圾回收后,eden区会将剩余存活的对象放入某块survivor区(这里假设是s1),
在这里插入图片描述

在下一次年轻代的垃圾回收后,会将eden区存活的对象和S2中存活的对象一并复制到s1中,并清除掉eden区和s2

重复这样的过程:
在这里插入图片描述
将eden区存活的对象和S1中存活的对象一并复制到s2中

在这里插入图片描述
将eden区存活的对象和S2中存活的对象一并复制到s1中

在这里插入图片描述
当到达一个阈值时(升级年龄),如果某些对象还活着,那这些对象将被放入到老年代中。

总结过程:

1、YGC回收之后,大多数的eden区对象会被回收,活着的进入s0
2、再次YGC,活着的对象eden + s0 -> s1
3、再次YGC,eden + s1 -> s0
4、年龄足够 -> 老年代 (15 CMS 6)
5、s区装不下 -> 老年代

GC概念

在这里插入图片描述

MinorGC = YGC
MajorGC = FGC

CG何时触发

在这里插入图片描述

1、YGC

当eden区空间不足的时候,就会触发YGC。

2、FGC

当old区空间不足时,会触发FGC。除此之外,还可以通过手动调用System.gc来通知虚拟机触发垃圾回收(但这个调用并不一定保证虚拟机会立马进行回收)

细节探究

栈上分配

如果你学过c或者c++,一定知道有个东西叫struct结构体的东西,是可以直接在栈上分配的。
而java也实现了类似的机制,一个对象在被new出来之后,首先会尝试向栈上进行分配,但栈上分配存在前提:

1、线程私有小对象

它必须是线程私有的,且比较小的,能够让栈放的下的对象。

2、无逃逸

什么叫逃逸? 就是说new出来的对象出了它所在的代码块,就没有人再能够找到它了,这样的就是无逃逸的。

void alloc(int i) {
    //只在当前代码块可以找到
    new User(i, "name " + i);
}

但如果你在代码块里操作将这个对象的引用赋予给了某个成员属性,那就产生了逃逸。


void alloc(int i) {
    //赋予给成员变量,发生了逃逸
    this.user = new User(i, "name " + i);
}
支持标量替换

此外,还支持标量替换。形如这种:

void test(int i) {
    int a = this.a;
    int b = this.b;
}

所谓标量替换就是说,JVM可以优化,让两个int值代替对象的存在,而不必分配对象空间。

线程本地分配(Thread Local Allocation Buffer)

每个线程会默认占用eden区1%独属于自己线程的空间,这样多线程的时候会首先向这块空间去分配,而无需竞争eden区就可以申请空间,提高效率。
当然,前提得是一个小对象,如果是比较大的话,这块空间也是无法放的下,只能去竞争了。

测试程序
public class TestTLAB {
    //User u;
    class User {
        int id;
        String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    void alloc(int i) {
        new User(i, "name " + i);
    }

    public static void main(String[] args) {
        TestTLAB t = new TestTLAB();
        long start = System.currentTimeMillis();
        for(int i=0; i<1000_0000; i++) t.alloc(i);
        long end = System.currentTimeMillis();
        System.out.println(end - start);

    }
}

默认JVM是开启了逃逸分析,标量替换,线程本地分配的。
我们直接来运行下程序,看一下执行时间:582
然后再通过JVM参数指定:

-XX:-DoEscapeAnalysis //取消逃逸分析
-XX:-EliminateAllocations //取消标量替换
-XX:-UseTLAB //取消使用线程本地分配

再看一下执行时间:1050
可以发现,差距还是不小的。

对象何时进入老年代

在这里插入图片描述

通过分代年龄

一般来说,升代年龄是可以通过参数来指定。
默认的话,不同垃圾回收器的年龄各有不同。
但我们之前分析过markword,知道是用4位来存储分代年龄的,因此可以断定,这里最大也只能是15。

动态年龄

当进行垃圾回收后,eden区+某个s区的存活对象一次性放到另外一个s区的时候,s区的大小超过了50%的时候,此时会直接把年龄最大的放入老年代。
参考文章:https://www.jianshu.com/p/989d3b06a49d

分配担保

YGC期间 survivor区空间不够了 空间担保直接进入老年代
参考:https://cloud.tencent.com/developer/article/1082730

对象分配过程图

在这里插入图片描述

一个对象被new出来,首先会尝试看能不能放入栈上,如果可以,只需要弹出就可以。
如果不能放,要看这个对象是不是大对象,如果是,则直接进入old区。
不是大对象,则优先看能否使用线程本地分配,无论能与否,都进入eden区。
在垃圾回收的时候,开始使用复制算法来回将eden区和某一s区的存活对象放入另外一个空的s区,反复这样的过程。
直到年龄判断或者动态年龄导致,会让对象进入old区,直到FGC才回收。
在old区满了的时候,会触发FGC,FGC会将新生代和老年代的区域都进行垃圾回收。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值