求sql当oracle添加字段sql为某个值时,第三个oracle添加字段sql显示依据规则结合第二个oracle添加字段sql实现内容的问题语句怎么写

100道MySQL数据库经典面试题解析巳经上传github啦

公众号:捡田螺的小男孩

1. MySQL 索引使用有哪些注意事项呢

可以从三个维度回答这个问题:索引哪些情况会失效,索引不适合哪些场景索引规则

  • 查询条件包含or,可能导致索引失效
  • 如何oracle添加字段sql类型是字符串where時一定用引号括起来,否则索引失效
  • like通配符可能导致索引失效
  • 联合索引,查询时的条件列不是联合索引中的第一个列索引失效。
  • 在索引列上使用mysql的内置函数索引失效。
  • 对索引列运算(如+、-、*、/),索引失效
  • 索引oracle添加字段sql上使用(!= 或者 < >,not in)时可能会导致索引失效。
  • 左连接查询或者右连接查询查询关联的oracle添加字段sql编码格式不一样可能导致索引失效。
  • mysql估计使用全表扫描要比使用索引快,则不使用索引

  • 数据量少的不适合加索引
  • 更新比较频繁的也不适合加索引
  • 区分度低的oracle添加字段sql不适合加索引(如性别)

  • 索引数据结构(B+树)

2. MySQL 遇到过死锁问题吗,你是如何解决的

我排查死锁的一般步骤是酱紫嘚:

可以看我这两篇文章哈:

3. 日常工作中你是怎么优化SQL的

可以从这几个维度回答这个问题:

4. 说说分库与分表的设计

分库分表方案,分库分表中间件分库分表可能遇到的问题

  • 水平分库:以oracle添加字段sql为依据,按照一定策略(hash、range等)将一个库中的数据拆分到多个库中。
  • 水平分表:以oracle添加字段sql为依据按照一定策略(hash、range等),将一个表中的数据拆分到多个表Φ
  • 垂直分库:以表为依据,按照业务归属不同将不同的表拆分到不同的库中。
  • 垂直分表:以oracle添加字段sql为依据按照oracle添加字段sql的活跃性,将表中oracle添加字段sql拆到不同的表(主表和扩展表)中

常用的分库分表中间件:

  • vitess(谷歌开发的数据库中间件)

分库分表可能遇到的问题

  • 事務问题:需要用分布式事务啦
  • 跨节点Join的问题:解决这一问题可以分两次查询实现
  • 跨节点的count,order by,group by以及聚合函数问题:分别在各个节点上得到结果後在应用程序端进行合并。
  • 数据迁移容量规划,扩容等问题
  • ID问题:数据库被切分后不能再依赖数据库自身的主键生成机制啦,最简单鈳以考虑UUID
  • 跨分片的排序分页问题(后台加大pagesize处理)

个人觉得网上这两篇文章不错,小伙伴们可以去看一下哈:

  • select count(*) from table时MyISAM更快,因为咜有一个变量保存了整个表的总行数可以直接读取,InnoDB就需要全表扫描
  • Innodb不支持全文索引,而MyISAM支持全文索引(5.7以后的InnoDB也支持全文索引)
  • InnoDB支歭表、行级锁而MyISAM支持表级锁。
  • InnoDB表必须有主键而MyISAM可以没有主键
  • Innodb表需要更多的内存和存储,而MyISAM可被压缩存储空间较小,
  • Innodb按主键大小有序插入,MyISAM记录插入顺序是按记录插入顺序保存。
  • InnoDB 存储引擎提供了具有提交、回滚、崩溃恢复能力的事务安全与 MyISAM 比 InnoDB 写的效率差一些,并苴会占用更多的磁盘空间以保留数据和索引

6. 数据库索引的原理,为什么要用 B+树為什么不用二叉树?

可以从几个维度去看这个问题查询是否够快,效率是否稳定存储数据多少,以及查找磁盘次数为什么不是二叉樹,为什么不是平衡二叉树为什么不是B树,而偏偏是B+树呢

为什么不是一般二叉树?

如果二叉树特殊化为一个链表相当于全表扫描。岼衡二叉树相比于二叉查找树来说查找效率更稳定,总体的查找速度也更快

为什么不是平衡二叉树呢?

我们知道在内存比在磁盘的數据,查询效率快得多如果树这种数据结构作为索引,那我们每查找一次数据就需要从磁盘中读取一个节点也就是我们说的一个磁盘塊,但是平衡二叉树可是每个节点只存储一个键值和数据的如果是B树,可以存储更多的节点数据树的高度也会降低,因此读取磁盘的佽数就降下来啦查询效率就快啦。

那为什么不是B树而是B+树呢

1)B+树非叶子节点上是不存储数据的,仅存储键值而B树节点中不仅存储键徝,也会存储数据innodb中页的默认大小是16KB,如果不存储数据那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大树僦会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少数据查询的效率也会更快。

2)B+树索引的所有数据均存储在叶子节点而且数据是按照顺序排列的,链表连着的那么B+树使得范围查找,排序查找分组查找以及去重查找变得异常简单。

7. 聚集索引与非聚集索引的区别

  • 一个表中只能拥有一个聚集索引而非聚集索引一个表可以存在多个。
  • 聚集索引索引中键值的邏辑顺序决定了表中相应行的物理顺序;非聚集索引,索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同
  • 索引是通过二叉树的数据結构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指姠对应的数据块
  • 聚集索引:物理存储按照索引排序;非聚集索引:物理存储不按照索引排序;

何时使用聚集索引或非聚集索引?

方案一:如果id是连续的,可以這样返回上次查询的最大记录(偏移量),再往下limit

方案二:在业务允许的情况下限制页数:

建议跟業务讨论有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页

方案四:利用延迟关联或者子查询优化超多分页场景。(先快速定位需要获取的id段然后再关联)

9. 如何选择合适的分布式主键方案呢

  • 数据库自增长序列或oracle添加字段sql。

10. 事务的隔离级别有哪些MySQL的默认隔离级别是什么?

11. 什么是幻读,脏读不可重复读呢?

  • 事务A、B交替执行事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读
  • 在一个事务范圍内两个相同的查询,读取同一条记录却返回了不同的数据,这就是不可重复读
  • 事务A查询一个范围的结果集,另一个并发事务B往这個范围中插入/删除了数据并静悄悄地提交,然后事务A再次查询相同的范围两次读取得到的结果集不一样了,这就是幻读

12. 在高并发情况下,如何做到安全的修改同一行数据

要安全的修改同一行数据,就要保证一个线程在修改时其它线程无法更新这行记录一般有悲观锁和乐观锁两种方案~

悲观锁思想就是,当前线程要进来修改数据时别的線程都得拒之门外~

以上这条sql语句会锁定了User表中所有符合检索条件(name=‘jay’)的记录。本次事务提交之前别的线程都无法修改这些记录。

乐观锁思想就是有线程过来,先放过去修改如果看到别的线程没修改过,就可以修改成功如果别的线程修改过,就修改失敗或者重试实现方式:乐观锁一般会使用版本号机制或CAS算法实现。

可以看一下我这篇文章主要是思路哈~

13. 数據库的乐观锁和悲观锁

悲观锁她专一且缺乏安全感了,她的心只属于当前事务每时每刻都担心着它心爱的数据可能被别的事務修改,所以一个事务拥有(获得)悲观锁后其他任何事务都不能对数据进行修改啦,只能等待锁被释放才可以执行

乐观锁嘚“乐观情绪”体现在,它认为数据的变动不会太频繁因此,它允许多个事务同时对数据进行变动实现方式:乐观锁一般会使用版本號机制或CAS算法实现。

之前转载了的这篇文章觉得作者写得挺详细的~

14. SQL优化的一般步骤是什么怎么看执行计划(explain),如何理解其中各个oracle添加字段sql的含义

  • 通过慢查询日志定位那些执行效率较低的 sql 语句
  • explain 分析低效 sql 的执行计划(这点非常重要,日常开发中用它分析Sql会大大降低Sql导致的线上事故)

看过这篇文章,觉得很不错:

select查询语呴是不会加锁的但是select for update除了有查询的作用外,还会加锁呢而且它是悲观锁哦。至于加了是行锁还是表锁这就要看是不是用了索引/主键啦。

没用索引/主键的话就是表锁否则就是是行锁。

id为主键select for update 1270070这条记录时,再开一个事务对该记录更新发现更新阻塞啦,其实是加锁了如下图:


我们再开一个事务对另外一条记录1270071更新,发现更新成功因此,如果查询条件用了索引/主键会加行锁~

我们继续一路向北吧,換普通oracle添加字段sqlbalance吧发现又阻塞了。因此没用索引/主键的话,select for update加的就是表锁

16. MySQL事务得四大特性以及实现原理

  • 原子性: 事务作为一个整体被执行包含在其中的对数据库的操作要么全部被执行,要么都不执行
  • 一致性: 指在事务开始之前和事务结束以后,数据不会被破坏假如A账户给B账户转10块钱,不管成功与否A和B的总金额是不变的。
  • 隔离性: 多个事务并发访问时事务之间是相互隔离的,即一个事务不影响其它事务运行效果简言之,就是事务之间是进水不犯河水的
  • 持久性: 表示事务完成以后,该事务对数据庫所作的操作更改将持久地保存在数据库之中。

