Java虚拟机GC机制

引言

程序中分配在堆上的内存,当不再需要的时候,需要及时回收以便后续能申请到内存可使用。像C、C++等语言,如果需要释放无用内存空间需要由编程人员自己来处理。而Java语言的虚拟机支持管理内存生命周期、自动释放无用内存,即GC机制。

1 相关基础概念

1.1 运行时数据分区

java代码运行在虚拟机,分为两类:一类是线程共享数据区,即一个进程中所有的线程都可访问的区域;一类是线程隔离数据区,即进程中各个线程是相互隔离的。整个内存分区结构如下图:
在这里插入图片描述

  • 方法区
    存储已被虚拟机加载的类信息、常量、静态变量等,类似C程序的代码区;

  • 存放动态分配的内存,诸如new出来的对象的内存。它是GC管理的年轻代和老年代区域;
  • 虚拟机栈
    它是每个线程执行java代码的函数栈。用于存储局部变量表、操作栈、动态链接、方法返回地址等信息;
  • 本地方法栈
    类似于虚拟机栈,它是每个线程执行native代码的函数栈;
  • 程序计数器
    记录当前线程运行方法执行到了第几行。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。

1.2 可达性分析

了解C++智能指针的朋友,可能会提出基于引用计数可以实现:当不再需要时,释放内存。但是,基于引用计数无法解决环形持有的问题。同样,这时有人会说可以基于弱引用方式破解环形引用,但是弱引用的前提是写代码的人事先知道有环行引用、然后用弱引用去破除,故而弱引用任然无法解决问题。
Java虚拟机采用一种可达性策略去根除基于引用计数存在的问题。基本思想就是:在GC过程中,标记是否能被GC Roots对象引用可达,可达是指对象的上级对象、再上级、。。。,最终能到达GC Roots对象。没有标记为GC Roots可达的对象,就是可释放的对象。

1.2 GC Roots

GC Roots对象又称根对象,在虚拟机规范中以下对象作为GC Roots:

  • 系统类加载器(system class loader)加载的对象;
  • 活着的线程,包括等待或阻塞的线程;
  • Java方法栈和native方法栈中的一些参数/局部变量;
  • 静态变量、常量引用;
  • Held by JVM - JVM由于特殊目的为GC保留的对象,但实际上这个与JVM的实现是有关的。

2 常见GC算法

2.1 标记-清除算法

算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
在这里插入图片描述
优缺点

  • 算法比较简单、容易实现
  • 容易产生内存碎片

2.2 拷贝算法

算法将内存分为等大两块,基本思想是:每次使用一块,当GC的时候,把存活对象拷贝到另一块;下次GC的时候,又把存活对象拷贝回来;。。。循环使用。
在这里插入图片描述
优缺点

  • 算法简单高效、也避免了内存碎片
  • 每次只使用了一半内存,浪费极大

2.3 标记-整理算法

算法核心思想是:当标记完存活对象后,将存活对象移动到一端,使得所有的空闲内存连续。
在这里插入图片描述
优缺点

  • 避免了内存碎片、也不会浪费额外空间
  • 整个算法相对复杂、而且性能耗时也大

3 GC过程概括

3.1 分代GC

Java虚拟机GC将内存分为新生代、老年代和永久代。新生代和老年代内存对应着堆区;而永久代对应着方法区。

  • 新生代
    新生代默认占三分之一的堆空间。整个新生代又分为两个Survivor分区和一个Eden,默认占比为1:1:8。任何时刻,都有一个Survivor分区空闲。
  • 老年代
    老年代默认占三分之二的堆空间。它是所有大对象和长久存活对象的存留区域。
  • 永久代
    永久代是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。所以在HotSpot虚拟机中,永久代就是方法区。

3.2 Minor GC与Full GC

堆内存的GC过程分为Minor GC和Full GC。当对象构建的时候,会优先在新生代分配内存,若新生代不满足分配空间大小就会触发Minor GC;若Minor GC仍然不满足空间大小,就会触发Full GC,然后在老年代分配内存;若老年代不满足分配空间大小,就会触发异常。特别地,Minor GC和Full GC绝不是只在申请对象空间不够的时候才触发

  • Minor GC
    当对象在新生代分配内存后,存在于新生代的Eden分区和一块Survivor分区(假设这块称为 Survivor A,另一块称为Survivor B)。每当经过一次Minor GC,便会将存活对象拷贝到Survivor B、同时将对象年龄+1;当下次Minor GC时,又会将Eden分区和Survivor B中的对象拷贝到Survivor A、同样将对象年龄+1;。。。依次往复,当对象年龄默认达到15(其值可以通过虚拟机参数XX:MaxTenuringThreshold修改)后,就会将对象拷贝到老年代。特别地,当Minor GC过程中,空闲的Survivor无法容纳的对象将提前进入老年代
  • Full GC
    Full GC一般采用标记整理算法,所以频繁的Full GC会导致系统卡顿。

3.3 永久代GC

原则上,方法区的对象会尽力避免GC,因为GC方法区的性价比特低。只回收废弃常量无用类。必须满足如下所有条件就是废弃常量和无用类:

  • 该类对应的Class对象没有在任何地方被引用;
  • 加载该类的所有ClassLoader已经被回收;
  • 该类所有的实例都被回收, Java堆中不存在该类的任何实例。

3.4 Minor GC优化

  • 问题
    当进行Minor GC时,所有线程必须同步阻塞,必然影响性能。
  • TLAB
    为了解决上述问题,每个线程在初始化时都会在Eden中申请一块内存作为线程独占内存分配区域,称为TLAB。类似于Linux内核中的CPU变量。
    当TLAB中剩余内存小于要分配的对象内存大小时,就会在线程共享的新生代中申请;或者废弃当前TLAB、重新申请TLAB空间然后再次进行内存分配。两种方案各有利弊。为了解决这个问题:虚拟机定义了一个阈值,TLAB中剩余内存小于要分配的对象内存大小的情况下,若申请对象内存大于该阈值,就会在线程共享的堆中申请;反之,就会废弃当前TLAB、重新申请TLAB空间然后再次进行内存分配。
    例如,TLAB总空间100KB,使用了90KB,剩余10KB,如果设置的阈值为20KB,那么如果新对象的内存大于20KB,则直接在线程共享分配的堆中分配,如果小于20KB,则会废弃掉之前的那个TLAB,重新分配一个TLAB空间来给新对象分配内存。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值