jvm学习篇01 - 垃圾回收机制

垃圾回收机制

内容来自《深入Java虚拟机》的一些读后速记和理解。

Java运行时数据区域

复习一下Java虚拟机中的内存区域。

线程私有的区域
  • 程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

  • Java虚拟机栈
  • 线程私有的,且生命周期和线程相同。
  • 每当执行一个Java方法时,都会在Java虚拟机栈中创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 在这个区域内,当线程所请求的栈深度大于Java虚拟机所能允许的最大深度时,会发生StackOverflowError栈溢出异常;若Java虚拟机栈容量允许动态扩展,那么当栈扩展时申请不到足够的内存时就会抛出OutOfMemoryError内存不足异常。
  • 本地方法栈

本地方法栈同Java虚拟机栈类似,只是本地方法栈是为本地方法(native)服务的,而Java虚拟机栈是为Java方法服务的。

线程共享的区域
  • 线程共享,在虚拟机启动时创建,用于存储对象实例和数组。
  • 从垃圾收集的角度看,堆可以分为新生代和老年代。
  • 从内存分配的角度看,线程共享的Java堆还可以划分出线程私有的分配缓冲区(ThreadLocal Allocation Buffer,TLAB)提升对象分配内存时的效率。
  • 当堆中没有内存可以分配给实例对象,或堆无法扩展时就会报OOM异常。
  • 方法区
  • 线程共享,主要存储已加载的类型信息、静态变量、常量等。
  • JDK8以后使用本地内存中实现的元空间代替永久代作为方法区。
  • 当方法区无法满足新的内存分配时,就会报内存溢出异常OOM。
  • 运行时常量池作为方法区的一部分,Class文件中有常量池表,主要用于存放编译期间生成的各种字面量和符号引用,这部分内容将会在类加载之后存放到方法区的运行时常量池中。
回收区域

垃圾回收主要针对 方法区 两个区域。
堆中主要回收的是 对象 ,方法区中主要针对常量池的回收和类的卸载,回收 废弃的常量和不再使用的类型。

对象的创建过程

复习一下创建对象new的过程。

  • 类加载检查:判断对象所属的类型是否能在常量池中定位到一个符号引用,并且这个类型是否已经加载、解析、初始化(类加载机制)过。

  • 分配内存:对象所需的内存在类加载阶段就确定下来的,从堆中划分出该大小的内存块分配给该对象。根据虚拟机堆中的内存是否规整(垃圾收集器是否带有空间压缩能力)可以使用两种分配方式

    • “指针碰撞” :堆中的内存是规整的,已经分配了的内存和空闲的内存之间存在一个指针作为分界点的指示器,分配内存时仅仅是把指针往空闲内存的方向移动对象大小的距离即可。
    • “空闲列表” : 堆中的内存不是规整的,因此需要维护一个列表,用于记录堆中的哪些内存时可用的,从空闲列表中找到一块大于对象大小的内存分配给对象 ,并更新列表记录。

    并发环境下分配内存时存在线程安全的问题时怎么解决:

    • 对分配内存的操作进行同步处理——虚拟机中是采用CAS失败重试来保证分配内存操作的原子性
    • 把内存分配的操作按照线程划分到不同的内存空间上,即每个线程先提前划分一小块内存,即本地线程分配缓冲(TLAB),每个线程分配内存时都在自己的分配缓冲区中进行,只有在本地分配缓冲区用完了,分配内存时才去进行同步操作。
  • 初始化零值 : 将对象的分配到的内存空间(除了对象头)都初始化为零值,这样保证了对象所有实例字段都可以在不赋初始值就可以使用。

    对象的内存空间布局:

    • 对象头:存储两类数据
      • 第一类 用于存储对象自身运行时的数据(可称之为Mark Word),例如哈希码(hashcode)、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID等。该部分存储空间时根据对象的状态进行复用的,即对象状态不同存储内容不同。
      • 第二类 是类型指针,即对象指向它的类型的指针,可由该指针确定对象属于哪个类的实例。
    • 实例数据 :对象真正存储的有效信息,即我们在程序代码里定义的各种类型的字段内容。
    • 对齐填充:仅仅起占位符的作用,
  • 设置对象头

  • 执行构造函数:执行构造函数——即Class文件中的cinit()方法,按照程序员的意愿去进行对象的初始化。

自此,一个真正可用的对象就创建出来了。

判断一个对象是否回收
  1. 引用计数器:每个对象都有一个引用计数器,当存在某个地方引用了该对象,则该对象引用计数器就加1,当某个对象的引用计数器值为0时证明没有其他地方引用了该对象,则该对象就是可以被回收的。但是这种方式存在循环引用的问题,因此Java不使用这种方式。
  2. 可达性分析算法:以一组GC Roots为起点,根据引用关系向下搜索,走过的路径我们称之为引用链,若某个对象存在到GC Roots的引用链,则该对象是可达的,这些对象就是存活的不会被垃圾收集器回收。若不可达的对象,要真正宣告该对象死亡,还需要经过两次标记,判断对象不可达时,就进行第一次标记,然后进行一次筛选,即判断是否需要去执行对象的finalize()方法,即判断是否覆盖了finalize()方法且0该finalize()还未执行过(该方法只能被执行一次),若对象在执行finalize()方法时重新与GC Roots建立了引用关系,那么就会在第二次标记时被移出垃圾收集的范围,成功”自救“,否则就会被第二次标记,宣告死亡,被垃圾收集器回收。