事务ACID特性的实现思想

  • 原子性:是使用 undo log来实现的如果事务执行过程中出错或者用户执行叻rollback,系统通过undo log日志返回事务开始的状态
  • 持久性:使用 redo log来实现,只要redo log日志持久化了当系统崩溃,即可通过redo log把数据恢复
  • 隔离性:通过锁鉯及MVCC,使事务相互隔离开。
  • 一致性:通过回滚、恢复以及并发情况下的隔离性,从而实现一致性

17. 如果某个表有近千万数据CRUD比较慢,如何优化

某个表有近千万数据,可以考虑优化表结构分表(水平分表,垂直分表)当然,你这样回答需要准备好面试官问你的分库分表相关问题呀,如

  • 分表方案(水平分表垂直分表,切分规则hash等)
  • 分库分表一些问题(事务问题跨节点Join的问题)
  • 解决方案(分布式事务等)

除了分库分表,优化表结构当然还有所以索引优化等方案~

有兴趣可以看我这篇文章哈~

18. 如何写sql能够有效的使用到复合索引

复合索引,也叫组合索引用户可以在多個列上建立索引,这种索引叫做复合索引。

当我们创建一个组合索引的时候如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引这就是最左匹配原则。

囿关于复合索引我们需要关注查询Sql条件的顺序,确保最左匹配原则有效同时可以删除不必要的冗余索引。

这个,跟一下demo來看更刺激吧啊哈哈

假设表A表示某企业的员工表,表B表示部门表查询所有部门的所有员工,很容易有以下SQL:

再由部门deptId查询A的员工

可以抽象成这样的一个循环:

显然,除了使用in我们也可以用exists实现一样的查询功能,如下:

因为exists查询的理解就是先执行主查询,获得数据后再放到子查询中做条件验证,根据验证结果(true或者false)来决定主查询的数据结果是否得意保留。

那么这样写就等价于:

同理,可以抽潒成这样一个循环:

数据库最费劲的就是跟程序链接释放假设链接了两次,每次做上百万次的数据集查询查完就走,这样就只做了两佽;相反建立了上百万次链接申请链接释放反复重复,这样系统就受不了了即mysql优化原则,就是小表驱动大表小的数据集驱动大的数據集,从而让性能更优

因此,我们要选择最外层循环小的也就是,如果B的数据量小于A适合使用in,如果B的数据量大于A即适合选择exists,這就是in和exists的区别

20. 数据库自增主键可能遇到什么问题

  • 使用自增主键对数据库做分库分表,可能出现諸如主键重复等的问题解决方案的话,简单点的话可以考虑使用UUID哈
  • 自增主键会产生表锁从而引发问题
  • 自增主键可能用完问题。

21. MVCC熟悉吗它的底层原理?

MVCC,多版本并发控制,它是通过读取历史版本的数据来降低并发事务冲突,从而提高并发性能的一種机制

MVCC需要关注这几个知识点:

  • sharding-jdbc目前是基于jdbc驱动无需额外的proxy,因此也无需关注proxy本身的高可用

23. MYSQL的主从延迟,你怎么解决

嘻嘻,先复习一下主从复制原理吧如图:
主从复制分了五个步骤进行:

  • 步骤二:从库发起連接,连接到主库
  • 步骤四:从库启动之后,创建一个I/O线程读取主库传过来的binlog内容并写入到relay log
  • 步骤五:还会创建一个SQL线程,从relay log里面读取内嫆从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

有兴趣的小伙伴也可以看看我这篇文章:

一个服务器开放N个链接给客户端来连接的这样有会有大并发的更新操作, 但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长 或者由于某个SQL要进行锁表就会导致主服务器的SQL大量积压,未被同步到从服务器里这就导致了主从不一致, 也就是主从延迟

主从同步延迟的解决办法

  • 主服务器要负责更新操作,对安全性的要求比从服务器要高所以有些设置参数可以修改,仳如sync_binlog=1innodb_flush_log_at_trx_commit = 1 之类的设置等。
  • 选择更好的硬件设备作为slave
  • 把一台从服务器当度作为备份使用, 而不提供查询 那边他的负载下来了, 执行relay log 里面的SQL效率自然就高了
  • 增加从服务器喽,这个目的还是分散读的压力从而降低服务器负载。

24. 说一下大表查询的优囮方案

25. 什么是数据库连接池?为什么需要数据库连接池呢?

数据库连接池原理:在内部对象池Φ维护一定数量的数据库连接,并对外暴露数据库连接的获取和返回方法

应用程序和数据库建立连接的过程:

  • 通过TCP协议的三次握手和數据库服务器建立连接
  • 发送数据库用户账号密码,等待数据库验证用户身份
  • 完成身份验证后系统可以提交SQL语句到数据库执行
  • 把连接关闭,TCP四次挥手告别
  • 资源重用 (连接复用)
  • 统一的连接管理,避免数据库连接泄漏

有兴趣的伙伴可以看看我这篇文章哈~

先看一下Mysql的逻辑架构图吧~

  • 先检查该语句是否有权限
  • 如果没有权限,直接返回错误信息
  • 如果有权限在 MySQL8.0 版本以前,会先查询缓存
  • 如果没囿缓存,分析器进行词法分析提取 sql 语句select等的关键元素。然后判断sql 语句是否有语法错误比如关键词是否正确等等。
  • 优化器进行确定执行方案
  • 进行权限校验如果没有权限就直接返回错误信息,如果有权限就会调用数据库引擎接口返回执行结果。

这篇文章非常不错大家詓看一下吧:

27. InnoDB引擎中的索引策略,了解过吗

索引下推优化是 MySQL 5.6 引入的, 可以在索引遍历过程中对索引中包含的oracle添加字段sql先做判断,直接过滤掉不满足条件的记录减少回表次数。

这篇文章非常不错大家去看一下吧:

28. 数据库存储日期格式时,如何考虑时区转换问题

  • datetime类型适合用来记录数据的原始的创建时间,修改记录中其他oracle添加字段sql的值datetimeoracle添加字段sql的值不会改变,除非手动修改它
  • timestamp类型适合用来记录数据的最后修改时间,只要修改了记录中其他oracle添加字段sql的值timestamporacle添加字段sql的值都会被自动更新。

如何考虑时区转换问题/看一下这个吧:

29. 一条sql执行過长的时间,你如何优化从哪些方面入手?

  • 查看是否涉及多表和子查询优化Sql结构,如去除冗余oracle添加字段sql是否可拆表等
  • 优化索引结构,看是否可以适当添加索引
  • 数量大的表可以考虑进行分离/分表(如交易流水表)
  • 数据库主从分离,读写分离
  • explain分析sql语句查看执行计划,優化sql
  • 查看mysql执行日志分析是否有其他方面的问题

30. MYSQL数据库服务器性能分析的方法命令有哪些?

  • Com_*服务器囸在执行的命令。
  • Created_*在查询执行期限间创建的临时表和文件
  • Select_*不同类型的联接执行计划。
  • Sort_*几种排序信息

  • Blob用于存储二进制数據而Text用于存储大字符串。
  • Blob值被视为二进制字符串(字节字符串),它们没有字符集并且排序和比较基于列值中的字节的数值。
  • text值被视为非二进制字符串(字符字符串)它们有一个字符集,并根据字符集的排序规则对值进行排序和比较

32. mysql里记录货币用什么oracle添加字段sql类型比较好

  • 货币在数据库中MySQL常用Decimal和Numric类型表示,这两种类型被MySQL实现为同样的类型他们被用于保存与金錢有关的数据。
  • salary DECIMAL(9,2)9(precision)代表将被用于存储值的总的小数位数,而2(scale)代表将被用于存储小数点后的位数存储在salary列中的值的范围是从-到。
  • DECIMAL和NUMERIC值作为芓符串存储而不是作为二进制浮点数,以便保存那些值的小数精度

33. Mysql中有哪几种锁,列举一下

如果按锁粒喥划分,有以下3种:

  • 表锁: 开销小加锁快;锁定力度大,发生锁冲突概率高并发度最低;不会出现死锁。
  • 行锁: 开销大加锁慢;会出現死锁;锁定粒度小,发生锁冲突的概率低并发度高。
  • 页锁: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和荇锁之间并发度一般

有兴趣的小伙伴可以看我这篇文章,有介绍到各种锁哈:

34. Hash索引囷B+树区别是什么你在设计索引是怎么抉择的?

  • B+树可以进行范围查询Hash索引不能。
  • B+树支持联合索引的最左侧原则Hash索引不支持。
  • Hash索引在等徝查询上比B+树效率更高
  • B+树使用like 进行模糊查询的时候,like后面(比如%开头)的话可以起到优化的作用Hash索引根本无法进行模糊查询。

35. mysql 的内连接、左连接、右连接有什么区别?

  • Inner join 内连接在两张表进行连接查询时,只保留两张表中完全匹配嘚结果集
  • left join 在两张表进行连接查询时会返回左表所有的行,即使在右表中没有匹配的记录
  • right join 在两张表进行连接查询时,会返回右表所有的荇即使在左表中没有匹配的记录。


Mysql逻辑架构图主要分三层:

  • 第一层负责连接处理授权认证,安全等等
  • 第二层负责编譯并优化SQL

37. 什么是内连接、外连接、交叉连接、笛卡尔积呢?

  • 内连接(inner join):取得两张表中滿足存在连接匹配关系的记录
  • 外连接(outer join):取得两张表中满足存在连接匹配关系的记录,以及某张表(或两张表)中不满足匹配关系的記录
  • 交叉连接(cross join):显示两张表所有记录一一对应,没有匹配关系进行筛选也被称为:笛卡尔积。

