任何一个JVM问题都可以深究-垃圾回收

引言

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

说说java的引用类型?

强引用

   类似Object obj = new Object() 这类的强引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

弱引用

   用来描述非必需对象,但是它的强度比软引用关联的对象只能生存到下一次收集发生之前。软引用用SoftRefrence来表示。

虚引用

   一个对象是否有虚引用的存在,完全不会对其生存空间造成影响,也无法通过虚引用获取一个对象实例。JDK 1.2 后用 PhantomRefrence 来表示。

弱引用

  弱引用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收器之前,JDK 1.2weakRefrence 来表示。

垃圾回收的目的是?

  清除不再使用的引用,自动释放内存。

GC是如何判断对象是否被回收的?

  GCRoot,如果一个对象与GC Roots之间没有直接或间接的引用关系,如:

  • 某个失去任何引用的对象
  • 两个互相环岛循环引用对象

判决这些对象"死缓",是可以被回收的

什么对象可以作为 GC Roots 呢?

  • 类静态属性中引用的对象
  • 常量引用的对象
  • 虚拟机栈中引用的对象
  • 本地方法栈的引用对象

说说垃圾回收的相关算法?

  • 标记-清除算法

  该算法从每个GC Roots出发,依次标记有引用关系的对象,最后将没有被标记的对象清除。但是这种算法会带来大量的空间碎片,导致需要分配一块较大的连续空间的时候容易触发FGC

  • 标记-整理算法

   类似计算机的磁盘整理,首先会从GC Roots 出发标记为存活对象,然后将存活对象,然后将存活对象整理到内存空间的一端,形成连续的已使用空间,最后把已使用的空间之外的一部分全部清理掉,这样就不会产生空间碎片问题。

  • Mark-Copy算法

  为了能够将并行地标记和整理将空间分为两块,每次只激活其中一块,垃圾回收时只需把存活的的对象复制到另一块未激活空间上,将未激活的空间标记为已激活,将已激活空间标记为未激活,然后清除空间中的原对象。
  堆内存空间分为Eden和两块较小记为未激活,然后清除原空间的元对象。堆内存空间分为较大的Eden和两块较小的Survivor,每次只使用EdenSurvivor区的一块。这种情形下的Mark-Copy减少内存空间的浪费。
  Mark-Copy现作为主流的YGC算法进行新生代的垃圾回收。

说说你知道的垃圾回收器?

  • Seral

  Seral回收器是一个主要用于YGC的垃圾回收器,采用串行单线程的方式完成GC任务,其中Stop The World 简称 STW ,即垃圾回收的某个阶段会暂停整个应用程序执行。FGC的时间相对较长,频繁FGC会严重影响应用程序的性能

  • CMS

算法:

  标记-清除算法,会产生大量的空间碎片

解决方案:

   -XX: + UseCMSCommpactAtFullCollection = n参数,在执行 n 次 FGC 后, JVM再在老生代执行空间碎片整理,执行一次空间碎片整理,但是空间碎片整理阶段也会引发STW

步骤:

  1. 初始标记(Initital Mark)
  2. 并发标记(Concurrent Mark)
  3. 重新标记(ReMark)
  4. 并发清除(Concurrent Mark)
问题:

a. 1,3: 引发STW

解决方案:
   为了减少STW次数,GMS还可以通过配置 -XX: +CMSFullGCsBeforeCompaction = n 参数,在执行 n 次FGC之后,JVM再在老年代执行空间碎片整理。

b. 2,4: 可以和应用程序并发执行,也是比较耗时的操作,但并不影响应用程序政策执行。


  • G1

HotSpot 在 JDK7 中推出了 新一代 G1 垃圾回收,通过 -XX: + UseG1GC参数启动。和CMS相比,G1 具备压缩功能,能避免碎片问题,G1的暂停时间更加可控,性能总体还是不错的

new一个对象干了哪些事情?

   Java是面向对象的静态强类型语言,声明并创建对象很常见,根据某个类声明一个引用变量指向被创建的对象,并使用此引用变量来操作对象,那么在new一个对象干了哪些事情呢?

字节码角度:

1.NEW
  • 如果找不到Class对象,则进行类加载
  • 加载成功后,则在堆中分配内存,从Object开始到本地路径上所有的属性都要分配内存
  • 内存分配完毕,进行0值初始化
    • 引用是占据存储空间的,它是一个变量值,占用4个字节
      • 将执向实例对象的引用变量压入虚拟机栈的栈顶
2.DUP
  • 在栈顶复制该引用变量,这时的栈顶有两个指向堆内实例对象引用变量。
    • 如果方法有参数,还需要把参数压入操作栈中。两个引用变量的目的不同,其中压入底下的引用用于赋值,
    • 或者保存到局部变量表,另一个栈顶的引用变量作为句柄调用相关的方法。
3.INVOKESPECIAL:
  • 调用对象实例方法,通过栈顶的引用变量调用方法。是类初始化时执行的方法,而是对象初始化时执行的方法。

执行步骤角度:

1.确认类元信息是否存在:
  • JVM接收到new指令时,首先在metaspace内检查需要创建的类元信息是否存在

    • 如果不存在,那么在双亲委托派模式下,使用当前类加载器以 ClassLoader + 包名 + 类名 为 Key 进行查找对应的.class文件
      • 如果没有找到,则抛出ClassNotFoundException
      • 如果找到,则进行类加载,并生成对应的Class对象。
2.分配对象内存:
  • 计算对象的内存占用空间
    • 如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小,接着在堆中划分一块内存给新对象
    • 在分配内存空间时,需要进行同步操作,比如采用CAS 失败重试,区域加锁等方式保证分配操作的原子性
3.设定默认值:

  成员变量值都需要设定位默认值,即各种不同形式的0值

4.设置对象头:

  设置新对象的哈希码,GC信息,锁信息,对象所属的类元信息等。这个过程的具体设置取决于JVM实现。

5.执行init方法:

  初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

猜你喜欢

转载自juejin.im/post/5dfccc5a6fb9a01616436517