局部性原理和分代回收思想

大学学习操作系统或者计算机组成原理的时候都提到一个重要概念,叫局部性原理。

局部性原理是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。

后来发现,这个原理说的存储器不只是高速缓存(Cache),访问内存(RAM)、磁盘(ROM)时都有局部性。其实在项目中使用redis、memcache这样的缓存也体现了局部性原理的重要性。

在Java虚拟机的堆空间中,活跃的对象往往是小部分,大多数对象创建若干次后就会被回收,这就让Java虚拟机引入了分代回收的算法。我不清楚Sun公司的研究人员是不是受到了局部性原理的启发,不过我确实是通过局部性原理理解了分代思想。

分代回收就是将堆空间分为新生代和老年代。新生代用来存储新创建的对象。当对象存活时间很长时将其移动到老年代。根据新生代和老年代的特性,Java虚拟机可以采用不同的回收算法。

新生代的对象大多数存活时间很短,因此可以采用耗时短的回收算法,新生代触发的GC一般叫做Minor GC。

老年代的对象往往可以长时间存活,因此老年代的垃圾回收频率不高,往往是堆空间用完的时候才会针对老年代进行垃圾回收。但老年代的回收一般是进行全堆扫描,找出能被回收的空间,这会耗费大量时间。现代的垃圾回收器会用各种手段避免进行全堆扫描。老年代的GC叫做 Full GC。

本篇我们重点关注的是Minor GC,也就是针对新生代的垃圾回收。

新生代的内存划分和回收机制

上一篇提到了复制算法,他的思想是把堆空间分为1:1两部分,并维持两个from和to指针。由于Java虚拟机中活跃的对象是小部分,因此实际上复制算法并不需要保持1:1的比例(Sun公司给出的理论上是98%的对象都是用几次就回收了的)。假定按照我们预测的,新生代的大部分对象会死亡,那么使用复制算法仅需要复制少量的数据,算法效果也会很好。因此新生代又被划分为 Eden区和两个大小相同的 Survivor区。

Survivor区的大小默认是自动调节的,也可以手动调节。需要注意的是,Survivor区分配的内存越多,堆空间的使用效率越低。

我们使用new指令时,new指令会在Eden区中划分出一块作为存储对象的内存。如果new新对象时Eden 区满了,这时候会触发一次Minor GC。Minor GC存活下来的对象会移动到Survivor区。Java虚拟机会记录Survivor区中的对象被复制了多少次。

跟上篇说到的复制算法一样,to指针指向一个空的Survivor区。进行Minor GC时,Eden区和from中的存活对象会被复制到to指向的区域中,然后交换from和to指针。这样当下次有Minor GC的时候保证to中的内容是空的。

什么时机晋升老年代

有两种情况会让新生代的对象晋升到老年代。第一种是对象复制次数达到设定值。如果一个对象复制次数为15(默认为15,可使用 -XX:+MaxTenuringThreshold修改),则这个对象会被晋升到老年代。第二种是Survivor区占用超过50%(可使用 -XX:TargetSurvivorRatio修改)时,虚拟机会将复制次数较高的对象复制到老年代。

Minor GC如何避免全堆扫描

我们希望Minor GC时只扫描新生代的GC Roots,然而老年代对象有可能引用了新生代对象。这种情况我们无法预期,所以Minor GC的时候必须要考虑老年代对新生代对象的引用,把老年代对新生代的引用加入GC Roots里面。你会发现,新生代的GC要扫描老年代,这岂不是跟全堆扫描一样了吗?

Hotspot虚拟机使用了卡表(Card Table)技术去解决这个问题。具体操作是Java虚拟机把整个堆分成一个个512字节的卡,并且维护一张表用来存储每张卡的标识位。这个标识位代表这张卡是否包含对新生代对象的引用。如果存在就认为这张卡是脏的。

这样在Minor GC的时候就可以不进行全堆扫描,而是从卡表中寻找脏卡,并将脏卡中的对象加入到GC Roots中。脏卡扫描结束后则清空标识位。

写屏障(write barrier)

在解释执行器中,Java虚拟机需要截获每个可能更新引用的操作,并把对应位置的标识位标记为脏。这样来保证每个可能指向新生代对象的卡都被标记到。

但是在即时编译器生成的机器码中,这块代码并不在Java虚拟机管理之下。因此这部分代码需要插入额外的逻辑,这就是所谓的写屏障。

写屏障不会判断是否指向了新生代对象,而是把这块区域认为已经指向了新生代。这样就简化了指令,变为一个简单的移位操作。写屏障虽然带来了性能上的开销,但是它加大了Minor GC的吞吐率。因此这些代价还是值得的。

虚共享(false sharing)

假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这代表一共有64张卡。HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB。

如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。

Hotspot引入了新的参数-XX:+UseCondCardMark,来减少写卡表的操作。在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。伪代码如下:

if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;

总结

总结一下,本篇从局部性原理出发引出了 Java 虚拟机分代回收的思想。分代回收是指,堆内存分为新生代和老年代,并采用不同的垃圾回收算法。

其中把新生代分为 Eden区和两个大小相同的 Survivor 区。新生代的GC 成为 Minor GC。Minor GC 时 Eden 区和 from 指向的 Survivor 区的存活对象会被存储到 to 指向的 Survivor 区。当 Survivor 区对象复制次数到一定值或者Survivor区空间使用超过一定值的时候,会把对象晋升到老年代。