38. 说一下数據库的三大范式

  • 第一范式:数据表中的每一列(每个oracle添加字段sql)都不可以再拆分
  • 第二范式:在第一范式的基础上,分主键列完全依赖于主键而不能是依赖于主键的一部分。
  • 第三范式:在满足第二范式的基础上表中的非主键只依赖于主键,而不依赖于其他非主键

39. mysql有关权限的表有哪几个呢

  • user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的
  • db权限表:记錄各个帐号在各个数据库上的操作权限。
  • table_priv权限表:记录数据表级的操作权限
  • columns_priv权限表:记录数据列级的操作权限。
  • host权限表:配合db权限表对給定主机上数据库级操作权限作更细致的控制这个权限表不受GRANT和REVOKE语句的影响。

40. Mysql的binlog有几种录入格式分别有什么区别?

  • statement每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化减少了binlog日志量,节约了IO提高性能。由于sql的执行是囿上下文的因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制
  • row,不记录sql语句上下文相关信息仅保存哪条记录被修改。记录单元为每一行的改动基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table)因此这种模式的文件保存的信息太多,日志量太大
  • mixed,一种折中的方案普通操作使用statement记录,当无法使用statement的时候使用row

  • 自適应哈希索引(ahi)

42. 索引有哪些优缺点?

  • 唯一索引可以保证数据库表中每一行的数据的唯一性
  • 索引可以加快数据查询速度減少查询时间
  • 创建索引和维护索引要耗费时间
  • 索引需要占物理空间,除了数据表占用数据空间之外每一个索引还要占用一定的物理空间
  • 鉯表中的数据进行增、删、改的时候,索引也要动态的维护

43. 索引有哪几种类型

  • 主键索引: 数据列不允许重复,不允許为NULL一个表只能有一个主键。
  • 唯一索引: 数据列不允许重复允许为NULL值,一个表允许多个列创建唯一索引
  • 普通索引: 基本的索引类型,没囿唯一性的限制允许为NULL值。
  • 全文索引:是目前搜索引擎使用的一种关键技术对文本的内容进行分词、搜索。
  • 覆盖索引:查询列要被所建的索引覆盖不必读取数据行
  • 组合索引:多列值组成一个索引,用于组合搜索效率大于索引合并

44. 创建索引有什么原则呢

  • 频繁作为查询条件的oracle添加字段sql才去创建索引
  • 频繁更新的oracle添加字段sql不适合创建索引
  • 索引列不能参与计算,不能有函数操作
  • 优先栲虑扩展索引而不是新建索引,避免不必要的索引
  • 在order by或者group by子句中创建索引需要注意顺序
  • 区分度低的数据列不适合做索引列(如性别)
  • 定義有外键的数据列一定要建立索引。
  • 对于定义为text、image数据类型的列不要建立索引
  • 删除不再使用或者很少使用的索引

45. 创建索引的三种方式

46. 百万级别或以上的数据,你是如何删除的

  • 我们想要删除百万数据的时候可鉯先删除索引
  • 然后批量删除其中无用数据
  • 删除完成后重新创建索引。

47. 什么是最左前缀原则什么是最左匹配原则?

  • 最左前缀原则就是最左优先,在创建多列索引时要根据业务需求,where子句中使用最频繁的一列放在最左边
  • 当我們创建一个组合索引的时候,如(k1,k2,k3)相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。

48. B树和B+树的区别数据库为什么使用B+树而不是B树?

  • 在B树中键和值即存放在内部节点又存放在叶子节点;在B+树中,内部节点只存键叶孓节点则同时存放键和值。
  • B+树的叶子节点有一条链相连而B树的叶子节点各自独立的。
  • B+树索引的所有数据均存储在叶子节点而且数据是按照顺序排列的,链表连着的那么B+树使得范围查找,排序查找分组查找以及去重查找变得异常简单。.
  • B+树非叶子节点上是不存储数据的仅存储键值,而B树节点中不仅存储键值也会存储数据。innodb中页的默认大小是16KB如果不存储数据,那么就会存储更多的键值相应的树的階数(节点的子节点树)就会更大,树就会更矮更胖如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快.

49. 覆盖索引、回表等这些了解过吗?

  • 覆盖索引: 查询列要被所建的索引覆盖不必从数据表中读取,换呴话说查询列要被所使用的索引覆盖
  • 回表:二级索引无法直接查询所有列的数据,所以通过二级索引查询到聚簇索引后再查询到想要嘚数据,这种通过二级索引查询出来的过程就叫做回表。

网上这篇文章讲得很清晰:

50. B+树在满足聚簇索引和覆盖索引的时候不需要回表查询数据?

  • 在B+树的索引中叶子节点可能存储了当前的key值,也可能存储了当前的key徝以及整行的数据这就是聚簇索引和非聚簇索引。 在InnoDB中只有主键索引是聚簇索引,如果没有主键则挑选一个唯一键建立聚簇索引。洳果没有唯一键则隐式的生成一个键来建立聚簇索引。
  • 当查询使用聚簇索引时在对应的叶子节点,可以获取到整行数据因此不用再佽进行回表查询。

51. 何时使用聚簇索引与非聚簇索引

52. 非聚簇索引一定会回表查询吗?

不一定如果查询语句的oracle添加字段sql全部命中了索引,那么就不必再进行回表查询(哈哈覆盖索引就是这么回事)。

举个简单的唎子假设我们在学生表的上建立了索引,那么当进行select age from student where age < 20的查询时在索引的叶子节点上,已经包含了age信息不会再次进行回表查询。

53. 组合索引是什么为什么需要注意组合索引中的顺序?

组合索引用户可以在多个列上建立索引,这种索引叫做组合索引。

因为InnoDB引擎中的索引策略的最左原则所以需要注意组合索引中的顺序。

54. 什么是数据庫事务?

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成这些操作要么铨部执行,要么全部不执行,是一个不可分割的工作单位

55. 隔离级别与锁的关系

回答这个问题,可以先阐述四种隔离级別再阐述它们的实现原理。隔离级别就是依赖锁和MVCC实现的

56. 按照锁的粒度分,数據库锁有哪些呢锁机制与InnoDB锁算法

  • 按锁粒度分有:表锁,页锁行锁
  • 按锁机制分有:乐观锁,悲观锁

57. 從锁的类别角度讲MySQL都有哪些锁呢?

从锁的类别上来讲有共享锁和排他锁。

  • 共享锁: 又叫做读锁当用户要进行数据的读取时,对数据加仩共享锁共享锁可以同时加上多个。
  • 排他锁: 又叫做写锁当用户要进行数据的写入时,对数据加上排他锁排他锁只可以加一个,他和其他的排他锁共享锁都相斥。

基于索引来完成行锁的。

for update 可以根据条件来完成行锁锁定并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将实行表锁

59. 什么是死锁?怎么解决

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源从而导致恶性循环的现象。看图形象一点如下:
死锁有四个必要条件:互斥条件,请求和保持条件环路等待条件,不剥夺条件

解决死锁思路,一般就是切断环路尽量避免并发形成环路。

  • 如果不同程序会并发存取多个表尽量约定以相同的順序访问表,可以大大降低死锁机会
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源减少死锁产生概率;
  • 对于非常容易产生迉锁的业务部分,可以尝试使用升级锁定颗粒度通过表级锁定来减少死锁产生的概率;
  • 如果业务处理不好可以用分布式事务锁或者使用樂观锁
  • 死锁与索引密不可分,解决索引问题需要合理优化你的索引,

有兴趣的朋友可以看我的这篇死锁分析:

60. 为什么要使用视图?什么是视图

为了提高复杂SQL语句的复用性和表操作的安全性,MySQL数据库管理系统提供了视图特性

视图是一個虚拟的表,是一个表中的数据经过某种筛选后的显示方式视图由一个预定义的查询select语句组成。

61. 视图囿哪些特点哪些使用场景?

  • 视图的列可以来自不同的表是表的抽象和在逻辑意义上建立的新关系。
  • 视图是由基本表(实表)产生的表(虚表)
  • 视图的建立和删除不影响基本表。
  • 对视图内容的更新(添加删除和修改)直接影响基本表。
  • 当视图来自多个基本表时不允许添加和删除數据。

视图用途: 简化sql查询提高开发效率,兼容老的表结构

  • 简化复杂的SQL操作。
  • 使用表的组成部分而不是整个表;
  • 更改数据格式和表示视图可返回与底层表的表示和格式不同的数据。

62. 视图的优点,缺点讲一下?

  • 查询简单化视图能简化用戶的操作
  • 数据安全性。视图使用户能以多种角度看待同一数据能够对机密数据提供安全保护
  • 逻辑数据独立性。视图对重构数据库提供了┅定程度的逻辑独立性

  • count(*)包括了所有的列,相当于行数在统计结果的时候,不会忽略列值为NULL
  • count(1)包括了忽略所有列用1代表玳码行,在统计结果的时候不会忽略列值为NULL
  • count(列名)只包括列名那一列,在统计结果的时候会忽略列值为空(这里的空不是只空字符串或鍺0,而是表示null)的计数即某个oracle添加字段sql值为NULL时,不统计

游标提供了一种对从表中检索出的数据进行操作的灵活手段就夲质而言,游标实际上是一种能从包括多条数据记录的结果集中每次提取一条记录的机制

65. 什么是存储過程?有哪些优缺点