补充:一般可作为GC Roots的对象
1.虚拟机栈中局部变量表中的引用
2.本地方法栈JNI中的引用
3.方法区中常量的引用
4 方法区中静态成员属性的引用

类卸载条件
  • 该类的所有实例已被回收;
  • 加载该类的类加载器已被回收;
  • 该类的java.lang.Class对象没有其他地方引用。
分代收集理论
  1. 弱分代假说:对象都是朝生夕灭的
  2. 强分代假说:熬过越多次垃圾收集的对象越难以消亡
  3. 跨代引用假说:跨代引用相比同代引用仅占少数

对于跨代引用,会在新生代中维护一个“记忆集”的数据结构,将老年代划分成若干小块,用于记录老年代中哪些内存块存在跨代引用,进行可达性分析算法时,只需要将这些内存块中的对象加入GC Roots进行扫描。

垃圾收集算法

1.标记-清除算法:标记所有可回收对象,统一回收。容易产生内碎片,不利于内存分配。分配内存使用“空闲链表”的方式,遍历链表找到size大于对象大小时的内存块进行分配。
2. 标记-复制算法:将内存空间划分为大小相等的两半,每次都使用其中的半区,当半区内存不够时就会标记所有存活对象,移动到另一个半区,然后将原分区清理干净。优点是得到的内存空间是规整的,可以通过”指针碰撞“的方式给对象分配内存,有利于内存分配;但是会带来内存间移动的开销,且每次都使用内存的一半,空间浪费较严重,因此现在商用的虚拟机都使用该算法对新生代进行垃圾回收,且将新生代划分为一块Eden区和两块Survivor区,比例为8:1,每次给对象分配内存时都在Eden区,垃圾收集时将Eden区和From Survivor区中的可存活对象移动到To Survivor 区中,这里存在一个老年代担保机制,在Survivor区不足以容纳一次垃圾收集可存活的对象时,就会将这些对象直接放入老年代中。
3. 标记-整理算法:不同于标记-清除算法,这种算法是移动式的回收算法,首先标记所有可存活对象,将这些对象移动到内存空间的一边,然后将边界以外的内存空间清理掉,这种方式得到的内存空间也是规整的,可以使用“指针碰撞”的方式进行内存分配,内存分配效率高,只需要将指针移动对象大小的距离即可分配指定大小的内存给对象。缺点就是如果存在大量可存活的对象,移动对象的开销较大,但是得到分配内存效率的提高,是值得的,提高了程序的吞吐量。

经典的垃圾收集器
  1. Serial:单线程的新生代垃圾收集器,采取复制算法,优点是简单高效,但是需要停止用户线程,可用于客户端下运行的虚拟机的垃圾收集。
  2. ParNew:Serial的多线程并行版本,存在多条垃圾收集线程同时执行,高效利用了系统资源,采取复制算法,JDK9以后只有它可以和老年代的收集器CMS配合使用了。
  3. Parallel Scavenge:前面的垃圾收集器注重的是低停顿时间(用户线程停顿时可以叫成“Stop the world”),而这款新生代垃圾收集器注重的是达到一个可控制的吞吐量,由于垃圾收集的停顿时间缩短是要以牺牲吞吐量为代价的,自己较难掌控,但是通过Parallel Scavenge的垃圾收集的自适应调节策略,该垃圾收集器可以通过监控当前系统的运行情况动态调整参数来达到一个最合适的停顿时间或者最大的吞吐量,这是该垃圾收集器的一个重要特性。
  4. Serial Old:Serial的老年代版本,它也是单线程的,使用的是标记-整理算法,除了可以配合Serial使用,还可以作为CMS发生并发失败时的后备预案。
  5. Parallel Old:Parallel Scavange的老年代版本,配合Parallel Scavange使用,支持多线程并发收集,使用的是标记-整理算法,在注重吞吐量和CPU资源敏感的场合下可以使用这种组合。
  6. CMS:以获取最短回收停顿时间为目标,基于标记-清除算法。它首次支持让用户线程和垃圾收集线程基本上同时工作,是老年代的垃圾收集器,垃圾收集主要包括以下几个过程:

①初始标记:标记与GC Roots直接关联的那些对象。需要“stop the world”,不过停顿时间很短。
②并发标记:从GC Roots开始,根据引用关系向下搜索进行标记动作,这一部分耗时较长,但是可以和用户线程并发执行。
③重新标记:因为并发标记阶段是与用户线程并发执行的,因此可能存在因为用户线程继续运作而导致标记变动,重新标记就是为了修正标记变动的这一部分对象的标记记录。(标注给自己看:增量更新和原始快照还不太懂)
④并发清除:清除掉所有不可存活的对象

CMS垃圾收集器的优点就是低停顿时间,缺点也不容忽视:

1.无法处理”浮动垃圾“,可能会出现“Concurrent mode failure”并发失败导致一次stop the world,这时候就要启动Serial Old后备预案来进行一次Full GC,这样子停顿时间反而更多了。
2.基于标记-清除算法,容易产生大量内碎片。
3.对处理器资源较敏感。

注:JDK8新生代默认使用Parallel Scavange垃圾收集器,老年代使用 ParallelOld垃圾收集器。

  1. G1:面向全堆的垃圾收集器,面向局部收集和基于Region的内存布局。不遵循固定大小的和区域的分代区域划分,而是将内存空间划分为多个大小相等Region,每个Region区域都可以根据需要充当新生代或者老年代的角色采用不同策略的进行处理。内部还会维护一个优先级列表,每次都会去回收价值收益最大的那些Region。(根据经验)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值