针对Minor GC 可能出现的老年代对象包含新生代对象引用的问题,Hotspot虚拟机是用卡表技术来解决的。

参考文章

JVM之卡表(Card Table)

郑雨迪:深入拆解虚拟机

深入理解Java虚拟机:JVM高级特性与最佳实践

JVM学习(三):垃圾回收算法的更多相关文章

  1. JVM学习--(四)垃圾回收算法

    我们都知道java语言与C语言最大的区别就是内存自动回收,那么JVM是怎么控制内存回收的,这篇文章将介绍JVM垃圾回收的几种算法,从而了解内存回收的基本原理. stop the world 在介绍垃圾 ...

  2. JVM学习记录-垃圾回收算法

    简述 因为各个平台的虚拟机的垃圾收集器的实现各有不同,所以只介绍几个常见的垃圾收集算法. JVM中常见的垃圾收集算法有以下四种: 标记-清除算法(Mark-Sweep). 复制算法(Copying). ...

  3. JVM学习笔记——垃圾回收篇

    JVM学习笔记--垃圾回收篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分 我们会分为以下几部分进行介绍: 判断垃圾回收对象 垃圾回收算法 分代垃圾回收 垃圾回收器 ...

  4. JVM中的垃圾回收算法GC

    GC是分代收集算法:因为Young区,需要回收垃圾对象的次数操作频繁:Old区次数上较少收集:基本不动Perm区.每个区特点不一样,所以就没有通用的最好算法,只有合适的算法. GC的4大算法 1.引用 ...

  5. jvm系列三垃圾回收

    三.垃圾回收 1.如何判断对象可以回收 引用计数法 弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放 可达性分析算法 JVM中的垃圾回收器通过可达性分析来探索所有存活的对象 扫描堆中的 ...

  6. JVM虚拟机和垃圾回收算法

    类加载机制 双亲委派模型 垃圾回收算法 CMS G1 类加载机制 双亲委派模型 双亲委派模型: 需要加载一个类,先委托父类加载,父类找父类,依次递归加载;加载不到再由自己加载 垃圾回收算法 JVM的内 ...

  7. @JVM新一代的垃圾回收算法

    垃圾回收的瓶颈 传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限.但是他无法解决的一个问题,就是Full GC所带来的应用暂停.在一些对实时性要 ...

  8. 【JVM】JVM中的垃圾回收算法

    1.标记 -清除算法 "标记-清除"(Mark-Sweep)算法,如它的名字一样,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收 ...

  9. 谈谈JVM垃圾回收机制及垃圾回收算法

    一.垃圾回收机制的意义 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理.由于有个垃圾回收机制 ...

  10. Java中的垃圾回收算法详解

    一.前言   前段时间大致看了一下<深入理解Java虚拟机>这本书,对相关的基础知识有了一定的了解,准备写一写JVM的系列博客,这是第二篇.这篇博客就来谈一谈JVM中使用到的垃圾回收算法. ...

随机推荐

  1. lftp下载文件无法覆盖,提示" file already existst and xfer:clobber is unset" 问题解决

    在 /etc/lftp.conf   文件中添加以下配置即可 set xfer:clobber on

  2. DPDK 网络加速在 NFV 中的应用

    目录 文章目录 目录 前文列表 传统内核协议栈的数据转发性能瓶颈是什么? DPDK DPDK 基本技术 DPDK 架构 DPDK 核心组件 应用 NUMA 亲和性技术减少跨 NUMA 内存访问 应用 ...

  3. git如何从远端获取某个文件

    git fetch git checkout origin/master -- path/folder/filename

  4. Linux学习笔记:shell

    目录 通配符 特殊符号 变量 环境变量 默认变量 shell script case if for until while function 本文更新于2019-08-23. 通配符 *:0个至无穷多 ...

  5. Mac 配置flutter

    1. vim ~/.base_profile 2. 如下 export PATH=/Users/korea/Desktop/development/flutter/bin:$PATH export P ...

  6. STS中依赖项的设置

    经过试验,把依赖项总结一下,可能会不断修改. 1. 父依赖项(固定) <parent> <groupId>org.springframework.boot</groupI ...

  7. C++ N叉树的实现

    引言 最近一个项目需要使用多叉树结构来存储数据,但是基于平时学习的都是二叉树的结构,以及网上都是二叉树为基础来进行学习,所以今天实现一个多叉树的数据结构. 理论基础 树和二叉树: 多叉树:多叉树,顾名 ...

  8. 零零散散的shell笔记

    ls __paddlepalm_* > __palminfo__ 名字以__paddlepalm_开头的文件名打印到后面那个info里面 https://www.runoob.com/linux ...

  9. OracleLinux6安装

    针对Oracle数据库安装的linux系统 1.首先要有oracle linux的镜像 链接:https://pan.baidu.com/s/1S3xYr4YNGtU-351bVaS1-Q 提取码:a ...

  10. redis5.0 数据结构与命令

    1.redis 支持如下5种数据结构 数据结构 说明 简介 String 字符串 key-val Hash 哈希 filed-val 映射表 List 列表 双向链表 Set 集合 element(元 ...