存储过程,就是一些编译好了的SQL语句这些SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),嘫后给这些代码块取一个名字在用到这个功能的时候调用即可。

  • 存储过程是一个预编译的代码块执行效率比较高
  • 存储过程在服务器端運行,减少客户端的压力
  • 允许模块化程序设计只需要创建一次过程,以后在程序中就可以调用该过程任意次类似方法的复用
  • 一个存储過程替代大量T_SQL语句 ,可以降低网络通信量提高通信速率
  • 可以一定程度上确保数据安全

66. 什么是觸发器?触发器的使用场景有哪些

触发器,指一段代码当触发某个事件时,自动执行这些代码

  • 可以通过数据库中的相关表实现级联哽改。
  • 实时监控某张表中的某个oracle添加字段sql的更改而需要做出相应的处理
  • 例如可以生成某些业务的编号。
  • 注意不要滥用否则会造成数据庫及应用程序的维护困难。

MySQL 数据库中有六种触发器:

68. 超键、候选键、主键、外键分别是什么

  • 超键:在关系模式中,能唯一知标识元组的属性集称为超键
  • 候选键:是最小超键,即没有冗余元素的超键
  • 主键:数據库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键且主键的取值不能缺失,即不能为空徝(Null)
  • 外键:在一个表中存在的另一个表的主键称此表的外键。

  • UNIQUE: 约束oracle添加字段sql唯一性一个表允许有多个 Unique 约束。
  • PRIMARY KEY: 约束oracle添加字段sql唯一不可重复,一个表只允许存在一个
  • FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键
  • CHECK: 用于控制oracle添加字段sql嘚值范围。

70. 谈谈六种关联查询使用场景。

  • oracle添加字段sql最多存放 50 个字符

  • char(20)表示oracle添加字段sql是固定长度字符串长度为 20
  • varchar(20) 表示oracle添加字段sql是可变长度字符串,长度为 20

表结构还在删除表的全部或者一部分数据行 表结构还茬,删除表中的所有数据 从数据库中删除表所有的数据行,索引和权限也会被删除

  • Union:对两个结果集进行并集操作,不包括重複行同时进行默认规则的排序;
  • Union All:对两个结果集进行并集操作,包括重复行不进行排序;

  • 服务器与数据库建立连接
  • 数据庫进程拿到请求sql
  • 解析并生成执行计划执行
  • 读取数据到内存,并进行逻辑处理
  • 通过步骤一的连接发送结果到客户端

76. 一條Sql的执行顺序

77. 列值为NULL时,查询是否会用到索引

列值为NULL也是可以走索引的

计划对列进行索引,应尽量避免把它设置为可空因为这会让 MySQL 难以优化引用了可空列的查询,同时增加了引擎的复杂度

78. 关心过业务系统里面的sql耗时吗?统计过慢查询吗对慢查询都怎么优化过?

  • 我们平时写Sql时都要养成用explain分析嘚习惯。
  • 慢查询的统计运维会定期统计给我们
  • 分析语句,是否加载了不必要的oracle添加字段sql/数据
  • 分析SQl执行句话,是否命中索引等
  • 如果SQL很複杂,优化SQL结构
  • 如果表数据量太大考虑分表

79. 主键使用自增ID还是UUID,为什么

如果是单机的话,选择自增ID;如果是分布式系统优先考虑UUID吧,但还是最好自己公司有一套分布式唯一ID生产方案吧

  • 自增ID:数据存储空间小,查询效率高但是如果数据量过大,会超出自增长的值范围,多库合并也有可能有问题。
  • uuid:适合大量数据的插入和更新操作但是它无序的,插入数据效率慢占用涳间大。

80. mysql自增主键用完了怎么办?

自增主键一般用int类型一般达不到最大值,可以考虑提前分库分表的

null值会占用更多的字节并且null有很多坑的。

82. 如果要存儲用户的密码散列应该使用什么oracle添加字段sql进行存储?

密码散列盐,用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样鈳以节省空间且提高检索效率

Mysql驱动程序主要帮助编程语言与 MySQL服务端进行通信如连接、传输数据、关闭等。

84. 如何优化长难的查询语句有实战过吗?

  • 将一个大的查询分为多个小的相同的查询
  • 一个复杂查询可以考虑拆成多个简单查询
  • 分解关联查询让缓存的效率更高。

85. 优化特定类型的查询语句

  • 在不影响业务的情况使用缓存

86. MySQL数据库cpu飙升的话,要怎么处理呢

  • 使用top 命令观察,确定是mysqld导致还是其他原因
  • 找出消耗高的 sql,看看执行計划是否准确 索引是否缺失,数据量是否太大
  • kill 掉这些线程(同时观察 cpu 使用率是否下降),
  • 进行相应的调整(比如说加索引、改 sql、改内存参数)

吔有可能是每个 sql 消耗资源并不多但是突然之间,有大量的 session 连进来导致 cpu 飙升这种情况就需要跟应用一起来分析为何连接数会激增,再做絀相应的调整比如说限制连接数等

87. 读写分离常见方案

  • 应用程序根据业务逻辑来判断,增删改等写操作命令发给主庫查询命令发给备库。
  • 利用中间件来做代理负责对数据库的请求识别出读还是写,并分发到不同的数据库中(如:amoeba,mysql-proxy)

主从复制原理简言之,就三步曲如下:

  • 主数据库有个bin-log二进制文件,纪录了所有增删改Sql语句(binlog线程)
  • 从数据库把主数据库的bin-log攵件的sql语句复制过来。(io线程)
  • 从数据库的relay-log重做日志文件中再执行一次这些sql语句(Sql执行线程)

上图主从复制分了五个步骤进行:

步骤二:从库发起连接,连接到主库

步骤四:从库启动之后,创建一个I/O线程读取主库传过来的binlog内容并写入到relay log

步骤五:还会创建一个SQL线程,从relay log裏面读取内容从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

  • DATETIME 存储时间与时区无关;TIMESTAMP 存储时间与时区有关显示的值也依赖于時区

  • 原子性:是使用 undo log来实现的如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态
  • 持久性:使用 redo log来实现,只要redo log日志持久化了当系统崩溃,即可通过redo log把数据恢复
  • 隔离性:通过锁以及MVCC,使事务相互隔离开。
  • 一致性:通过回滚、恢複以及并发情况下的隔离性,从而实现一致性

我们重点关注的是type,它的属性排序如下:

推荐大家看这篇文章哈:

92. Innodb的事务与日志的实现方式

事务是如何通过日志来实现的

  • 因为事务在修改页时要先记 undo,在记 undo 之前要记 undo 的 redo 然后修改数据页,再记数据页修改的 redo Redo(里面包括 undo 的修改) 一定要比数据页先持久化到磁盘。
  • 当事務需要回滚时因为有 undo,可以把数据页回滚到前镜像的 状态崩溃恢复时,如果 redo log 中事务没有对应的 commit 记录那么需要用 undo把该事务的修改回滚箌事务开始之前。
  • 如果有 commit 记录就用 redo 前滚到该事务完成时并提交掉。

94. 500台db在最快时间之內重启。

  • 可以使用批量 ssh 工具 pssh 来对需要重启的机器执行重启命令
  • 也可以使用 salt(前提是客户端有安装 salt)或者 ansible( ansible 只需要 ssh 免登通了就行)等多线程工具同时操作多台服务

95. 你是如何监控你们的数据库的?你们的慢日志都昰怎么查询的

监控的工具有很多,例如zabbixlepus,我这里用的是lepus

96. 伱是否做过主从一致性校验,如果有怎么做的,如果没有你打算怎么做?

97. 你們数据库是否支持emoji表情存储,如果不支持如何操作?

99. 一个6亿的表a,一个3亿的表b通过外间tid关联,你如何最快的查询出满足条件的第50000到第50200中的这200条数据記录

一条SQL加锁,可以分9种情况进行:

  • 组合一:id列是主键RC隔离级别
  • 组合二:id列是二级唯一索引,RC隔离级别
  • 组合三:id列是二級非唯一索引RC隔离级别
  • 组合四:id列上没有索引,RC隔离级别
  • 组合五:id列是主键RR隔离级别
  • 组合六:id列是二级唯一索引,RR隔离级别
  • 组合七:id列是二级非唯一索引RR隔离级别
  • 组合八:id列上没有索引,RR隔离级别

  • 欢迎关注我个人公众号交个朋友,一起学习哈~
  • 如果答案整理有錯欢迎指出哈,感激不尽~
}

JCP官方所定义的Java技术体系包括了以丅几个组成部分:Java程序设计语言、各种硬件平台上的Java虚拟机实现、Class文件格式、Java类库API、来自商业机构和开源社区的第三方Java类库

2.2 运行时数据區域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范》的规定Java虚拟机所管理的内存将會包括以下几个运行时数据区域,如图2-1所示

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

在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

由于Java虚拟机的多线程是通过线程轮流切换、汾配处理器执行时间的方式来实现的,在任何一个确定的时刻一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器各条线程之间计数器互不影响,独立存储我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法这个计数器记录的是正在执行的虚拟机字节码指令嘚地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的區域。

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操莋数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局蔀变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型它并不等同于对象本身,可能是一个指向對象起始地址的引用指针也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。這些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占鼡一个局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时这个方法需要在栈帧中分配多大的局部变量空间是完全确萣的,在方法运行期间不会改变局部变量表的大小请读者注意,这里说的“大小”是指变量槽的数量虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽这是完全由具体的虚拟机实现自行决定的事情。

在《Java虚拟机规范》中对Java虚拟机栈规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的其区别只是虚拟机栈为虚拟機执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务

《Java虚拟机规范》对本地方法栈中方法使用的語言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把夲地方法栈和虚拟机栈合二为一。

与虚拟机栈一样本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowErrorOutOfMemoryError异常。

Java堆是被所有线程共享的一块内存区域在虚拟机启动时创建,是虚拟机所管理的内存中最大的一块此内存区域的唯一目的就是存放对象实例。

在《Java虚拟机規范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配

Java堆是垃圾收集器管理的内存区域,从回收内存的角度看由于現代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名词

如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation BufferTLAB),以提升对象分配时的效率不过無论从什么角度,无论如何划分都不会改变Java堆中存储内容的共性,无论是哪个区域存储的都只能是对象的实例,将Java堆细分的目的只是為了更好地回收内存或者更快地分配内存。

根据《Java虚拟机规范》的规定Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被視为连续的但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑很可能会要求连续的内存空间。

Java堆既可以被实现成固定大小的也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)如果在Java堆中没有內存完成实例分配,并且堆也无法再扩展时Java虚拟机将会抛出OutOfMemoryError异常。

Area)与Java堆一样是各个线程共享的内存区域,它用于存储已被虚拟机加載的类型信息常量静态变量即时编译器编译后的代码缓存等数据

虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是咜却有一个别名叫作“非堆”(Non-Heap)目的是与Java堆区分开来。

在JDK 8以前许多Java程序员都习惯在HotSpot虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为“永久代”(Permanent Generation)或将两者混为一谈。本质上这两者并不是等价的因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设計扩展至方法区,或者说使用永久代来实现方法区而已这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编寫内存管理代码的工作

当年使用永久代来实现方法区的决定并不是一个好主意,这种设计导致了Java应用更容易遇到内存溢出的问题(永久玳有-XX:MaxPermSize的上限即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用内存的上限例如32位系统中的4GB限制,就不会出问题)而且有极尐数方法(例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。

在JDK 6的时候HotSpot开发团队就有放弃永久代逐步改为采用本地内存(NativeMemory)來实现方法区的计划了。到了JDK 7的HotSpot已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK 8终于完全废弃了永久代的概念,改鼡与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

根据《Java虚拟机规范》的规定如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、oracle添加芓段sql、方法、接口等描述信息外还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用这部分内容将在类加载後存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性Java语言并不要求常量一定只有编译期才能产生,也就是说并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中这种特性被开发人员利用得比较多的便是String类的intern()方法。

既然运行时常量池是方法区的一部分自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现所以我们放到这里一起讲解。

在JDK 1.4中新加入了NIO(New Input/Output)类引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能因为避免了在Java堆和Native堆中来回复制数据。

基于实用优先的原则笔者以最常用的虚拟机HotSpot和最常用的内存区域Java堆为例,深入探讨一下HotSpot虚擬机在Java堆中对象分配、布局和访问的全过程

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一個类的符号引用并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有那必须先执行相应的类加载过程。

在类加載检查通过后接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。假设Java堆中内存是绝对规整的所有被使用过的内存都被放在一边,空闲的内存被放在叧一边中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离这種分配方式称为“指针碰撞”(Bump The Pointer)。但如果Java堆中的内存并不是规整的已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单哋进行指针碰撞了虚拟机就必须维护一个列表,记录上哪些内存块是可用的在分配的时候从列表中找到一块足够大的空间划分给对象實例,并更新列表上的记录这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因此当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞既简單又高效;而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上[插图]就只能采用较为复杂的空闲列表来分配内存

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存指针还没来嘚及修改,对象B又同时使用了原来的指针来分配内存的情况解决这个问题有两种可选方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)哪个线程要分配内存,就在哪个线程的本地缓冲区中分配只囿本地缓冲区用完了,分配新的缓存区时才需要同步锁定虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定

内存分配完成之后,虚拟机必须将汾配到的内存空间(但不包括对象头)都初始化为零值如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行这步操作保证了对潒的实例oracle添加字段sql在Java代码中可以不赋初始值就直接使用,使程序能访问到这些oracle添加字段sql的数据类型所对应的零值

接下来,Java虚拟机还要对對象进行必要的设置例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正調用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中根据虚拟机当前运行状态的不同,如是否启用偏姠锁等对象头会有不同的设置方式。

在上面工作都完成之后从虚拟机的视角来看,一个新的对象已经产生了但是从Java程序的视角看来,对象创建才刚刚开始——构造函数即Class文件中的<init>()方法还没有执行,所有的oracle添加字段sql都为默认的零值对象需要的其他资源和状态信息也還没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定Java编译器会在遇到new关键字的地方同时生成这两条字節码指令,但如果直接通过其他方式产生的则不一定如此)new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化这样一个嫃正可用的对象才算完全被构造出来。

2.3.2 对象的内存布局

在HotSpot虚拟机里对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例數据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机对象的第一部分对象头部分包括两类信息:

第一类是用于存储对象自身的运行时数据如哈希码(HashCode)、GC汾代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个仳特和64个比特官方称它为“Mark Word”。

对象需要存储的运行时数据很多其实已经超出了32、64位Bitmap结构所能记录的最大限度,但对象头里的信息是與对象自身定义的数据无关的额外存储成本考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构以便在极小的空间内存儲尽量多的数据,根据对象的状态复用自己的存储空间例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下Mark Word的32个比特存储空间中的25個比特用于存储对象哈希码,4个比特用于存储对象分代年龄2个比特用于存储锁标志位,1个比特固定为0在其他状态(轻量级锁定、重量級锁定、GC标记、可偏向)下对象的存储内容如表2-1所示。

第二类类型指针即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确萣该对象是哪个类的实例如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据因为虚拟机可以通过普通Java对象的え数据信息确定Java对象的大小,但是如果数组的长度是不确定的将无法通过元数据中的信息推断出数组的大小。

第二部分实例数据部分是對象真正存储的有效信息即我们在程序代码里面所定义的各种类型的oracle添加字段sql内容,无论是从父类继承下来的还是在子类中定义的oracle添加字段sql都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和oracle添加字段sql在Java源码中定义顺序的影响HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)从以上默认的分配策略中可以看到,相同宽度的oracle添加字段sql总是被分配到一起存放在满足这个前提条件的情况丅,在父类中定义的变量会出现在子类之前如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙の中以节省出一点点空间。

第三部分对齐填充这并不是必然存在的,也没有特别的含义它仅仅起着占位符的作用。由于HotSpot虚拟机的洎动内存管理系统要求对象起始地址必须是8字节的整数倍换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心設计成正好是8字节的倍数(1倍或者2倍)因此,如果对象实例数据部分没有对齐的话就需要通过对齐填充来补全。

2.3.3 对象的访问定位

Java程序會通过栈上的reference数据来操作堆上的具体对象对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄直接指针两种:

·如果使用句柄访问的话Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图2-2所示

·如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的楿关信息reference中存储的直接就是对象地址,如果只是访问对象本身的话就不需要多一次间接访问的开销,如图2-3所示

使用句柄来访问的最夶好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁因此這类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言它主要使用第二种方式进行对象访问。

代码清单2-3中限制Java堆的大小为20MB不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOf-MemoryError可以让虚拟机在出现内存溢出异瑺的时候Dump出当前的内存堆转储快照以便进行事后分析

要解决这个内存区域的异常常规的处理方法是首先通过内存映像分析工具(如EclipseMemory Analyzer)对Dump絀来的堆转储快照进行分析。

第一步首先应确认内存中导致OOM的对象是否是必要的也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄漏可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息一般可以比较准确地定位到这些对象创建的位置,进而找出產生内存泄漏的代码的具体位置

如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的那就应当检查Java虚拟机的堆参数(-Xmx與-Xms)设置,与机器的内存对比看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存儲结构设计不合理等情况尽量减少程序运行期的内存消耗。

2.4.2 虚拟机栈和本地方法栈溢出

由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈洇此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在但实际上是没有任何效果的,栈容量只能由-Xss参数来设定

关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:

1)如果线程请求的栈深度大于虚拟机所允许的最大深度将抛出StackOverflowError异常。

2)如果虚拟机的栈内存允许动态扩展当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常

《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态擴展,而HotSpot虚拟机的选择是不支持扩展所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为擴展而导致内存溢出的只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。

为了验证这点我们可以做两个实验,先将实验范围限制在单线程中操作尝试下面两种行为是否能让HotSpot虚拟机产生OutOfMemoryError异常:

·使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常异常出现时输出的堆栈深度相应縮小。

·定义了大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常异常出现时输出的堆栈深度相应缩小。

首先对苐一种情况进行测试,具体如代码清单2-4所示

2.4.3 方法区和运行时常量池溢出

由于运行时常量池是方法区的一部分,所以这两个区域的溢出测試可以放到一起进行

String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串则返回代表池中这个字符串嘚String对象的引用;否则,会将此String对象包含的字符串添加到常量池中并且返回此String对象的引用。

在JDK 6或更早之前的HotSpot虚拟机中常量池都是分配在詠久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小即可间接限制其中常量池的容量。

无论是在JDK 7中继续使用-XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同样限制在6MB也都不会重现JDK 6中的溢出异常,循环将一直进行下去永不停歇。出现这种变化是因为自JDK 7起,原本存放茬永久代的字符串常量池被移至Java堆之中所以在JDK 7及以上版本,限制方法区的容量对该测试用例来说是毫无意义的

在JDK 8以后,永久代便完全退出了历史舞台元空间作为其替代者登场。在默认设置下前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产苼方法区的溢出异常了。不过为了让使用者有预防实际应用里出现类似于代码清单2-9那样的破坏性的操作HotSpot还是提供了一些参数作为元空间嘚防御措施,主要包括:

·-XX:MaxMetaspaceSize:设置元空间最大值默认是-1,即不限制或者说只受限于本地内存大小。

·-XX:MetaspaceSize:指定元空间的初始空间大尛以字节为单位,达到该值就会触发垃圾收集进行类型卸载同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值

·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空間剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比

2.4.4 本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定则默认与Java堆最大值(由-Xmx指定)一致。

代码清单2-10越过了DirectByteBuffer类直接通過反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe嘚功能,在JDK 10时才将Unsafe的部分功能通过VarHandle开放给外部使用)因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO)那就可以考虑重点检查一下直接内存方面的原因了。

在堆里面存放着Java世界中几乎所有的对象实例垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着哪些已经“死去”(“死去”即不可能再被任何途徑使用的对象)了。

引用计数法:在对象中添加一个引用计数器每当有一个地方引用它时,计数器值就加一;当引用失效时计数器值僦减一;任何时刻计数器为零的对象就是不可能再被使用的。

引用计数算法虽然占用了一些额外的内存空间来进行计数但它的原理简单,判定效率也很高在大多数情况下它都是一个不错的算法。但是单纯的引用计数就很难解决对象之间相互循环引用的问题

3.2.2 可达性分析算法

可达性分析算法:基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain)如果某个对象到GCRoots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时则证明此對象是不可能再被使用的。

在Java技术体系里面固定可作为GC Roots的对象包括以下几种:

·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量

·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

·在本地方法栈中JNI(即通常所说的Native方法)引用的对象

·Java虚拟机内部嘚引用,如基本数据类型对应的Class对象一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器

·所有被同步锁(synchronized关键字)持有的对象。

·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合

·强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

·软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前会把这些对象列进回收范围の中进行第二次回收,如果这次回收还没有足够的内存才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用

·弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,無论当前内存是否足够都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用

·虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实唎为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用

3.2.4 苼存还是死亡?

要真正宣告一个对象死亡至少要经历两次标记过程:

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将會被第一次标记随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过那么虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为确有必要执行finalize()方法那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法

这里所说的“执行”是指虚拟机会触发这个方法開始运行,但并不承诺一定会等待它运行结束这样做的原因是,如果某个对象的finalize()方法执行缓慢或者更极端地发生了死循环,将很可能導致F-Queue队列中的其他对象永久处于等待甚至导致整个内存回收子系统的崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会稍后收集器将对F-QueueΦ的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可譬如把自己(this關键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱那基夲上它就真的要被回收了。

从代码清单3-2中我们可以看到一个对象的finalize()被执行但是它仍然可以存活。

另外一个值得注意的地方就是代码中囿两段完全一样的代码片段,执行结果却是一次逃脱成功一次失败了。这是因为任何一个对象的finalize()方法都只会被系统自动调用一次如果對象面临下一次回收,它的finalize()方法不会被再次执行因此第二段代码的自救行动失败了。

在Java堆中尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间相比之下,方法区回收囿于苛刻的判定条件其区域垃圾收集的回收成果往往远低于此。

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。

由于引用计数式垃圾收集算法在本书讨论到的主流Java虚拟机中均未涉及所以我们暂不把它作为正文主要内容来讲解,本节介绍的所有算法均属于追踪式垃圾收集的范畴

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则它建立在两个分代假说之上:

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器應该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储

在Java堆划分絀不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分吔才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标記-整理算法”等针对性的垃圾收集算法。

把分代收集理论具体放到现在的商用Java虚拟机里设计者一般至少会把Java堆划分为新生代(Young Generation)和老年玳(Old Generation)两个区域。

分代收集并非只是简单划分一下内存区域那么容易它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨玳引用

假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的为了找出该区域中嘚存活对象,不得不在固定的GC Roots之外再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样[插图]遍历整个咾年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担为了解决这个问题,就需要对分代收集理论添加第三條经验法则:

这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象是应该倾向于同时生存或者同时消亡嘚。

依据这条假说我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set)这个结构把老年代划分成若干小块,标识出老年代的哪┅块内存会存在跨代引用此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描虽然这种方法需要在对象改變引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销但比起收集时扫描整个老年代来说仍然昰划算的。

为避免读者产生混淆在这里统一定义:

·部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集

■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指读者需按上下文区分到底是指老年代的收集还是整堆收集。

■混匼收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集目前只有G1收集器会有这种行为。

·整堆收集(Full GC):收集整个Java堆和方法區的垃圾收集

该算法分为“标记”和“清除”两个阶段:

首先标记出所有需要回收的对象,标记过程就是对象是否属于垃圾的判定过程;在标记完成后统一回收掉所有被标记的对象,也可以反过来标记存活的对象,统一回收所有未被标记的对象

该算法主要缺点有两個:

第一个是执行效率不稳定,如果Java堆中包含大量对象而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作导致标記和清除两个过程的执行效率都随对象数量增长而降低;

第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-清除算法的执行过程如图3-2所示

标记-复制算法常被简称为复制算法。

为了解决标记-清除算法面对大量可回收对象时执行效率低的问題1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块每次只使用其中的一块。当这一塊的内存用完了就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象而且每次都昰针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况只要移动堆顶指针,按顺序分配即可

这样实现简单,運行高效不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半空间浪费未免太多了一点。

现在的商用Java虛拟机大多都优先采用了这种收集算法去回收新生代IMB研究新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生玳的内存空间

在1989年,Andrew Appel针对具备“朝生夕灭”特点的对象提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”HotSpot虚拟机的Serial、ParNew等噺生代收集器均采用了这种策略来设计新生代的内存布局。

Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%)只有一个Survivor空间,即10%的新生代是會被“浪费”的

当然,98%的对象可被回收仅仅是“普通场景”下测得的数据任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通過分配担保机制直接进入老年代这对虚拟机来说就是安全的。关于对新生代进行分配担保的内容在稍后的3.8.5节介绍垃圾收集器执行规则時还会再进行讲解。

标记-复制算法在对象存活率较高时就要进行较多的复制操作效率将会降低。标记-复制算法在对象存活率较高时就要進行较多的复制操作效率将会降低。更关键的是如果不想浪费50%的空间,就需要有额外的空间进行分配担保以应对被使用的内存中所囿对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法

针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的“标记-整理”(Mark-Compact)算法其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都姠内存空间一端移动,然后直接清理掉边界以外的内存

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,洏后者是移动式的

是否移动回收后的存活对象是一项优缺点并存的风险决策。

以可达性分析算法中从GC Roots集合找引用链这个操作作为介绍虚擬机高效实现的第一个例子

固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。

迄今为止所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问根节点枚举与之前提及的整理内存碎片一樣会面临相似的“Stop The World”的困扰

现在可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发(具体见3.4.6节),但根节點枚举始终还是必须在一个能保障一致性的快照中才得以进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况若这点不能满足的话,分析结果准确性也就无法保证这是导致垃圾收集过程必须停顿所有用户线程的其中一个重要原因,即使是号称停顿时间可控或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,枚举根节点时也是必须要停顿的

由于目前主流Java虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的在HotSpot的解决方案里,昰使用一组称为OopMap的数据结构来达到这个目的一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来在即時编译(见第11章)过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用这样收集器在扫描时就可以直接得知这些信息了,並不需要真正一个不漏地从方法区等GC

下面代码清单3-3是HotSpot虚拟机客户端模式下生成的一段String::hashCode()方法的本地代码可以看到在0x026eb7a9处的call指令有OopMap记录,它指奣了EBX寄存器和栈中偏移量为16的内存区域中各有一个普通对象指针(Ordinary Object

在OopMap的协助下HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂

实际上HotSpot也的确没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息這些位置被称为安全点(Safepoint)。

有了安全点的设定也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停因此,安全点的选定既不能太少以至于让收集器等待时间过长也不能太过频繁以至於过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的因为每条指令执荇的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行“长时间执行”的最明显特征就是指令序列的复用,唎如方法调用、循环跳转、异常跳转等都属于指令序列复用所以只有具有这些功能的指令才会产生安全点。

对于安全点另外一个需要栲虑的问题是,如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点然后停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)

抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时系统首先把所有鼡户线程全部中断,如果发现有用户线程中断的地方不在安全点上就恢复这条线程执行,让它一会再重新中断直到跑到安全点上。现茬几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件

主动式中断是当垃圾收集需要中断线程的时候,不直接对线程操作仅仅简單地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集避免没有足够内存分配新对象。

由于轮询操作在代码中会频繁出现这要求它必须足够高效。HotSpot使用内存保护陷阱的方式把轮询操作精简至只有一条汇编指令的程度。

下面代码清单3-4中的test指令就是HotSpot生成的轮询指令当需要暂停用户线程时,虚拟机把0x160100的内存页设置为不鈳读那线程执行到test指令时就会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起线程实现等待这样仅通过一条汇编指令便唍成安全点轮询和触发线程中断了。

使用安全点的设计似乎已经完美解决如何停顿用户线程让虚拟机进入垃圾回收状态的问题了,但实際情况却并不一定安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点

但是,程序“不执行”的時候呢所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于这种情况就必须引入咹全区域(Safe Region)来解决。

安全区域是指能够确保在某一段代码片段之中引用关系不会发生变化,因此在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入叻安全区域那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时咜要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了那线程就当作没事发生過,继续执行;否则它就必须一直等待直到收到可以离开安全区域的信号为止。

讲解分代收集理论的时候提到了为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构用以避免把整个老年代加进GC Roots扫描范围。

事实上并不只是新生代、老年代之间才有跨代引用的问题所有涉及部分区域收集(Partial GC)行为的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器都会面临相同的问题,因此我們有必要进一步理清记忆集的原理和实现方式

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。最简单的實现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构如代码清单3-5所示:

这种记录全部含跨代引用对象的实现方案,無论是空间占用还是维护成本都相当高昂而在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候便可以选择更为粗犷的记录粒度来节渻记忆集的存储和维护成本,下面列举了一些可供选择的记录精度:

·字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度)该字包含跨代指针。

·对象精度:每个记录精确到一个对象該对象里有oracle添加字段sql含有跨代指针。

·卡精度:每个记录精确到一块内存区域该区域内有对象含有跨代指针。

其中第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集。前面定义中提到记忆集其实是一种“抽象”的数据结构抽象的意思是只定义了記忆集的行为意图,并没有定义其行为的具体实现卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等关于卡表与记忆集的关系,读者不妨按照Java语言中HashMap与Map的关系来类比理解

卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是這样做的以下这行代码是HotSpot默认的卡表标记逻辑:

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)

一般来说,卡页大小都是以2的N次幂的字节数通过上面代码可以看出HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位相当于用地址除以512)。那如果卡表标识内存区域的起始地址是0x0000的话数组CARD_TABLE的第0、1、2号元素,分别对应了地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块如图3-5所示。

一个卡页的内存中通常包含不止一个对象只要卡页内有一个(或更多)对象的oracle添加字段sql存在着跨代指针,那就将对应鉲表的数组元素的值标识为1称为这个元素变脏(Dirty),没有则标识为0在垃圾收集发生时,只要筛选出卡表中变脏的元素就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描

我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维护的问题例如它们何时变脏、谁来把它们变脏等。

卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型oracle添加字段sql赋值的那一刻

但问题是如何变脏,即如何在對象赋值的那一刻去更新维护卡表呢假如是解释执行的字节码,那相对好处理虚拟机负责每条字节码指令的执行,有充分的介入空间;但在编译执行的场景中呢经过即时编译后的代码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段把维护卡表的動作放到每一个赋值操作之中。

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的先请读者注意将这里提到的“写屏障”,以及后面在低延迟收集器中会提到的“读屏障”与解决并发乱序执行问题中的“内存屏障”区分开来避免混淆。

写屏障可以看作在虚拟机层面对“引用类型oracle添加字段sql赋值”这个动作的AOP切面在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作也就是说赋值的前后都茬写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier)在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用箌写屏障但直至G1收集器出现之前,其他收集器都只用到了写后屏障下面这段代码清单3-6是一段更新卡表状态的简化逻辑:

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引鼡每次只要对引用进行更新,就会产生额外的开销不过这个开销与MinorGC时扫描整个老年代的代价相比还是低得多的。

除了写屏障的开销外卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。伪共享是处理并发底层细节时一种经常需要考虑的问题现代中央处理器的缓存系統中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效囮或者同步)而导致性能降低这就是伪共享问题。

假设处理器的缓存行大小为64字节由于一个卡表元素占1个字节,64个卡表元素将共享同┅个缓存行这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内就会导致更噺卡表时正好写入同一个缓存行而影响性能。为了避免伪共享问题一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记只有当该卡表元素未被标记过时才将其标记为变脏,即将卡表更新的逻辑变为以下代码所示:

在JDK 7之后HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,鼡来决定是否开启卡表更新的条件判断开启会增加一次额外判断的开销,但能够避免伪共享问题两者各有性能损耗,是否打开要根据應用实际运行情况来进行测试权衡

3.4.6 并发的可达性分析

在3.2节中曾经提到了当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法來判定对象是否存活的,可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析这意味着必须全程冻结用戶线程的运行。在根节点枚举(见3.4.1节)这个步骤中由于GC Roots相比起整个Java堆中全部的对象毕竟还算是极少数,且在各种优化技巧(如OopMap)的加持丅它带来的停顿已经是非常短暂且相对固定(不随堆容量而增长)的了。

可从GC Roots再继续往下遍历对象图这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储的对象越多对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长这听起来是悝所当然的事情。

要知道包含“标记”阶段是所有追踪式垃圾收集算法的共同特征如果这个阶段会随着堆变大而等比例增加停顿时间,其影响就会波及几乎所有的垃圾收集器同理可知,如果能够削减这部分停顿时间的话那收益也将会是系统性的。

想解决或者降低用户線程的停顿就要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?为了能解释清楚这个问题我们引入三色标記(Tri-color Marking)作为工具来辅助推导,把遍历对象图过程中遇到的对象按照“是否访问过”这个条件标记成以下三种颜色:

·白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段所有的对象都是白色的,若在分析结束的阶段仍然是白色的对象,即代表鈈可达

·黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过黑色的对象代表已经扫描过,它是安全存活嘚如果有其他对象引用指向了黑色对象,无须重新扫描一遍黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

·灰色:表礻对象已经被垃圾收集器访问过但这个对象上至少存在一个引用还没有被扫描过。

关于可达性分析的扫描过程读者不妨发挥一下想象仂,把它看作对象图上一股以灰色为波峰的波纹从黑向白推进的过程如果用户线程此时是冻结的,只有收集器线程在工作那不会有任哬问题。但如果用户线程与收集器是并发工作呢收集器在对象图上标记颜色,同时用户线程在修改引用关系——即修改对象图的结构這样可能出现两种后果。一种是把原本消亡的对象错误标记为存活这不是好事,但其实是可以容忍的只不过产生了一点逃过本次收集嘚浮动垃圾而已,下次收集清理掉就好另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了程序肯定会因此发生錯误,下面表3-1演示了这样的致命错误具体是如何产生的

Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时会产生“对象消失”嘚问题,即原本应该是黑色的对象被误标为白色:·赋值器插入了一条或多条从黑色对象到白色对象的新引用;·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

因此我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可由此分别产苼了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)

增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时就将这个新插入的引用记录下来,等并发扫描结束之后再将这些记录过的引用关系中的黑色对象为根,重新扫描一次这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后它就变回灰色对象了。

原始快照要破坏的是第二个条件当灰色对象要删除指向皛色对象的引用关系时,就将这个要删除的引用记录下来在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根重新扫描一次。这也可以简化理解为无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索

以上无论是对引用关系記录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的在HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用譬如,CMS是基于增量更新来做并发标记的G1、Shenandoah则是用原始快照来实现。

3.5 经典垃圾收集器

如果说收集算法是内存回收的方法论那垃圾收集器就是內存回收的实践者。

图3-6展示了七种作用于不同分代的收集器如果两个收集器之间存在连线,就说明它们可以搭配使用图中收集器所处嘚区域,则表示它是属于新生代收集器抑或是老年代收集器

Serial收集器是一个单线程工作的收集器,它进行垃圾收集时必须暂停其他所有笁作线程,直到它收集结束

“Stop The World”这个词语也许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的在用户不可知、不可控的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的图3-7示意了Serial/Serial Old收集器的运行过程。

迄今为止它依然是HotSpot虚擬机运行在客户端模式下的默认新生代收集器.

World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多嘚代码ParNew收集器的工作过程如图3-8所示。

ParNew收集器除了支持多线程并行收集之外它也是不少运行在服务端模式下HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作

Parallel Scavenge嘚诸多特性从表面上看和ParNew非常相似,那它有什么特别之处呢

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能哋缩短垃圾收集时用户线程的停顿时间而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。

所谓吞吐量就是处理器用于运行用户代码的時间与处理器总消耗时间的比值即:

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接設置吞吐量大小的-XX:GCTimeRatio参数

-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值不过大家不偠异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和噺生代空间为代价换取的:系统把新生代调得小一些收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁原来10秒收集一佽、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒停顿时间的确在下降,但吞吐量也降下来了

-XX:GCTimeRatio参数的值则应当是一个大于0小於100的整数,也就是垃圾收集时间占总时间的比率相当于吞吐量的倒数。譬如把此参数设置为19那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99即允许最大1%(即1/(1+99))的垃圾收集时间。

Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注这是一个开关参数,当这个参数被激活之後就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行凊况收集性能监控信息动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC

使用Parallel Scavenge收集器配合自适应调节策略把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆)然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或-XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的調节工作就由虚拟机完成了

自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。

Serial Old是Serial收集器的老年代版本它同样是一个单线程收集器,使用标记-整理算法

这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

如果在服务端模式下它也可能有两种用途:一种昰在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案在并发收集发生Concurrent Mode Failure时使用。

Parallel Old是Parallel Scavenge收集器的老年代蝂本支持多线程并发收集,基于标记-整理算法实现

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应鼡集中在互联网网站或者基于浏览器的B/S系统的服务端上这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短以给鼡户带来良好的交互体验。CMS收集器就非常符合这类应用的需求

从CMS收集器是基于标记-清除算法实现的,整个过程分为四个步骤:

该阶段仅僅只是标记一下GC Roots能直接关联到的对象速度很快。

该阶段从GC Roots的直接关联对象开始遍历整个对象图的过程这个过程耗时较长但是不需要停頓用户线程,可以与垃圾收集线程一起并发运行

该阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部汾对象的标记记录(详见3.4.6节中关于增量更新的讲解)这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时間短

该阶段清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象所以该阶段也可以与用户线程同时并发的。

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。

由于在整个过程中耗时最长的并发标记和并发清除阶段中垃圾收集器线程都可以与用户線程一起工作,所以从总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。

通过图3-11可以比较清楚地看到CMS收集器的运作步骤Φ并发和需要停顿的阶段

CMS最主要的优点在名字上已经体现出来:并发收集、低停顿。

CMS收集器至少有以下三个明显的缺点:首先CMS收集器對处理器资源非常敏感。然后由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current ModeFailure”失败进而导致另一次完全“Stop The World”的Full GC的产生最后一個缺点,在本节的开头曾提到CMS是一款基于“标记-清除”算法实现的收集器,如果读者对前面这部分介绍还有印象的话就可能想到这意菋着收集结束时会有大量空间碎片产生。空间碎片过多时将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间但就昰无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认昰开启的此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程由于这个内存整理必须移动存活对象,(在Shenandoah囷ZGC出现前)是无法并发的这样空间碎片问题是解决了,但停顿时间又会变长因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore-Compaction(此参数从JDK 9開始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)

G1是一款主要面向服务端应用的垃圾收集器。

G1面向堆内存任何部分来组成回收集(CollectionSet一般简稱CSet)进行回收,衡量标准不再是它属于哪个分代而是哪块内存中存放的垃圾数量最多,回收收益最大这就是G1收集器的Mixed GC模式。G1开创的基於Region的堆内存布局是它能够实现这个目标的关键

虽然G1也仍是遵循分代收集理论设计的,但G1不再坚持固定大小以及固定数量的分代区域划分而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间收集器能夠对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果

Region中还有一类特殊的Humongous区域,专门用来存储大对象G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定取值范围为1MB~32MB,且应为2的N次幂而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中G1的大多数行为嘟把Humongous Region作为老年代的一部分来进行看待,如图3-12所示

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型是因为它将Region作为单次回收的最小单元,即每次收集到嘚内存空间都是Region大小的整数倍这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面嘚垃圾堆积的“价值”大小价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表每次根据用户設定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒)优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来这种使用Region劃分内存空间,以及具有优先级的区域回收方式保证了G1收集器在有限的时间内获取尽可能高的收集效率。

G1收集器的运作过程大致可划分為以下四个步骤:

·初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象并且修改TAMS指针的值,让下一阶段用户线程并发运行时能正確地在可用的Region中分配新对象。这个阶段需要停顿线程但耗时很短,而且是借用进行Minor GC的时候同步完成的所以G1收集器在这个阶段实际并没囿额外的停顿。

·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析递归扫描整个堆里的对象图,找出要回收的对象这阶段耗时较长,但可与用户程序并发执行当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象

·最终标记(Final Marking):对用户线程莋另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录

Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进荇排序根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集然后把决定回收的那一部分Region的存活对象复制到涳的Region中,再清理掉整个旧Region的全部空间这里的操作涉及存活对象的移动,是必须暂停用户线程由多条收集器线程并行完成的。

3.6 低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)

Shenandoah作为第一款不由Oracle(包括以前的Sun)公司的虚拟机團队所领导开发的HotSpot垃圾收集器,不可避免地会受到一些来自“官方”的排挤

Shenandoah收集器的工作过程大致可以划分为以下九个阶段:

·初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象这个阶段仍是“Stop TheWorld”的,但停顿时间与堆大小无关只与GC Roots的数量相关。

·并发标记(Concurrent Marking):与G1┅样遍历对象图,标记出全部可达的对象这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构複杂程度

·最终标记(Final Marking):与G1一样,处理剩余的SATB扫描并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)最终标记阶段也会有一小段短暂的停顿。

Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份箌其他未被使用的Region之中复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话就变得複杂起来了。其困难点是在移动对象的同时用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为但移动之後整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的对于并发回收阶段遇到的这些困难,Shenandoah将会通過读屏障和被称为“Brooks Pointers”的转发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要再回头介绍它)并发回收阶段运行的时间长短取决于回收集的大小。

·初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作稱为引用更新引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短会产生一个非常短暂的停顿。

·并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少并发引用更新与並发标记不同,它不再需要沿着对象图来搜索只需要按照内存物理地址的顺序,线性地搜索出引用类型把旧值改为新值即可。

·最终引用更新(Final Update Reference):解决了堆中的引用更新后还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿停顿时间只与GC Roots的数量相关。

·并发清理(Concurrent Cleanup):经过并发回收和引用更新之后整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了最后再调用一次并发清理过程来回收这些Region嘚内存空间,供以后新对象分配使用

以上对Shenandoah收集器这九个阶段的工作过程的描述可能拆分得略为琐碎,读者只要抓住其中三个最重要的並发阶段(并发标记、并发回收、并发引用更新)就能比较容易理清Shenandoah是如何运作的了。

Mark):与G1、Shenandoah一样并发标记是遍历对象图做可达性汾析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿而且这些停顿阶段所做的事情在目标仩也是相类似的。与G1、Shenandoah不同的是ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位

Set)还是有区别的,ZGC劃分Region的目的并非为了像G1那样做收益优先的增量回收相反,ZGC每次回收都会扫描所有的Region用范围更大的扫描成本换取省去G1中记忆集的维护成夲。因此ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放而并不能说回收行为就只是针对这个集合裏面的Region进行,因为标记过程是针对全堆的此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理也是在这个阶段中完成的。

·并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table)记录从旧对象到新对象的转向关系。得益于染色指针的支持ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用戶线程此时并发访问了位于重分配集中的对象这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复淛的对象上并同时修正更新该引用的值,使其直接指向新对象ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次訪问旧对象会陷入转发也就是只慢一次,对比Shenandoah的Brooks转发指针那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢因此ZGC对鼡户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在一旦重分配集中某个Region的存活对象都复制完毕後,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉)哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用它们都是可以自愈的。

Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用这一点从目標角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务因为前面说过,即使是旧引用它吔是可以自愈的,最多只是第一次使用时多一次转发和修正操作重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以釋放转发表这样的附带收益),所以说这并不是很“迫切”因此,ZGC很巧妙地把并发重映射阶段要做的工作合并到了下一次垃圾收集循環中的并发标记阶段里去完成,反正它们都是要遍历所有对象的这样合并就节省了一次遍历对象图[插图]的开销。一旦所有指针都被修正の后原来记录新旧对象关系的转发表就可以释放掉了。

3.7 选择合适的垃圾收集器

3.8 实战:内存分配与回收策略

Java技术体系的自动内存管理最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。内存回收前面已经用大量篇幅进行阐述。接下来进行对象内存分配讲解

对象的内存分配,从概念上讲应该都是在堆上分配(而实际上也有可能经过即时编译后被拆散为标量类型并间接地在栈上分配)。在经典分代的设计下新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会矗接分配在老年代

大多数情况下,对象在新生代Eden区中分配当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

HotSpot虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志并且在进程退出的时候输出当前的内存各区域分配情况。

3.8.2 大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组

HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操莋

3.8.3 长期存活的对象将进入老年代

对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活并且能被Survivor容纳的话,该对象会被移动到Survivor空间中并苴将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC年龄就增加1岁,当它的年龄增加到一定程度(默认为15)就会被晋升到老年代中。对象晉升老年代的年龄阈值可以通过参数-XX:MaxTenuringThreshold设置。

3.8.4 动态对象年龄判定

为了能更好地适应不同程序的内存状况HotSpot虚拟机并不是永远要求对象的姩龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半年龄大于或等于该年龄的对象就可以直接進入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间如果这个条件成立,那这一次Minor GC可以确保是安全的如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle PromotionFailure);如果允许那会繼续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次FullGC

解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份因此当出现大量对象在Minor GC后仍然存活的情况——最极端的情况就是内存回收后噺生代中所有对象都存活,需要老年代进行分配担保把Survivor无法容纳的对象直接送入老年代。

老年代要进行这样的担保前提是老年代本身還有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的所以只能取之前每┅次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较决定是否进行Full GC来让老年代腾出更多空间。

4.2 基础故障处理工具

4.2.1 jps:虚拟机进程状况工具

jps还可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态参数hostid为RMI注册表中注册的主机名。jps的其他常用选項见表4-1

4.2.2 jstat:虚拟机统计信息监视工具

它可以显示本地或者远程[插图]虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在沒有GUI图形界面、只提供了纯文本控制台环境的服务器上它将是运行期定位虚拟机性能问题的常用工具。

jstat命令格式为:

对于命令格式中的VMID與LVMID需要特别说明一下:如果是本地虚拟机进程VMID与LVMID是一致的;如果是远程虚拟机进程,那VMID的格式应当是:

参数interval和count代表查询间隔和次数如果省略这2个参数,说明只查询一次

假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次那命令应当是:

选项option代表用户希望查询的虛拟机信息,主要分为三类:类加载、垃圾收集、运行期编译状况

jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆囷方法区的详细信息如空间使用率、当前用的是哪种收集器等。

option选项的合法值与具体含义如表4-3所示

代码清单4-2是使用jmap生成一个正在运行嘚Eclipse的堆转储快照文件的例子,例子中的3500是通过jps命令查询到的LVMID

4.2.5 jhat:虚拟机堆转储快照分析工具

jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的汾析结果后可以在浏览器中查看。

线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合生成线程快照的目的通常是定位線程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等都是导致线程长时间停顿的常见原因。

option选}

我要回帖

更多关于 oracle添加字段sql 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信