From c98186e0d4b99f8d80ebbecd06f05e30af95c8f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 15 Dec 2021 00:51:39 +0800 Subject: [PATCH 01/78] Update Java Notes --- DB.md | 147 +++++++++++++++++++++++++++++++++++++++++++++++++------- Java.md | 15 ++++-- 2 files changed, 141 insertions(+), 21 deletions(-) diff --git a/DB.md b/DB.md index e3ef7c8..48cb370 100644 --- a/DB.md +++ b/DB.md @@ -650,8 +650,6 @@ EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的 ### 数据空间 -==TODO:本节知识是抄录自《MySQL 实战 45 讲》不作为重点学习目标,暂时记录方便后续有了新的理解后更新知识== - #### 数据存储 系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd @@ -685,7 +683,7 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记 -#### 空间收缩 +#### 重建数据 重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,重建命令: @@ -709,7 +707,7 @@ MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此 Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构 -问题:想要收缩表空间,执行指令后整体占用空间增大 +问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 @@ -732,6 +730,8 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 +==本节知识是抄录自《MySQL 实战 45 讲》,作者目前没有更深的理解,暂时记录,后续有了新的认知后会更新知识== + 参考文章:https://time.geekbang.org/column/article/72388 @@ -3791,7 +3791,7 @@ InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 * 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 -* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段 row_id** 作为主键,这个字段长度为 6 个字节,类型为长整形 辅助索引: @@ -3868,6 +3868,10 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 * Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组 * File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性 +数据页中包含数据行,数据的存储是基于数据行的,数据行有 next_record 属性指向下一个行数据,所以是可以遍历的,但是一组数据至多 8 个行,通过 Page Directory 先定位到组,然后遍历获取所需的数据行即可 + +数据行中有三个隐藏字段:trx_id、roll_pointer、row_id(在事务章节会详细介绍它们的作用) + *** @@ -5529,7 +5533,7 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 BEGIN [WORK]; ``` - 说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改 + 说明:不填状态默认是读写事务 * 回滚事务,用来手动中止事务 @@ -5614,6 +5618,26 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +**** + + + +#### 事务 ID + +只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作 + +事务在执行过程中对某个表执行了**增删改操作或者创建表**,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0 + +事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量: + +* 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1 +* 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节 +* 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个递增的数字 + +**聚簇索引**的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引 + + + *** @@ -5669,6 +5693,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ### 原子特性 +#### 实现方式 + 原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) @@ -5686,16 +5712,100 @@ undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执 * 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment -rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment -* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html -参考文章:https://www.cnblogs.com/kismetv/p/10331633.html +*** + + + +#### DML 解析 + +##### INSERT + +乐观插入:当前数据页的剩余空间充足,直接将数据进行插入 + +悲观插入:当前数据页的剩余空间不足,需要进行页分裂,申请一个新的页面来插入数据,会造成更多的 redo log,undo log 影响不大 + +当向某个表插入一条记录,实际上需要向聚簇索引和所有二级索引都插入一条记录,但是 undo log 只需要针对聚簇索引记录,在回滚时会根据聚簇索引去所有的二级索引进行回滚操作 + +roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一条记录就是一个数据行,行格式中的 roll_pointer 就指向 undo log + + + +*** + + + +##### DELETE + +插入到页面中的记录会根据 next_record 属性组成一个单向链表,这个链表称为正常链表,被删除的记录也会通过 next_record 组成一个垃圾链表,该链表中所占用的存储空间可以被重新利用,并不会直接清除数据 + +在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: + +* 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** +* 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 +* 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** + +在对一条记录 delete mark 前会将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中,这样就会产生记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + +当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: + +* 如果可以容纳新纪录,就会直接重用已删除的记录的存储空间,然后让 PAGE_FREE 指向垃圾链表的下一个节点 +* 如果不能容纳新纪录,就直接向页面申请新的空间存储,并不会遍历垃圾链表 + +重用已删除的记录空间,可能会造成空间碎片,当数据页容纳不了一条记录时,会判断将碎片空间加起来是否可以容纳,判断为真就会重新组织页内的记录: + +* 开辟一个临时页面,将页内记录一次插入到临时页面,此时临时页面时没有碎片的 +* 把临时页面的内容复制到本页,这样就解放出了内存碎片,但是会耗费很大的性能资源 + + + +**** + + + +##### UPDATE + +执行 UPDATE 语句,对于更新主键和不更新主键有两种不同的处理方式 + +不更新主键的情况: + +* 就地更新(in-place update),如果更新后的列和更新前的列占用的存储空间一样大,就可以直接在原记录上修改 + +* 先删除旧纪录,再插入新纪录,这里的删除不是 delete mark,而是直接将记录加入垃圾链表,并且修改页面的相应的控制信息,执行删除的线程不是 purge,是执行更新的用户线程 + + 插入新记录时可能造成页空间不足,从而导致页分裂 + +更新主键的情况: + +* 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 +* 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 + + + +*** + + + +#### 回滚日志 + +undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + +Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot + +* 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 + +工作流程: + +* 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 +* 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot +* 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 开始记录 @@ -5817,11 +5927,6 @@ undo log 主要分为两种: * 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 -补充知识:purge 线程 - -* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 -* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 - *** @@ -5943,7 +6048,7 @@ RC、RR 级别下的 InnoDB 快照读区别 ### 持久特性 -#### 持久方式 +#### 实现方式 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 @@ -6021,7 +6126,9 @@ MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 * oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 * newest_modification:每次修改页面,都将 MTR **结束时**对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值 -全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint并不是同一个线程 +全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程 + +在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint 使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: @@ -6046,6 +6153,10 @@ SHOW ENGINE INNODB STATUS\G * 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** * 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn +问题:系统崩溃前没有提交的事务的 redo log 可能已经刷盘,这些数据可能在重启后也会恢复 + +解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 + 参考书籍:https://book.douban.com/subject/35231266/ diff --git a/Java.md b/Java.md index dc01f5b..e3ddbb2 100644 --- a/Java.md +++ b/Java.md @@ -19,6 +19,15 @@ +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,后随着学习的深入逐渐增加了很多知识 + +给初学者的一些个人建议: + +* 初学者对编程的认知比较浅显,一些专有词汇和概念难以理解,所以建议观看视频进行入门,大部分公开课视频讲的比较基础 +* 在有了一定的编程基础后,需要看一些经典书籍和技术博客,来扩容自己的知识广度和深度,可以长期保持记录笔记的好习惯 + + + *** @@ -140,8 +149,8 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, ``` - - + + *** @@ -4464,7 +4473,7 @@ public class Student implements Comparable{ } ``` -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(树) +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(红黑树) From 8ad135fd9c29437e0aac3aa128b0b0e293220ec1 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 15 Dec 2021 23:24:20 +0800 Subject: [PATCH 02/78] Update Java Notes --- DB.md | 147 +++++++++++++++++++++++++++++++++----------------------- Java.md | 8 +-- 2 files changed, 92 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index 48cb370..45d46c7 100644 --- a/DB.md +++ b/DB.md @@ -379,7 +379,32 @@ mysqlshow -uroot -p1234 test book --count #### 连接器 -池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 +##### 连接原理 + +池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,因为每个连接对应一个用来交互的线程,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 + +连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 + +客户端如果长时间没有操作,连接器就会自动断开,时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` + +数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: + +* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 +* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 + +MySQL 服务器可以同时和多个客户端进行交互,所以要保证每个连接会话的隔离性(事务机制部分详解) + +整体的执行流程: + + + + + +**** + + + +##### 连接状态 首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态 @@ -398,19 +423,6 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 - -客户端如果太长时间没动静,连接器就会自动断开,这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` - -数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: - -* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 -* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 - - - - - *** @@ -5281,10 +5293,10 @@ Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空 工作原理: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool - * 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 - **唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 + +**唯一索引的更新不能使用 Buffer,只有普通索引可以使用,直接写入 Buffer 就结束,不用校验唯一性** Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 @@ -5649,26 +5661,29 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 隔离级别分类: -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | ---------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 可串行化 | 无 | | + +* 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 +* 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) +* 脏写 (Dirty Write):当两个或多个事务选择同一行,最初的事务修改的值被后面事务修改的值覆盖,所有的隔离级别都可以避免脏写(又叫丢失更新),因为有行锁 -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 +* 脏读 (Dirty Reads):在一个事务处理过程中读取了另一个**未提交**的事务中修改过的数据 -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 +* 不可重复读 (Non-Repeatable Reads):在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 * 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -**隔离级别操作语法:** +隔离级别操作语法: * 查询数据库隔离级别 @@ -5750,7 +5765,7 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 * 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** -在对一条记录 delete mark 前会将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中,这样就会产生记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) 当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: @@ -5785,6 +5800,10 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 * 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 +在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中**,这样就记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + + + *** @@ -5853,7 +5872,7 @@ MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁 * 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -* 写-写:有线程安全问题,可能会存在丢失更新问题 +* 写-写:有线程安全问题,可能会存在脏写(丢失更新)问题 MVCC 的优点: @@ -5881,15 +5900,11 @@ MVCC 的优点: 实现原理主要是隐藏字段,undo日志,Read View 来实现的 -数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: - -* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 - -* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) - -* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 +数据库中的**聚簇索引**每行数据,除了自定义的字段,还有数据库隐式定义的字段: -* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 +* DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID +* DB_ROLL_PTR:回滚指针,**指向记录对应的 undo log 日志**,undo log 中又指向上一个旧版本的 undo log +* DB_ROW_ID:隐含的自增 ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) @@ -5897,15 +5912,13 @@ MVCC 的优点: - - *** -##### undo +##### 版本链 -undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,要**根据 undo log 逆推出以往事务的数据** undo log 的作用: @@ -5918,12 +5931,15 @@ undo log 主要分为两种: * update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 -每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 +每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log -* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 +补充:undo 是逻辑日志,这里只是直观的展示出来 +工作流程: + +* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 * 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 @@ -5935,26 +5951,26 @@ undo log 主要分为两种: ##### 读视图 -Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 +Read View 是事务进行读数据操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 -工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 Read View 几个属性: - m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) +- min_trx_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) +- max_trx_id:生成 Read View 时当前系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) - creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) -* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 +* db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) @@ -5993,8 +6009,8 @@ START TRANSACTION; -- 开启事务 ID 为 0 的事务创建 Read View: * m_ids:20、60 -* up_limit_id:20 -* low_limit_id:61 +* min_trx_id:20 +* max_trx_id:61 * creator_trx_id:0 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) @@ -6011,28 +6027,41 @@ ID 为 0 的事务创建 Read View: +##### 二级索引 + +只有在聚簇索引中才有 trx_id 和 roll_pointer 的隐藏列,对于二级索引判断可见性的方式: + +* 二级索引页面的 Page Header 中有一个 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,SELECT 语句访问某个二级索引时会判断 ReadView 的 min_trx_id 是否大于该属性,大于说明该页面的所有属性对 ReadView 可见 +* 如果属性判断不可见,就需要利用二级索引获取主键值,进行回表操作,得到聚簇索引后再按照聚簇索引的可见性判断的方法操作 + + + +*** + + + #### RC RR Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 RR、RC 生成时机: -- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View -- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View +- RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读) +- RR 隔离级别下,在第一次数据读取时才会创建 Read View(快照读) RC、RR 级别下的 InnoDB 快照读区别 -- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 +- RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 -- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 +- RR 级别下,某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的 - 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 + RR 级别下,通过 `START TRANSACTION WITH CONSISTENT SNAPSHOT` 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成 解决幻读问题: - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 给变为当前的事务 id,所以对当前事务就是可见的 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6040,8 +6069,6 @@ RC、RR 级别下的 InnoDB 快照读区别 - - *** diff --git a/Java.md b/Java.md index e3ddbb2..e0f0de8 100644 --- a/Java.md +++ b/Java.md @@ -531,11 +531,11 @@ public class Test1 { * || 和 |,&& 和& 的区别,逻辑运算符 - **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 + **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 - 两种运算符得到的结果完全相同,但得到结果的方式又一个重要区别:条件布尔运算符性能比较好。他检查第一个操作数的值,再根据该操作数的值进行操作,可能根本就不处理第二个操作数。 + 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** - 结论:如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值。但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** +* ^ 异或:两位相异为 1,相同为 0,又叫不进位加法。同或:两位相同为 1,相异为 0 * switch @@ -591,11 +591,13 @@ public class Test1 { 运算规则: * 正数的左移与右移,空位补 0 + * 负数原码的左移与右移,空位补 0 负数反码的左移与右移,空位补 1 负数补码,左移低位补 0(会导致负数变为正数的问题,因为移动了符号位),右移高位补 1 + * 无符号移位,空位补 0 From 744e3006467240d278b6cce5a3cffd337ddffbad Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 18 Dec 2021 00:05:29 +0800 Subject: [PATCH 03/78] Update Java Notes --- DB.md | 14 ++++++++------ Java.md | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/DB.md b/DB.md index 45d46c7..558ea03 100644 --- a/DB.md +++ b/DB.md @@ -3626,7 +3626,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) - 联合索引:顾名思义,就是将单列索引进行组合 - 唯一索引:索引列的值必须唯一,**允许有空值**,如果是联合索引,则列值组合必须唯一 - * NULL 值必须只出现一次 + * NULL 值可以出现多次,因为两个 NULL 比较的结果既不相等,也不不等,结果仍然是未知 * 可以声明不允许存储 NULL 值的非空唯一索引 - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 @@ -4108,7 +4108,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 #### 索引下推 -索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 +索引条件下推优化(Index Condition Pushdown,ICP)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 @@ -5762,7 +5762,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: * 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** + * 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 + * 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) @@ -5859,9 +5861,7 @@ Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot #### 并发控制 -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** - -MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制**,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读: * 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 * 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 @@ -5933,6 +5933,8 @@ undo log 主要分为两种: 每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log +说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据 + 补充:undo 是逻辑日志,这里只是直观的展示出来 @@ -6042,7 +6044,7 @@ ID 为 0 的事务创建 Read View: #### RC RR -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录 RR、RC 生成时机: diff --git a/Java.md b/Java.md index e0f0de8..35e4ddd 100644 --- a/Java.md +++ b/Java.md @@ -19,7 +19,7 @@ -初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,后随着学习的深入逐渐增加了很多知识 +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加了很多知识 给初学者的一些个人建议: From 84186472aa520ef9699b16a28becfb8c7e20d1f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 18 Dec 2021 22:32:45 +0800 Subject: [PATCH 04/78] Update Java Notes --- DB.md | 197 +++++++++++++++++++++++++++++++++++++++++--------------- Java.md | 8 +-- 2 files changed, 150 insertions(+), 55 deletions(-) diff --git a/DB.md b/DB.md index 558ea03..894b004 100644 --- a/DB.md +++ b/DB.md @@ -1946,6 +1946,10 @@ INSERT INTO card VALUES (NULL,'12345',1),(NULL,'56789',2); +*** + + + #### 一对多 举例:用户和订单、商品分类和商品 @@ -1976,6 +1980,10 @@ INSERT INTO orderlist VALUES (NULL,'hm001',1),(NULL,'hm002',1),(NULL,'hm003',2), +*** + + + #### 多对多 举例:学生和课程。一个学生可以选择多个课程,一个课程也可以被多个学生选择 @@ -6044,7 +6052,7 @@ ID 为 0 的事务创建 Read View: #### RC RR -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录 +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 **SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录** RR、RC 生成时机: @@ -6300,7 +6308,7 @@ InnoDB 刷脏页的控制策略: 锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 -作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 +利用 MVCC 性质进行读取的操作叫**一致性读**,读取数据前加锁的操作叫**锁定读** 锁的分类: @@ -6332,27 +6340,56 @@ InnoDB 刷脏页的控制策略: -### Server +### 内存结构 -FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +对一条记录加锁的本质就是在内存中创建一个锁结构与之关联,结构包括 -1. 上全局读锁(lock_global_read_lock) -2. 清理表缓存(close_cached_tables) -3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) +* 事务信息:锁对应的事务信息,一个锁属于一个事务 +* 索引信息:对于行级锁,需要记录加锁的记录属于哪个索引 +* 表锁和行锁信息:表锁记录着锁定的表,行锁记录了 Space ID 所在表空间、Page Number 所在的页号、n_bits 使用了多少比特 +* type_mode:一个 32 比特的数,被分成 lock_mode、lock_type、rec_lock_type 三个部分 + * lock_mode:锁模式,记录是共享锁、排他锁、意向锁之类 + * lock_type:代表表级锁还是行级锁 + * rec_lock_type:代表行锁的具体类型和 is_waiting 属性,is_waiting = true 时表示当前事务尚未获取到锁,处于等待状态。事务获取锁后的锁结构是 is_waiting 为 false,释放锁时会检查是否与当前记录关联的锁结构,如果有就唤醒对应事务的线程 -该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +一个事务可能操作多条记录,为了节省内存,满足下面条件的锁使用同一个锁结构: + +* 在同一个事务中的加锁操作 +* 被加锁的记录在同一个页面中 +* 加锁的类型时一样的 +* 加锁的状态时一样的 + + + + + +**** + + + +### Server MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) -MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** +MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,**当对一个表做增删改查的时候,加 MDL 读锁;当要对表做结构变更操作 DDL 的时候,加 MDL 写锁**,两种锁不相互兼容,所以可以保证 DDL、DML、DQL 操作的安全 -* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 +说明:DDL 操作执行前会隐式提交当前会话的事务,因为 DDL 一般会在若干个特殊事务中完成,开启特殊事务前需要提交到其他事务 -* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 +MDL 锁的特性: + +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始执行时申请,在整个事务提交后释放 + +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层能直接实现的锁 * MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) + +该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 @@ -6368,7 +6405,7 @@ MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一 MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 -* 加锁命令: +* 加锁命令:(对 InnoDB 存储引擎也适用) 读锁:所有连接只能读取数据,不能修改 @@ -6396,7 +6433,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 +锁调度:**MyISAM 的读写锁调度是写优先**,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 @@ -6532,14 +6569,16 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 #### 行级锁 -InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +##### 记录锁 + +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,**InnoDB 同时支持表锁和行锁** -InnoDB 实现了以下两种类型的行锁: +行级锁,也成为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: -- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁 锁的兼容性: @@ -6557,13 +6596,11 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +**** -*** - - -#### 锁操作 +##### 锁操作 两个客户端操作 Client 1和 Client 2,简化为 C1、C2 @@ -6648,13 +6685,13 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) - 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 + 由于 C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 -​ -*** + +**** @@ -6662,17 +6699,17 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ##### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 +当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 * 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 +加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以保护当前记录和前面的间隙 -* 加锁遵循前开后闭原则 -* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 +* 加锁遵循左开右闭原则 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会对 (10,11] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 @@ -6703,25 +6740,31 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -*** +**** ##### 意向锁 -InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) +InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock) 意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: * 意向共享锁(IS):事务有意向对表中的某些行加共享锁 - * 意向排他锁(IX):事务有意向对表中的某些行加排他锁 -InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +**IX,IS 是表级锁**,不会和行级的 X,S 锁发生冲突,意向锁是在加表级锁之前添加,为了在加表级锁时可以快速判断表中是否有记录被上锁,比如向一个表添加表级 X 锁的时: + +- 没有意向锁,则需要遍历整个表判断是否有锁定的记录 +- 有了意向锁,首先判断是否存在意向锁,然后判断该意向锁与即将添加的表级锁是否兼容即可,因为意向锁的存在代表有表级锁的存在或者即将有表级锁的存在 + +兼容性如下所示: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) -插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 +**插入意向锁** Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁,是行级锁 + +插入意向锁释放了一种插入信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值 2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 @@ -6729,16 +6772,18 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 死锁 +##### 自增锁 -当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 +系统会自动给 AUTO_INCREMENT 修饰的列进行递增赋值,实现方式: -死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 +* AUTO_INC 锁:表级锁,执行插入语句时会自动添加,在该语句执行完成后释放,并不是事务结束 +* 轻量级锁:为插入语句生成 AUTO_INCREMENT 修饰的列时获取该锁,生成以后释放掉,不需要等到插入语句执行完后释放 -解决策略: +系统变量 `innodb_autoinc_lock_mode` 控制采取哪种方式: -* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 -* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 +* 0:全部采用 AUTO_INC 锁 +* 1:全部采用轻量级锁 +* 2:混合使用,在插入记录的数量确定是采用轻量级锁,不确定时采用 AUTO_INC 锁 @@ -6746,8 +6791,57 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 +##### 隐式锁 + +一般情况下 INSERT 语句是不需要在内存中生成锁结构的,会进行隐式的加锁,保护的是插入后的安全 + +注意:如果插入的间隙被其他事务加了间隙锁,此次插入会被阻塞,并在该间隙插入一个插入意向锁 + +* 聚簇索引:索引记录有 trx_id 隐藏列,表示最后改动该记录的事务 id,插入数据后事务 id 就是当前事务。其他事务想获取该记录的锁时会判断当前记录的事务 id 是否是活跃的,如果不是就可以正常加锁;如果是就创建一个 X 的锁结构,该锁的 is_waiting 是 false,为自己的事务创建一个锁结构,is_waiting 是 true(类似 Java 中的锁升级) +* 二级索引:获取数据页 Page Header 中的 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,如果小于当前活跃的最小事务 id,就证明插入该数据的事务已经提交,否则就需要获取到主键值进行回表操作 + +隐式锁起到了延迟生成锁的效果,如果其他事务与隐式锁没有冲突,就可以避免锁结构的生成,节省了内存资源 + +INSERT 在两种情况下会生成锁结构: + +* 重复键:在插入主键或唯一二级索引时遇到重复的键值会报错,在报错前需要对对应的聚簇索引进行加锁 + * 隔离级别 <= Read Uncommitted,加 S 型 Record Lock + * 隔离级别 >= Repeatable Read,加 S 型 next_key 锁 + +* 外键检查:如果待插入的记录在父表中可以找到,会对父表的记录加 S 型 Record Lock。如果待插入的记录在父表中找不到 + * 隔离级别 <= Read Committed,不加锁 + * 隔离级别 >= Repeatable Read,加间隙锁 + + + + + +*** + + + #### 锁优化 +##### 优化锁 + +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM + +但是使用不当可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) + + + +**** + + + ##### 锁升级 索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 @@ -6781,21 +6875,20 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 优化锁 +##### 死锁 -InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM +不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁 -但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 -优化建议: +解决策略: -- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 -- 合理设计索引,尽量缩小锁的范围 -- 尽可能减少索引条件及索引范围,避免间隙锁 -- 尽量控制事务大小,减少锁定资源量和时间长度 -- 尽可使用低级别事务隔离(需要业务层面满足需求) +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 +* 主动死锁检测,发现死锁后**主动回滚死锁链条中较小的一个事务**,让其他事务得以继续执行,将参数 `innodb_deadlock_detect` 设置为 on,表示开启该功能。较小的意思就是事务执行过程中插入、删除、更新的记录条数 +通过执行 `SHOW ENGINE INNODB STATUS` 可以查看最近发生的一次死循环,全局系统变量 `innodb_print_all_deadlocks` 设置为 on,就可以将每个死锁信息都记录在 MySQL 错误日志中 +死锁一般是行级锁,当表锁发生死锁时,会在事务中访问其他表时直接报错,破坏了持有并等待的死锁条件 @@ -6805,6 +6898,8 @@ InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面 #### 锁状态 +查看锁信息 + ```mysql SHOW STATUS LIKE 'innodb_row_lock%'; ``` @@ -6829,7 +6924,7 @@ SHOW STATUS LIKE 'innodb_row_lock%'; ```mysql SELECT * FROM information_schema.innodb_locks; #锁的概况 -SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 +SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态,其中包括锁的情况 ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) diff --git a/Java.md b/Java.md index 35e4ddd..0d8a455 100644 --- a/Java.md +++ b/Java.md @@ -2324,7 +2324,7 @@ hashCode 的作用: * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 - Java 中的复制方法基本都是浅克隆:Object.clone()、System.arraycopy()、Arrays.copyOf() + **Java 中的复制方法基本都是浅克隆**:Object.clone()、System.arraycopy()、Arrays.copyOf() * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 @@ -2334,9 +2334,9 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( * Clone & Copy:`Student s = new Student` - `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 object,对对象的修改会影响对方 + `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 Object,对对象的修改会影响对方 - `Student s2 = s.clone()`:会生成一个新的Student对象,并且和s具有相同的属性值和方法 + `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和s具有相同的属性值和方法 * Shallow Clone & Deep Clone: @@ -18365,7 +18365,7 @@ Java中 的 Object 类中提供了 `clone()` 方法来实现浅克隆,实现 C } ``` - stu1 对象和 stu2 对象是同一个对象,将 stu2 对象中 name 属性改为李四,两个Citation对象中都是李四,这就是浅克隆的效果 + stu1 对象和 stu2 对象是同一个对象,将 stu2 对象中 name 属性改为李四,两个 Citation 对象中都是李四,这就是浅克隆的效果 * 序列化实现深克隆,或者重写克隆方法: From e60e7ffdef0f89a3dc85dde902fbefca7bb1ad6f Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 21 Dec 2021 00:26:39 +0800 Subject: [PATCH 05/78] Update Java Notes --- DB.md | 85 +++++++++++++++++++++++++++++++++++--------------------- Frame.md | 14 +++++----- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/DB.md b/DB.md index 894b004..c835ac6 100644 --- a/DB.md +++ b/DB.md @@ -5822,9 +5822,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 #### 回滚日志 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment +undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面 -Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot +每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段 * 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment * MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 @@ -5832,9 +5832,11 @@ Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot 工作流程: * 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 -* 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot -* 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 -* 开始记录 +* 回滚段页面有 1024 个 undo slot,首先去回滚段的两个 cached 链表获取缓存的 slot,缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务 +* 如果是缓存中获取的 slot,则该 slot 对应的 undo log segment 已经分配了,需要重新分配,然后从 undo log segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 每个事务 undo 日志在记录的时候占用两个 undo 页面的组成链表,分别为 insert undo 链表和 update undo 链表,链表的头节点页面为 first undo page 会包含一些管理信息,其他页面为 normal undo page + + 说明:事务执行过程的临时表也需要两个 undo 链表,不和普通表共用,这些链表并不是事务开始就分配,而是按需分配 @@ -6114,17 +6116,28 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 ##### 日志缓冲 -服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB - -补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR +服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 redo log buffer 的大小,默认是 16MB log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 * 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个) -* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针) -* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时也把这一组 redo log 当作一个不可分割的整体处理 +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(**碰撞指针**) + +MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR + +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理 + +* 所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer** + + + +*** -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: + + +##### 日志刷盘 + +redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO * 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO @@ -6139,31 +6152,39 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 * 服务器关闭时 * checkpoint 时(下小节详解) +redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样, +* `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 +* `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` -*** +redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像 +注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小 +服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 -##### 磁盘文件 -redo 存储在磁盘中的日志文件是被**循环使用**的,redo 日志文件组中每个文件大小一样格式一样,组成结构:前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的存储 log buffer 中的 block 镜像 -注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖 MTR 的大小 +*** -磁盘存储 redo log 的文件目录通过 `innodb_log_group_home_dir` 指定,默认是当前数据目录,文件大小: -* 通过两个参数调节:`innodb_log_file_size` 文件大小默认 48M,`innodb_log_files_in_group` 文件个数默认 2 最大 100 -* 服务器启动后磁盘空间不变,所以采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 + +##### 日志序号 lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘 +工作过程:写入 log buffer 数据时,buf_free 会进行偏移,偏移量就会加到 lsn 上 + MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: * oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 * newest_modification:每次修改页面,都将 MTR **结束时**对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值 -全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程 +全局变量 checkpoint_lsn 表示当前系统可以被覆盖的 redo 日志总量,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程 + +**checkpoint**:从 flush 链表尾部中找出还未刷脏的页面,该页面是当前系统中最早被修改的脏页,该页面之前产生的脏页都已经刷脏,然后将该页 oldest_modification 值赋值给 checkpoint_lsn,因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了 + +checkpoint_lsn 是一个总量,随着 lsn 写入的增加,刷脏的继续进行,所以 checkpoint_lsn 值就会一直变大,该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量 在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint @@ -6192,7 +6213,7 @@ SHOW ENGINE INNODB STATUS\G 问题:系统崩溃前没有提交的事务的 redo log 可能已经刷盘,这些数据可能在重启后也会恢复 -解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 +解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 @@ -10454,7 +10475,7 @@ RDB三种启动方式对比: #### 总结 -* RDB特殊启动形式的指令(客户端输入) +* RDB 特殊启动形式的指令(客户端输入) * 服务器运行过程中重启 @@ -10472,17 +10493,17 @@ RDB三种启动方式对比: * 全量复制:主从复制部分详解 -* RDB优点: +* RDB 优点: - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 - RDB 内部存储的是 redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制**等场景 - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 - - 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 -* RDB缺点: +* RDB 缺点: - bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 +* 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 @@ -10539,7 +10560,7 @@ AOF 持久化数据的三种策略(appendfsync): * 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 * fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 -异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 redis,然后重新加载 +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 @@ -10678,7 +10699,7 @@ AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢 - 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF - 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB - 灾难恢复选用 RDB -- 双保险策略,同时开启 RDB和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 - 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 @@ -10731,8 +10752,8 @@ fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid int main () { pid_t fpid; // fpid表示fork函数返回的值 - int count=0; - fpid=fork(); + int count = 0; + fpid = fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { @@ -10761,16 +10782,16 @@ int main () #include int main(void) { - int i=0; + int i = 0; // ppid 指当前进程的父进程pid // pid 指当前进程的pid, // fpid 指fork返回给当前进程的值,在这可以表示子进程 - for(i=0; i<2; i++){ + for(i = 0; i < 2; i++){ pid_t fpid = fork(); if(fpid == 0) - printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid); + printf("%d child %4d %4d %4d/n",i, getppid(), getpid(), fpid); else - printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid); + printf("%d parent %4d %4d %4d/n",i, getppid(), getpid(),fpid); } return 0; } diff --git a/Frame.md b/Frame.md index c8e1908..8947b3a 100644 --- a/Frame.md +++ b/Frame.md @@ -2505,7 +2505,7 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 +--------+-------------------------------------------------+----------------+ ``` -解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口大小,仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍 +解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口的大小仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍 @@ -2540,13 +2540,13 @@ public class HelloWorldClient { #### 固定长度 -服务器端加入定长解码器,每一条消息采用固定长度,缺点浪费空间 +服务器端加入定长解码器,每一条消息采用固定长度。如果是半包消息,会缓存半包消息并等待下个包到达之后进行拼包合并,直到读取一个完整的消息包;如果是粘包消息,空余的位置会进行补 0,会浪费空间 ```java serverBootstrap.childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { - ch.pipeline().addLast(new FixedLengthFrameDecoder(8)); + ch.pipeline().addLast(new FixedLengthFrameDecoder(10)); // LoggingHandler 用来打印消息 ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); } @@ -2692,7 +2692,7 @@ public class LengthFieldDecoderDemo { #### HTTP协议 -访问URL:http://localhost:8080/ +访问 URL:http://localhost:8080/ ```java public class HttpDemo { @@ -4537,9 +4537,9 @@ RocketMQ 的工作流程: - 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 - Broker 启动,跟所有的 NameServer 保持长连接,每隔 30s 时间向 NameServer 上报 Topic 路由信息(心跳包)。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 - 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic -- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer 拉取一次路由信息 +- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer **定时拉取**一次路由信息 - Producer 发送消息时,根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息,如果没有则会从 NameServer 上重新拉取并更新,轮询队列列表并选择一个队列 MessageQueue,然后与队列所在的 Broker 建立长连接,向 Broker 发消息 -- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,定时获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 @@ -4581,7 +4581,7 @@ RocketMQ 的工作流程: #### 通信原理 -==todo:后期学习了源码会进行扩充,现在暂时 copy 官方文档== +==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== 在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response From 2bfeaa8c14fe64a2c4ca17d4f6cf3bfcd7c982b5 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 25 Dec 2021 01:06:00 +0800 Subject: [PATCH 06/78] Update Java Notes --- DB.md | 5 +- Frame.md | 639 ++++++++++++++++++++++++++++++++++++++++++++++++------- Java.md | 8 +- 3 files changed, 568 insertions(+), 84 deletions(-) diff --git a/DB.md b/DB.md index c835ac6..ebf0128 100644 --- a/DB.md +++ b/DB.md @@ -4,7 +4,7 @@ ### 数据库 -数据库:DataBase,简称 DB,用于存储和管理数据的仓库,它的存储空间很大,可以存放百万上亿条数据。 +数据库:DataBase,简称 DB,存储和管理数据的仓库 数据库的优势: @@ -22,13 +22,12 @@ - 数据表 - 数据库最重要的组成部分之一 - - 它由纵向的列和横向的行组成(类似 excel 表格) + - 由纵向的列和横向的行组成(类似 excel 表格) - 可以指定列名、数据类型、约束等 - 一个表中可以存储多条数据 - 数据:想要永久化存储的数据 - diff --git a/Frame.md b/Frame.md index 8947b3a..4dc7ebc 100644 --- a/Frame.md +++ b/Frame.md @@ -42,6 +42,10 @@ pom.xml:Maven 需要一个 pom.xml 文件,Maven 通过加载这个配置文 +参考视频:https://www.bilibili.com/video/BV1Ah411S7ZE + + + *** @@ -1757,7 +1761,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty #### 基本介绍 -事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),里面有 run 方法处理 Channel 上源源不断的 IO 事件 +事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),有 run 方法处理 Channel 上源源不断的 IO 事件 事件循环组 EventLoopGroup 是一组 EventLoop,Channel 会调用 Boss EventLoopGroup 的 register 方法来绑定其中一个 Worker 的 EventLoop,后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理,保证了事件处理时的线程安全 @@ -1847,7 +1851,7 @@ static void invokeChannelRead(final AbstractChannelHandlerContext next, Object m ### Channel -#### 基本介绍 +#### 连接操作 Channel 类 API: @@ -2112,7 +2116,6 @@ public static void main(String[] args) { }) .bind(8080); } - ``` 服务器端依次打印:1 2 4 3 ,所以**入站是按照 addLast 的顺序执行的,出站是按照 addLast 的逆序执行** @@ -2690,7 +2693,7 @@ public class LengthFieldDecoderDemo { ### 协议设计 -#### HTTP协议 +#### HTTP 访问 URL:http://localhost:8080/ @@ -2750,7 +2753,7 @@ public class HttpDemo { -#### 自定义协议 +#### 自定义 处理器代码: @@ -4499,7 +4502,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性(每 10 秒) * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -4543,77 +4546,6 @@ RocketMQ 的工作流程: -**** - - - -#### 协议设计 - -在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在 RocketMQ 中,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 - -| Header字段 | 类型 | Request 说明 | Response 说明 | -| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | -| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | -| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | -| version | int | 请求方程序的版本 | 应答方程序的版本 | -| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | -| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | -| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | -| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) - -传输内容主要可以分为以下四部分: - -* 消息长度:总长度,四个字节存储,占用一个 int 类型 - -* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 - -* 消息头数据:经过序列化后的消息头数据 - -* 消息主体数据:消息主体的二进制字节数据内容 - - - -***** - - - -#### 通信原理 - -==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== - -在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response - -RocketMQ 的异步通信流程: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) - -RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,同时又在这之上做了一些扩展和优化 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) - -RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: - -* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接,创建 SocketChannel,并注册到 selector 上。RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),然后监听真正的网络数据 - -* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 -* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) -* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 - -| 线程数 | 线程名 | 线程具体说明 | -| ------ | ------------------------------ | ------------------------- | -| 1 | NettyBoss_%d | Reactor 主线程 | -| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | -| M1 | NettyServerCodecThread_%d | Worker 线程池 | -| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 - - - *** @@ -5215,3 +5147,556 @@ public class MessageListenerImpl implements MessageListener { ## 源码分析 + +### 服务启动 + +#### 启动方法 + +NamesrvStartup 类中有 Namesrv 服务的启动方法: + +```java +public static void main(String[] args) { + // 如果启动时 使用 -c -p 设置参数了,这些参数存储在 args 中 + main0(args); +} + +public static NamesrvController main0(String[] args) { + try { + // 创建 namesrv 控制器,用来初始化 namesrv 启动 namesrv 关闭 namesrv + NamesrvController controller = createNamesrvController(args); + // 启动 controller + start(controller); + return controller; + } catch (Throwable e) { + // 出现异常,停止系统 + System.exit(-1); + } + return null; +} +``` + +NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv 控制器 + +* `ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options),..)`:解析启动时的参数信息 + +* `namesrvConfig = new NamesrvConfig()`:创建 Namesrv 配置对象 + + * `private String rocketmqHome`:获取 ROCKETMQ_HOME 值 + * `private boolean orderMessageEnable = false`:顺序消息功能是否开启 + +* `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 + + ```java + public class NettyServerConfig implements Cloneable { + // 服务端启动时监听的端口号 + private int listenPort = 8888; + // 【业务线程池】 线程数量 + private int serverWorkerThreads = 8; + // 根据该值创建 remotingServer 内部的一个 publicExecutor + private int serverCallbackExecutorThreads = 0; + // netty 【worker】线程数 + private int serverSelectorThreads = 3; + // 【单向访问】时的并发限制 + private int serverOnewaySemaphoreValue = 256; + // 【异步访问】时的并发限制 + private int serverAsyncSemaphoreValue = 64; + // channel 最大的空闲存活时间 默认是 2min + private int serverChannelMaxIdleTimeSeconds = 120; + // 发送缓冲区大小 65535 + private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; + // 接收缓冲区大小 65535 + private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 是否启用 netty 内存池 默认开启 + private boolean serverPooledByteBufAllocatorEnable = true; + + // 默认 linux 会启用 【epoll】 + private boolean useEpollNativeSelector = false; + } + ``` + +* `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的监听端口设置为 9876 + +* `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 + + `in = new BufferedInputStream(new FileInputStream(file))`:读取指定目录的配置文件 + + `properties.load(in)`:将配置文件信息加载到 properties 对象,相关属性会复写到 Namesrv 配置和 Netty 配置对象 + + `namesrvConfig.setConfigStorePath(file)`:将配置文件的路径保存到配置保存字段 + +* `if (null == namesrvConfig.getRocketmqHome())`:检查 ROCKETMQ_HOME 配置是否是空,是空就报错 + +* `lc = (LoggerContext) LoggerFactory.getILoggerFactory()`:创建日志对象 + +* `controller = new NamesrvController(namesrvConfig, nettyServerConfig)`:**创建 Namesrv 控制器** + +NamesrvStartup#start:启动 Namesrv 控制器 + +* `boolean initResult = controller.initialize()`:初始化方法 + +* ` Runtime.getRuntime().addShutdownHook(new ShutdownHookThread())`:JVM HOOK 平滑关闭的逻辑, 当 JVM 被关闭时,主动调用 controller.shutdown() 方法,让服务器平滑关机 +* `controller.start()`:启动服务器 + + + +**** + + + + + +#### 控制器类 + +NamesrvController 用来初始化和启动 Namesrv 服务器 + +* 成员变量: + + ```java + private final ScheduledExecutorService scheduledExecutorService; // 调度线程池,用来执行定时任务 + private final RouteInfoManager routeInfoManager; // 管理【路由信息】的对象 + private RemotingServer remotingServer; // 【网络层】封装对象 + private ExecutorService remotingExecutor; // 业务线程池,用来 work + private BrokerHousekeepingService brokerHousekeepingService; // 用于监听 channel 状态 + private ExecutorService remotingExecutor; // 业务线程池 + ``` + +* 初始化: + + ```java + public boolean initialize() { + // 加载本地kv配置(我还不明白 kv 配置是啥) + this.kvConfigManager.load(); + // 创建网络服务器对象,【将 netty 的配置和监听器传入】 + // 监听器监听 channel 状态的改变,会向事件队列发起事件,最后交由 service 处理 + this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); + // 【创建业务线程池,默认线程数 8】 + // netty 线程解析报文成 RemotingCommand 对象,然后将该对象交给业务线程池再继续处理。 + this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads().); + + // 注册协议处理器(缺省协议处理器),处理器是 DefaultRequestProcessor,线程使用的是刚创建的业务的线程池 + this.registerProcessor(); + + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除。【心跳机制】 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + // 将两小时没有活动的 broker 关闭,通过 next.getKey() 获取 broker 的地址 + // 然后【关闭服务器与broker物理节点的 channel】 + NamesrvController.this.routeInfoManager.scanNotActiveBroker(); + } + }, 5, 10, TimeUnit.SECONDS); + + // 定时任务2:每 10 分钟打印一遍 kv 配置。 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + NamesrvController.this.kvConfigManager.printAllPeriodically(); + } + }, 1, 10, TimeUnit.MINUTES); + + return true; + } + ``` + +* 启动方法: + + ```java + public void start() throws Exception { + // 服务器网络层启动。 + this.remotingServer.start(); + + if (this.fileWatchService != null) { + this.fileWatchService.start(); + } + } + ``` + + + + + +*** + + + +#### 网络服务 + +##### 通信原理 + +RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,NettyRemotingServer 类负责框架的通信服务,同时又在这之上做了一些扩展和优化 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) + +RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: + +* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接创建 SocketChannel(RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),并注册到 Selector 上,然后监听真正的网络数据 + +* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 +* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) +* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 + +| 线程数 | 线程名 | 线程具体说明 | +| ------ | ------------------------------ | ------------------------- | +| 1 | NettyBoss_%d | Reactor 主线程 | +| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | +| M1 | NettyServerCodecThread_%d | Worker 线程池 | +| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | + +RocketMQ 的异步通信流程: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) + + + +==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 + + + +*** + + + +##### 成员属性 + +成员变量: + +* 服务器相关属性: + + ```java + private final ServerBootstrap serverBootstrap; // netty 服务端启动对象 + private final EventLoopGroup eventLoopGroupSelector; // netty worker 组线程池,【默认 3 个线程】 + private final EventLoopGroup eventLoopGroupBoss; // netty boss 组线程池,【一般是 1 个线程】 + private final NettyServerConfig nettyServerConfig; // netty 服务端网络配置 + private int port = 0; // 服务器绑定的端口 + ``` + +* 公共线程池:注册处理器时如果未指定线程池,则业务处理使用公共线程池,线程数量默认是 4 + + ```java + private final ExecutorService publicExecutor; + ``` + +* 事件监听器:Nameserver 使用 BrokerHouseKeepingService,Broker 使用 ClientHouseKeepingService + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* 事件处理线程池:默认是 8 + + ```java + private DefaultEventExecutorGroup defaultEventExecutorGroup; + ``` + +* 定时器:执行循环任务,并且将定时器线程设置为守护线程 + + ```java + private final Timer timer = new Timer("ServerHouseKeepingService", true); + ``` + +* 处理器:多个 Channel 共享的处理器 Handler,多个通道使用同一个对象 + + +构造方法: + +* 无监听器构造: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig) { + this(nettyServerConfig, null); + } + ``` + +* 有参构造方法: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig, + final ChannelEventListener channelEventListener) { + // 服务器对客户端主动发起请求时并发限制。【单向请求和异步请求】的并发限制 + super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue()); + // Netty 的启动器,负责组装 netty 组件 + this.serverBootstrap = new ServerBootstrap(); + // 成员变量的赋值 + this.nettyServerConfig = nettyServerConfig; + this.channelEventListener = channelEventListener; + + // 公共线程池的线程数量,默认给的0,这里最终修改为4. + int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + // 创建公共线程池,指定线程工厂,设置线程名称前缀:NettyServerPublicExecutor_[数字] + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory(){.}); + + // 创建两个 netty 的线程组,一个是boss组,一个是worker组,【linux 系统默认启用 epoll】 + if (useEpoll()) {...} else {...} + // SSL 相关 + loadSslContext(); + } + ``` + + + + + +*** + + + +##### 启动方法 + +核心方法的解析: + +* start():启动方法 + + ```java + public void start() { + // 向 channel pipeline 添加 handler,网络事件传播到当前 handler 时,【线程分配给 handler 处理事件】 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...); + + // 创建通用共享的处理器 handler,【非常重要的 NettyServerHandler】 + prepareSharableHandlers(); + + ServerBootstrap childHandler = + // 配置工作组 boss(数量1) 和 worker(数量3) 组 + this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector) + // 设置服务端 ServerSocketChannel 类型, Linux 用 epoll + .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) + // 设置服务端 channel 选项 + .option(ChannelOption.SO_BACKLOG, 1024) + // 客户端 channel 选项 + .childOption(ChannelOption.TCP_NODELAY, true) + // 设置服务器端口 + .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) + // 向 channel pipeline 添加了很多 handler,包括 NettyServerHandler + .childHandler(new ChannelInitializer() {}); + + // 客户端开启 内存池,使用的内存池是 PooledByteBufAllocator.DEFAULT + if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) { + childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } + + try { + // 同步等待建立连接,并绑定端口。 + ChannelFuture sync = this.serverBootstrap.bind().sync(); + InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress(); + // 将服务器成功绑定的端口号赋值给字段 port。 + this.port = addr.getPort(); + } catch (InterruptedException e1) {} + + // housekeepingService 不为空,则创建【网络异常事件处理器】 + if (this.channelEventListener != null) { + // 线程一直轮询 nettyEventExecutor 状态,根据 CONNECT,CLOSE,IDLE,EXCEPTION 四种事件类型 + // CONNECT 不做操作,其余都是回调 onChannelDestroy 关闭服务器与 Broker 物理节点的 Channel + this.nettyEventExecutor.start(); + } + + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的 请求 移除。 + this.timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + NettyRemotingServer.this.scanResponseTable(); + } + }, 1000 * 3, 1000); + } + ``` + +* registerProcessor():注册业务处理器 + + ```java + public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { + ExecutorService executorThis = executor; + if (null == executor) { + // 未指定线程池资源,将公共线程池赋值 + executorThis = this.publicExecutor; + } + // pair 对象,第一个参数代表的是处理器, 第二个参数是线程池,默认是公共的线程池 + Pair pair = new Pair(processor, executorThis); + + // key 是请求码,value 是 Pair 对象 + this.processorTable.put(requestCode, pair); + } + ``` + +* getProcessorPair():**根据请求码获取对应的处理器和线程池资源** + + ```java + public Pair getProcessorPair(int requestCode) { + return processorTable.get(requestCode); + } + ``` + + + +*** + + + +##### 请求方法 + +在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response + +服务器主动向客户端发起请求时,使用三种方法 + +* invokeSync(): 同步调用,服务器需要阻塞等待调用的返回结果 + * `int opaque = request.getOpaque()`:获取请求 ID(与请求码不同) + * `responseFuture = new ResponseFuture(...)`:创建响应对象,将请求 ID、通道、超时时间传入,没有回调函数和 Once + * `this.responseTable.put(opaque, responseFuture)`:**加入到响应映射表中**,key 为请求 ID + * `SocketAddress addr = channel.remoteAddress()`:获取客户端的地址信息 + * `channel.writeAndFlush(request).addListener(...)`:将**业务 Command 信息**写入通道,业务线程将数据交给 Netty ,Netty 的 IO 线程接管写刷数据的操作,**监听器由 IO 线程在写刷后回调** + * `if (f.isSuccess())`:写入成功会将响应对象设置为成功状态直接 return,写入失败设置为失败状态 + * `responseTable.remove(opaque)`:将当前请求的 responseFuture **从映射表移除** + * `responseFuture.setCause(f.cause())`:设置错误的信息 + * `responseFuture.putResponse(null)`:请求的业务码设置为 null + * `responseCommand = responseFuture.waitResponse(timeoutMillis)`:当前线程设置超时时间挂起,**同步等待响应** + * `if (null == responseCommand)`:超时或者出现异常,直接报错 + * `return responseCommand`:返回响应 Command 信息 +* invokeAsync():异步调用,有回调对象,无返回值 + * `boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制异步请求**的数量 + * `if (acquired)`:许可证获取失败说明并发较高,会抛出异常 + * `once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync)`:Once 对象封装了释放信号量的操作 + * `costTime = System.currentTimeMillis() - beginStartTime`:计算一下耗费的时间,超时不再发起请求 + * `responseFuture = new ResponseFuture()`:创建响应对象,包装了回调函数和 Once 对象 + * `this.responseTable.put(opaque, responseFuture)`:加入到响应映射表中,key 为请求 ID + * `channel.writeAndFlush(request).addListener(...)`:写刷数据 + * `if (f.isSuccess())`:写刷成功,设置 responseFuture 发生状态为 true + * `requestFail(opaque)`:写入失败,使用 publicExecutor **公共线程池异步执行回调对象的函数** + * `responseFuture.release()`:出现异常会释放信号量 + +* invokeOneway():单向调用,不关注响应结果 + * `request.markOnewayRPC()`:设置单向标记,对端检查标记可知该请是单向请求 + * `boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制单向请求**的数量 + + + + + +*** + + + +#### 处理器类 + +##### 协议设计 + +在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。在 RocketMQ 中,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 + +| Header字段 | 类型 | Request 说明 | Response 说明 | +| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | +| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | +| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | +| version | int | 请求方程序的版本 | 应答方程序的版本 | +| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | +| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | +| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | +| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) + +传输内容主要可以分为以下四部分: + +* 消息长度:总长度,四个字节存储,占用一个 int 类型 + +* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 + +* 消息头数据:经过序列化后的消息头数据 + +* 消息主体数据:消息主体的二进制字节数据内容 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#22-%E5%8D%8F%E8%AE%AE%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%BC%96%E8%A7%A3%E7%A0%81 + + + +**** + + + +##### 处理方法 + +NettyServerHandler 类用来处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** + +```java +class NettyServerHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + // 服务器处理接受到的请求信息 + processMessageReceived(ctx, msg); + } +} +public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + final RemotingCommand cmd = msg; + if (cmd != null) { + // 根据请求的类型进行处理 + switch (cmd.getType()) { + case REQUEST_COMMAND:// 客户端发起的请求,走这里 + processRequestCommand(ctx, cmd); + break; + case RESPONSE_COMMAND:// 客户端响应的数据,走这里【当前类本身是服务器类也是客户端类】 + processResponseCommand(ctx, cmd); + break; + default: + break; + } + } +} +``` + +NettyRemotingAbstract#processRequestCommand:处理请求的数据 + +* `matched = this.processorTable.get(cmd.getCode())`:根据业务请求码获取 Pair 对象,包含**处理器和线程池资源** + +* `pair = null == matched ? this.defaultRequestProcessor : matched`:未找到处理器则使用缺省处理器 + +* `int opaque = cmd.getOpaque()`:获取请求 ID + +* `Runnable run = new Runnable()`:创建任务对象 + + * `doBeforeRpcHooks()`:RPC HOOK 前置处理 + + * `callback = new RemotingResponseCallback()`:封装响应客户端逻辑 + + * `doAfterRpcHooks()`:RPC HOOK 后置处理 + * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 + * `response.setOpaque(opaque)`:将请求 ID 设置到 response + * `response.markResponseType()`:设置当前的处理是响应处理 + * `ctx.writeAndFlush(response)`: 将数据交给 Netty IO 线程,完成数据写和刷 + + * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 + + * `processor = (AsyncNettyRequestProcessor)pair.getObject1()`:获取处理器 + + * `processor.asyncProcessRequest(ctx, cmd, callback)`:异步调用,首先 processRequest,然后 callback 响应客户端 + + DefaultRequestProcessor.processRequest **根据业务码处理请求,执行对应的操作** + +* `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 + +* `pair.getObject2().submit(requestTask)`:获取处理器对应的线程池,将 task 提交,**从 IO 线程切换到业务线程** + +NettyRemotingAbstract#processResponseCommand:处理响应的数据 + +* `int opaque = cmd.getOpaque()`:获取请求 ID +* `responseFuture = responseTable.get(opaque)`:**从响应映射表中获取对应的对象** +* `responseFuture.setResponseCommand(cmd)`:设置响应的 Command 对象 +* `responseTable.remove(opaque)`:从映射表中移除对象,代表处理完成 +* `if (responseFuture.getInvokeCallback() != null)`:包含回调对象,异步执行回调对象 +* `responseFuture.putResponse(cmd)`:不好含回调对象,**同步调用时,需要唤醒等待的业务线程** + + + +流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 + + + + + + + + + + + diff --git a/Java.md b/Java.md index 0d8a455..d1f0b60 100644 --- a/Java.md +++ b/Java.md @@ -2320,6 +2320,8 @@ hashCode 的作用: #### 深浅克隆 +Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 + 深浅拷贝(克隆)的概念: * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 @@ -2328,15 +2330,13 @@ hashCode 的作用: * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 - Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 Object,对对象的修改会影响对方 - `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和s具有相同的属性值和方法 + `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和 s 具有相同的属性值和方法 * Shallow Clone & Deep Clone: @@ -16085,7 +16085,7 @@ public class QuickSort { 实现思路: -- 获得最大数的位数,可以通过将最大数变为String类型,再求长度 +- 获得最大数的位数,可以通过将最大数变为 String 类型,再求长度 - 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零** - 从最低位开始,依次进行一次排序 - 从最低位排序一直到最高位(个位 → 十位 → 百位 → … →最高位)排序完成以后,数列就变成一个有序序列 From 74f98a667c84532feb5a859d7c9f5d3be5e2451f Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 29 Dec 2021 23:29:46 +0800 Subject: [PATCH 07/78] Update README --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6433260..6f633dd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局**美观整洁**,如果对各位朋友有所帮助,希望可以给个 star。 +**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对读者朋友有所帮助。 个人邮箱:imseazean@gmail.com @@ -15,8 +15,7 @@ 其他说明: * 推荐使用 Typora 阅读笔记,打开目录栏效果更佳。 -* 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 -* Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 +* 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 +* 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 -* 笔记的编写是基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 From 21717ee194664be70adc062ccaa7d5198db3012e Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 29 Dec 2021 23:31:29 +0800 Subject: [PATCH 08/78] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f633dd..5b83132 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对读者朋友有所帮助。 +**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对各位读者朋友有所帮助。 个人邮箱:imseazean@gmail.com From a555f81dd9942c557efeac208928b6af0df5f7e5 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 1 Jan 2022 22:54:15 +0800 Subject: [PATCH 09/78] Update Java Notes --- DB.md | 18 +- Frame.md | 1002 +++++++++++++++++++++++++++++++++++++++++++++++++++--- Java.md | 2 +- Web.md | 24 +- 4 files changed, 983 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index ebf0128..2564c46 100644 --- a/DB.md +++ b/DB.md @@ -1226,7 +1226,7 @@ LIMIT SELECT 列名1,列名2,... FROM 表名; ``` -* 去除重复查询:只有值全部重复的才可以去除,需要创建临时表辅助查询 +* **去除重复查询**:只有值全部重复的才可以去除,需要创建临时表辅助查询 ```mysql SELECT DISTINCT 列名1,列名2,... FROM 表名; @@ -1556,6 +1556,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 分组查询 +分组查询会进行去重 + * 分组查询语法 ````mysql @@ -2052,6 +2054,8 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 + + *** @@ -2074,6 +2078,8 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 SELECT 列名 FROM 表名1,表名2 WHERE 条件; ``` +内连接中 WHERE 子句和 ON 子句是等价的 + @@ -2085,7 +2091,9 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 -应用实例:差学生成绩,也想查出缺考的人的成绩 +应用实例:查学生成绩,也想查出缺考的人的成绩 + +(内连接快还是外连接快?) * 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 @@ -2232,7 +2240,9 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 #### 查询优化 -不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 +不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。 + +系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 * 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 * 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树 @@ -2246,7 +2256,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 -详细内容可以参考:《MySQL 是怎样运行的》 +参考书籍:https://book.douban.com/subject/35231266/ diff --git a/Frame.md b/Frame.md index 4dc7ebc..b5c4d88 100644 --- a/Frame.md +++ b/Frame.md @@ -4502,7 +4502,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性(每 10 秒) +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性,每 10 秒清除一次两小时没有活跃的 Broker * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -4581,7 +4581,7 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 #### 存储结构 -Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的 +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** @@ -5030,15 +5030,19 @@ public class MessageListenerImpl implements MessageListener { #### 重投机制 -生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 -消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重试策略: +消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重投策略: - retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 - retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 - retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 +注意点: +* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** + +* 发送消息超时时间默认3000毫秒,就不会再尝试重试 @@ -5148,7 +5152,7 @@ public class MessageListenerImpl implements MessageListener { ## 源码分析 -### 服务启动 +### 服务端 #### 启动方法 @@ -5186,34 +5190,6 @@ NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv * `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 - ```java - public class NettyServerConfig implements Cloneable { - // 服务端启动时监听的端口号 - private int listenPort = 8888; - // 【业务线程池】 线程数量 - private int serverWorkerThreads = 8; - // 根据该值创建 remotingServer 内部的一个 publicExecutor - private int serverCallbackExecutorThreads = 0; - // netty 【worker】线程数 - private int serverSelectorThreads = 3; - // 【单向访问】时的并发限制 - private int serverOnewaySemaphoreValue = 256; - // 【异步访问】时的并发限制 - private int serverAsyncSemaphoreValue = 64; - // channel 最大的空闲存活时间 默认是 2min - private int serverChannelMaxIdleTimeSeconds = 120; - // 发送缓冲区大小 65535 - private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; - // 接收缓冲区大小 65535 - private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 是否启用 netty 内存池 默认开启 - private boolean serverPooledByteBufAllocatorEnable = true; - - // 默认 linux 会启用 【epoll】 - private boolean useEpollNativeSelector = false; - } - ``` - * `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的监听端口设置为 9876 * `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 @@ -5276,12 +5252,12 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 // 注册协议处理器(缺省协议处理器),处理器是 DefaultRequestProcessor,线程使用的是刚创建的业务的线程池 this.registerProcessor(); - // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除。【心跳机制】 + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除【扫描机制】 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { - // 将两小时没有活动的 broker 关闭,通过 next.getKey() 获取 broker 的地址 - // 然后【关闭服务器与broker物理节点的 channel】 + // 扫描 brokerLiveTable 表,将两小时没有活动的 broker 关闭, + //通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 NamesrvController.this.routeInfoManager.scanNotActiveBroker(); } }, 5, 10, TimeUnit.SECONDS); @@ -5319,7 +5295,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 -#### 网络服务 +#### 网络通信 ##### 通信原理 @@ -5360,7 +5336,7 @@ RocketMQ 的异步通信流程: ##### 成员属性 -成员变量: +NettyRemotingServer 类成员变量: * 服务器相关属性: @@ -5398,6 +5374,36 @@ RocketMQ 的异步通信流程: * 处理器:多个 Channel 共享的处理器 Handler,多个通道使用同一个对象 +* Netty 配置对象: + + ```java + public class NettyServerConfig implements Cloneable { + // 服务端启动时监听的端口号 + private int listenPort = 8888; + // 【业务线程池】 线程数量 + private int serverWorkerThreads = 8; + // 根据该值创建 remotingServer 内部的一个 publicExecutor + private int serverCallbackExecutorThreads = 0; + // netty 【worker】线程数 + private int serverSelectorThreads = 3; + // 【单向访问】时的并发限制 + private int serverOnewaySemaphoreValue = 256; + // 【异步访问】时的并发限制 + private int serverAsyncSemaphoreValue = 64; + // channel 最大的空闲存活时间 默认是 2min + private int serverChannelMaxIdleTimeSeconds = 120; + // 发送缓冲区大小 65535 + private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; + // 接收缓冲区大小 65535 + private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 是否启用 netty 内存池 默认开启 + private boolean serverPooledByteBufAllocatorEnable = true; + + // 默认 linux 会启用 【epoll】 + private boolean useEpollNativeSelector = false; + } + ``` + 构造方法: @@ -5453,7 +5459,7 @@ RocketMQ 的异步通信流程: ```java public void start() { - // 向 channel pipeline 添加 handler,网络事件传播到当前 handler 时,【线程分配给 handler 处理事件】 + // Channel Pipeline 内的 handler 使用的线程资源,【线程分配给 handler 处理事件】 this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...); // 创建通用共享的处理器 handler,【非常重要的 NettyServerHandler】 @@ -5493,7 +5499,7 @@ RocketMQ 的异步通信流程: this.nettyEventExecutor.start(); } - // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的 请求 移除。 + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的数据移除。 this.timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { @@ -5653,17 +5659,17 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `int opaque = cmd.getOpaque()`:获取请求 ID -* `Runnable run = new Runnable()`:创建任务对象 +* `Runnable run = new Runnable()`:创建任务对象,任务在提交到线程池后开始执行 * `doBeforeRpcHooks()`:RPC HOOK 前置处理 - * `callback = new RemotingResponseCallback()`:封装响应客户端逻辑 + * `callback = new RemotingResponseCallback()`:**封装响应客户端的逻辑** * `doAfterRpcHooks()`:RPC HOOK 后置处理 * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 * `response.setOpaque(opaque)`:将请求 ID 设置到 response * `response.markResponseType()`:设置当前的处理是响应处理 - * `ctx.writeAndFlush(response)`: 将数据交给 Netty IO 线程,完成数据写和刷 + * `ctx.writeAndFlush(response)`: **将响应数据交给 Netty IO 线程,完成数据写和刷** * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 @@ -5671,7 +5677,9 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `processor.asyncProcessRequest(ctx, cmd, callback)`:异步调用,首先 processRequest,然后 callback 响应客户端 - DefaultRequestProcessor.processRequest **根据业务码处理请求,执行对应的操作** + `DefaultRequestProcessor.processRequest`:**根据业务码处理请求,执行对应的操作** + + `ClientRemotingProcessor.processRequest`:处理回退消息,需要消费者回执一条消息给生产者 * `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 @@ -5684,15 +5692,917 @@ NettyRemotingAbstract#processResponseCommand:处理响应的数据 * `responseFuture.setResponseCommand(cmd)`:设置响应的 Command 对象 * `responseTable.remove(opaque)`:从映射表中移除对象,代表处理完成 * `if (responseFuture.getInvokeCallback() != null)`:包含回调对象,异步执行回调对象 -* `responseFuture.putResponse(cmd)`:不好含回调对象,**同步调用时,需要唤醒等待的业务线程** +* `responseFuture.putResponse(cmd)`:不包含回调对象,**同步调用时,唤醒等待的业务线程** +流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 -流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 +*** + + + +##### 路由注册 + +DefaultRequestProcessor REGISTER_BROKER 方法解析: + +```java +public RemotingCommand registerBroker(ChannelHandlerContext ctx, RemotingCommand request) { + // 创建响应请求的对象,设置为响应类型,【先设置响应的状态码时系统错误码】 + // 反射创建 RegisterBrokerResponseHeader 对象设置到 response.customHeader 属性中 + final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); + + // 获取出反射创建的 RegisterBrokerResponseHeader 用户自定义header对象。 + final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader(); + + // 反射创建 RegisterBrokerRequestHeader 对象,并且将 request.extFields 中的数据写入到该对象中 + final RegisterBrokerRequestHeader requestHeader = request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class); + + // CRC 校验,计算请求中的 CRC 值和请求头中包含的是否一致 + if (!checksum(ctx, request, requestHeader)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("crc32 not match"); + return response; + } + + TopicConfigSerializeWrapper topicConfigWrapper; + if (request.getBody() != null) { + // 【解析请求体 body】,解码出来的数据就是当前机器的主题信息 + topicConfigWrapper = TopicConfigSerializeWrapper.decode(request.getBody(), TopicConfigSerializeWrapper.class); + } else { + topicConfigWrapper = new TopicConfigSerializeWrapper(); + topicConfigWrapper.getDataVersion().setCounter(new AtomicLong(0)); + topicConfigWrapper.getDataVersion().setTimestamp(0); + } + + // 注册方法 + // 参数1 集群、参数2:节点ip地址、参数3:brokerName、参数4:brokerId 注意brokerId=0的节点为主节点 + // 参数5:ha节点ip地址、参数6当前节点主题信息、参数7:过滤服务器列表、参数8:当前服务器和客户端通信的channel + RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(..); + + // 将结果信息 写到 responseHeader 中 + responseHeader.setHaServerAddr(result.getHaServerAddr()); + responseHeader.setMasterAddr(result.getMasterAddr()); + // 获取 kv配置,写入 response body 中,【kv 配置是顺序消息相关的】 + byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC); + response.setBody(jsonValue); + + // code 设置为 SUCCESS + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + // 返回 response ,【返回的 response 由 callback 对象处理】 + return response; +} +``` + +RouteInfoManager#registerBroker:注册 Broker 的信息 + +* `RegisterBrokerResult result = new RegisterBrokerResult()`:返回结果的封装对象 + +* `this.lock.writeLock().lockInterruptibly()`:加写锁后**同步执行** + +* `brokerNames = this.clusterAddrTable.get(clusterName)`:获取当前集群上的 Broker 名称列表,是空就新建列表 + +* `brokerNames.add(brokerName)`:将当前 Broker 名字加入到集群列表 + +* `brokerData = this.brokerAddrTable.get(brokerName)`:获取当前 Broker 的 brokerData,是空就新建放入映射表 + +* `brokerAddrsMap = brokerData.getBrokerAddrs()`:获取当前 Broker 的物理节点 map 表,进行遍历,如果物理节点角色发生变化(slave → master),先将旧数据从物理节点 map 中移除,然后重写放入,**保证节点的唯一性** + +* `if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId)`:Broker 上的 Topic 不为 null,并且当前物理节点是 Broker 上的 master 节点 + + `tcTable = topicConfigWrapper.getTopicConfigTable()`:获取当前 Broker 信息中的主题映射表 + + `if (tcTable != null)`:映射表不空就加入或者更新到 Namesrv 内 + +* ` prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr)`:添加**当前节点的 BrokerLiveInfo** ,返回上一次心跳时当前 Broker 节点的存活对象数据。**NamesrvController 中的定时任务会扫描映射表 brokerLiveTable** + + ```java + BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr, new BrokerLiveInfo( + System.currentTimeMillis(),topicConfigWrapper.getDataVersion(), channel,haServerAddr)); + ``` + +* `if (MixAll.MASTER_ID != brokerId)`:当前 Broker 不是 master 节点,**获取主节点的信息**设置到结果对象 + +* `this.lock.writeLock().unlock()`:释放写锁 + + + +**** + + + +### 生产者 + +#### 生产者类 + +DefaultMQProducer 是生产者的默认实现类 + +成员变量: + +* 生产者实现类: + + ```java + protected final transient DefaultMQProducerImpl defaultMQProducerImpl + ``` + +* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 + + ```java + private String producerGroup; + ``` + +* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 + + ```java + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + // 值为【TBW102】,Just for testing or demo program + ``` + +* 消息重投:系统特性消息重试部分详解了三个参数的作用 + + ```java + private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 + private int retryTimesWhenSendAsyncFailed = 2; // 异步 + private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 + ``` + +* 消息队列: + + ```java + private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 + ``` + +* 消息属性: + + ```java + private int sendMsgTimeout = 3000; // 发送消息的超时限制 + private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 + private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M + private TraceDispatcher traceDispatcher = null; // 消息轨迹 + +构造方法: + +* 构造方法: + + ```java + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + // 创建生产者实现对象 + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } + ``` + +成员方法: + +* start():启动方法 + + ```java + public void start() throws MQClientException { + // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 + this.setProducerGroup(withNamespace(this.producerGroup)); + // 生产者实现对象启动 + this.defaultMQProducerImpl.start(); + if (null != traceDispatcher) { + // 消息轨迹的逻辑 + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } + } + ``` + +* send():**发送消息**: + + ```java + public SendResult send(Message msg){ + // 校验消息 + Validators.checkMessage(msg, this); + // 设置消息 Topic + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg); + } + ``` + +* request():请求方法,**需要消费者回执消息**,又叫回退消息 + + ```java + public Message request(final Message msg, final MessageQueue mq, final long timeout) { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } + ``` + + + + +*** + + + +#### 实现者类 + +##### 成员属性 + +DefaultMQProducerImpl 类是默认的生产者实现类 + +成员变量: + +* 实例对象: + + ```java + private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 + private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + ``` + +* 主题发布信息映射表:key 是 Topic,value 是发布信息 + + ```java + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + ``` + + ```java + public class TopicPublishInfo { + private boolean orderTopic = false; + private boolean haveTopicRouterInfo = false; + // 主题的全部队列 + private List messageQueueList = new ArrayList(); + // 使用 ThreaLocal 保存发送消息使用的队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); + // 主题的路由数据 + private TopicRouteData topicRouteData; + } + ``` + +* 异步发送消息:相关信息 + + ```java + private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 + private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 + private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + ``` + +* 定时器:执行定时任务 + + ```java + private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + ``` + +* 状态信息:服务的状态,默认创建状态 + + ```java + private ServiceState serviceState = ServiceState.CREATE_JUST; + ``` + +* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 + + ```java + private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + ``` + +* 容错策略:选择队列的容错策略 + + ```java + private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + ``` + +* 钩子:用来进行前置或者后置处理 + + ```java + ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 + ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 + private final RPCHook rpcHook; // 传递给 NettyRemotingClient + ``` + +构造方法: + +* 默认构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + // 默认 RPC HOOK 是空 + this(defaultMQProducer, null); + } + ``` + +* 有参构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { + // 属性赋值 + this.defaultMQProducer = defaultMQProducer; + this.rpcHook = rpcHook; + + // 创建【异步消息线程池任务队列】,长度是 5w + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); + // 创建默认的异步消息任务线程池 + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法,参数默认是 true,代表正常的启动路径 + + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + + * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER + + * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + + `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID + + * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + + * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 + + * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + + * `if (startFactory) `:正常启动路径 + + `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + + * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 + + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) + * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 + + * 生产者 msg 添加信息关联 ID 发送到 Broker + * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 + * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + +* sendDefaultImpl():发送消息 + + ```java + //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 + private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + ``` + + * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 + + * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 + + * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 + + * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + + * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 + + * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 + + `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 + + `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 + + `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 + + * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** + + * `return topicPublishInfo`:返回 TBW102 主题的发布信息 + + * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 + * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name + * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 + * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName + + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** + + * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 + * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 + * `int index = this.sendWhichQueue.getAndIncrement()`:选择队列的索引 + * `int pos = Math.abs(index) % this.messageQueueList.size()`:获取该索引对应的队列位置 + * `return this.messageQueueList.get(pos)`:返回消息队列 + + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + * `sendResult = this.sendKernelImpl`:核心发送方法 + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 + + * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker + + * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + +* sendKernelImpl():**核心发送方法** + + ```java + //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 + private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + ``` + + * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 + + * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel + + * `byte[] prevBody = msg.getBody()`:获取消息体 + + * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID + + `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + + * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 + + * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 + + * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 + + * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + + * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 + + `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + + * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 + * `request.setBody(msg.getBody())`:**将消息放入请求体** + * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 + +* request():请求方法,消费者回执消息,这种消息是异步消息 + + * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 + + * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + + * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + + ```java + public Message waitResponseMessage(final long timeout) throws InterruptedException { + // 请求挂起 + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + + ```java + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + ``` + + + +**** + + + +### 客户端 + +#### 实例对象 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: + + ```java + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 + ``` + +* 生产者消费者的映射表:key 是组名 + + ```java + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable + ``` + +* 网络层配置: + + ```java + private final NettyClientConfig nettyClientConfig; + ``` + +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + + ```java + private final MQClientAPIImpl mQClientAPIImpl; + ``` + +* 本地路由数据:key 是主题名称,value 路由信息 + + ```java + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + +* 锁信息:两把锁,锁不同的数据 + + ```java + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + ``` + +* 调度线程池:单线程,执行定时任务 + + ```java + private final ScheduledExecutorService scheduledExecutorService; + ``` + +* Broker 映射表:key 是 BrokerName + + ```java + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; + ``` + +* **客户端的协议处理器**:用于处理 IO 事件 + + ```java + private final ClientRemotingProcessor clientRemotingProcessor; + ``` + +* 消息服务: + + ```java + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + ``` + +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 + + ```java + private final DefaultMQProducer defaultMQProducer; + ``` + +* 心跳次数统计: + + ```java + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + ``` + +* 公共配置类: + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +构造方法: + +* MQClientInstance 有参构造: + + ```java + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } + ``` + +* MQClientAPIImpl 有参构造: + + ```java + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } + ``` + + + +*** + + + +##### 成员方法 + +* start():启动方法 + + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* Netty 配置对象: + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + diff --git a/Java.md b/Java.md index d1f0b60..edc5ccd 100644 --- a/Java.md +++ b/Java.md @@ -4436,7 +4436,7 @@ TreeSet 集合自排序的方式: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` - * 比较者大于被比较者,返回正数 + * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 diff --git a/Web.md b/Web.md index 7c70150..5262ce7 100644 --- a/Web.md +++ b/Web.md @@ -2164,7 +2164,7 @@ HTTP 和 HTTPS 的区别: * 优点:运算速度快 * 缺点:无法安全的将密钥传输给通信方 -* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,公钥公开给任何人(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有RSA、DSA等 +* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,**公钥公开给任何人**(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有 RSA、DSA 等 * 优点:可以更安全地将公开密钥传输给通信发送方 * 缺点:运算速度慢 @@ -2174,28 +2174,28 @@ HTTP 和 HTTPS 的区别: * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 思想:锁上加锁 + +* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改,一般是通过哈希算法 + +* 数字证书:由权威机构给某网站颁发的一种认可凭证 HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加密,客户端生成的随机密钥,用来进行对称加密 ![](https://gitee.com/seazean/images/raw/master/Web/HTTP-HTTPS加密过程.png) -1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口 - -2. 服务器端有一个密钥对,即公钥和私钥,用来进行非对称加密,服务器端保存着私钥不能泄露,公钥可以发给任何客户端 - -3. 服务器将公钥发送给客户端 - -4. 客户端收到服务器端的数字证书之后,会对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key,即客户端密钥。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,HTTPS 中的第一次 HTTP 请求结束 - +1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口,请求携带了浏览器支持的加密算法和哈希算法 +2. 服务器端会向数字证书认证机构提出公开密钥的申请,认证机构对公开密钥做数字签名后进行分配,会将公钥绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) +3. 服务器将数字证书发送给客户端,私钥由服务器持有 +4. 客户端收到服务器端的数字证书后对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,**这个随机值就是用于进行对称加密的密钥**,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 5. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端密钥发送给服务器 - 6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文 - 7. 服务器将加密后的密文发送给客户端 - 8. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据,这样 HTTPS 中的第二个 HTTP 请求结束,整个 HTTPS 传输完成 + +参考文章:https://www.cnblogs.com/linianhui/p/security-https-workflow.html + 参考文章:https://www.jianshu.com/p/14cd2c9d2cd2 From be312e31a807598dc13bd0f33e98ddd676e0e6e4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 3 Jan 2022 15:04:59 +0800 Subject: [PATCH 10/78] Update README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b83132..4ac1828 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 内容说明: * DB:MySQL、Redis -* Frame:Maven、Netty +* Frame:Maven、Netty、RocketMQ * Java:JavaSE、JVM、Algorithm、Design Pattern * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot @@ -18,4 +18,3 @@ * 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 * 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 - From 4debfde6642506a8e4b4f2e88189efa8038f7745 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 6 Jan 2022 00:48:25 +0800 Subject: [PATCH 11/78] Update Java Notes --- DB.md | 30 ++- Frame.md | 651 +++++++++++++++++++++++++++++++++++++++++++------------ Java.md | 2 +- Prog.md | 16 +- 4 files changed, 544 insertions(+), 155 deletions(-) diff --git a/DB.md b/DB.md index 2564c46..c815155 100644 --- a/DB.md +++ b/DB.md @@ -5320,7 +5320,7 @@ Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编 MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: * 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 -* 如果存在对应的缓存页,直接获取使用 +* 如果存在对应的缓存页,直接获取使用,提高查询数据的效率 @@ -5419,6 +5419,8 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod SHOW ENGINE INNODB STATUS\G ``` +`Buffer pool hit rate` 字段代表**内存命中率**,表示 Buffer Pool 对查询的加速效果 + 核心参数: * `innodb_buffer_pool_size`:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小,默认 128M @@ -5441,8 +5443,6 @@ SHOW ENGINE INNODB STATUS\G innodb_log_buffer_size=10M ``` -Buffer Pool 中有一块内存叫 Change Buffer 用来对增删改操作提供缓存,可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% - 在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,每个实例独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各实例互不影响,提高了并发能力 * 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 @@ -5456,6 +5456,30 @@ MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改 +*** + + + +#### 其他内存 + +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改**操作提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% + +Server 层针对优化**查询**的内存为 Net Buffer,内存的大小是由参数 `net_buffer_length`定义,默认 16k,实现流程: + +* 获取一行数据写入 Net Buffer,重复获取直到 Net Buffer 写满,调用网络接口发出去 +* 若发送成功就清空 Net Buffer,然后继续取下一行;若发送函数返回 `EAGAIN` 或 `WSAEWOULDBLOCK`,表示本地网络栈 `socket send buffer` 写满了,进入等待,直到网络栈重新可写再继续发送 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询内存优化.png) + +MySQL 采用的是边算边发的逻辑,因此对于数据量很大的查询来说,不会在 Server 端保存完整的结果集,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是不会把内存打爆导致 OOM + + + + +参考文章:https://blog.csdn.net/qq_33589510/article/details/117673449 + + + *** diff --git a/Frame.md b/Frame.md index b5c4d88..b809388 100644 --- a/Frame.md +++ b/Frame.md @@ -3816,7 +3816,7 @@ public class Consumer { - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 - 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 -在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个 queue,消息都是有序的 +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4552,8 +4552,6 @@ RocketMQ 的工作流程: -### 消息存储 - #### 生产消费 At least Once:至少一次,指每个消息必须投递一次,Consumer 先 Pull 消息到本地,消费完成后才向服务器返回 ACK,如果没有消费一定不会 ACK 消息 @@ -4575,101 +4573,6 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 -*** - - - -#### 存储结构 - -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 - -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - -* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M -* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 hash 索引 - -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 - - - -**** - - - -#### 存储优化 - -##### 存储媒介 - -两种持久化的方案: - -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 - -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 - - 注意:磁盘的顺序读写要比随机读写快很多,可以匹配上网络的速度,RocketMQ 的消息采用的顺序写 - -页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化 - -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理) - -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 - - - -*** - - - -##### 内存映射 - -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - -* read:读取本地文件内容 - -* write:将读取的内容通过网络发送出去 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 - -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用零拷贝技术,提高消息存盘和网络发送的速度。 - -RocketMQ 主要通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 - -MappedByteBuffer 内存映射的方式限制一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - - - -*** - - - -#### 刷盘机制 - -同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 - -异步刷盘:利用 OS 的 PageCache 的优势,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 - -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - - - **** @@ -4912,6 +4815,27 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** ### 消息重试 +#### 重投机制 + +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 + +如下方法可以设置消息重投策略: + +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 +- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 + +注意点: + +* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** +* 发送消息超时时间默认3000毫秒,就不会再尝试重试 + + + +*** + + + #### 重试机制 Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: @@ -4927,7 +4851,7 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG **无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 `%RETRY%+consumerGroup` 的重试队列中 -消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下: +消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下表示: | 第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 | | :--------: | :------------------: | :--------: | :------------------: | @@ -4942,7 +4866,9 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG 如果消息重试 16 次后仍然失败,消息将**不再投递**,如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递 -说明:一条消息无论重试多少次,消息的 Message ID 是不会改变的 +时间间隔不支持自定义配置,最大重试次数可通过自定义参数 `MaxReconsumeTimes` 取值进行配置,若配置超过 16 次,则超过的间隔时间均为 2 小时 + +说明:一条消息无论重试多少次,**消息的 Message ID 是不会改变的** @@ -5024,26 +4950,6 @@ public class MessageListenerImpl implements MessageListener { -*** - - - -#### 重投机制 - -生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 - -消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重投策略: - -- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 -- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 -- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 - -注意点: - -* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** - -* 发送消息超时时间默认3000毫秒,就不会再尝试重试 - *** @@ -5081,7 +4987,7 @@ public class MessageListenerImpl implements MessageListener { 消息队列 RocketMQ 消费者在接收到消息以后,需要根据业务上的唯一 Key 对消息做幂等处理 -在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,几种情况: +At least Once 机制保证消息不丢失,但是可能会造成消息重复,RocketMQ 中无法避免消息重复(Exactly-Once),在互联网应用中,尤其在网络不稳定的情况下,几种情况: - 发送时消息重复:当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或客户端宕机,导致服务端对客户端应答失败。此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 @@ -5150,11 +5056,13 @@ public class MessageListenerImpl implements MessageListener { -## 源码分析 +## 原理解析 ### 服务端 -#### 启动方法 +#### 服务启动 + +##### 启动方法 NamesrvStartup 类中有 Namesrv 服务的启动方法: @@ -5221,7 +5129,7 @@ NamesrvStartup#start:启动 Namesrv 控制器 -#### 控制器类 +##### 控制器类 NamesrvController 用来初始化和启动 Namesrv 服务器 @@ -5899,7 +5807,7 @@ DefaultMQProducer 是生产者的默认实现类 -#### 实现者类 +#### 默认实现 ##### 成员属性 @@ -5920,19 +5828,6 @@ DefaultMQProducerImpl 类是默认的生产者实现类 private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); ``` - ```java - public class TopicPublishInfo { - private boolean orderTopic = false; - private boolean haveTopicRouterInfo = false; - // 主题的全部队列 - private List messageQueueList = new ArrayList(); - // 使用 ThreaLocal 保存发送消息使用的队列 - private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); - // 主题的路由数据 - private TopicRouteData topicRouteData; - } - ``` - * 异步发送消息:相关信息 ```java @@ -6078,13 +5973,13 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 - * `int index = this.sendWhichQueue.getAndIncrement()`:选择队列的索引 - * `int pos = Math.abs(index) % this.messageQueueList.size()`:获取该索引对应的队列位置 - * `return this.messageQueueList.get(pos)`:返回消息队列 - + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + * `sendResult = this.sendKernelImpl`:核心发送方法 + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 @@ -6152,7 +6047,107 @@ DefaultMQProducerImpl 类是默认的生产者实现类 } ``` - + + + +*** + + + +#### 路由信息 + +TopicPublishInfo 类用来存储路由信息 + +成员变量: + +* 顺序消息: + + ```java + private boolean orderTopic = false; + ``` + +* 消息队列: + + ```java + private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + ``` + + ```java + // 【消息队列类】 + public class MessageQueue implements Comparable, Serializable { + private String topic; + private String brokerName; + private int queueId;// 队列 ID + } + ``` + +* 路由数据:主题对应的路由数据 + + ```java + private TopicRouteData topicRouteData; + ``` + + ```java + public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; // 队列数据 + private List brokerDatas; // Broker 数据 + private HashMap/* Filter Server */> filterServerTable; + } + ``` + + ```java + public class QueueData implements Comparable { + private String brokerName; // 节点名称 + private int readQueueNums; // 读队列数 + private int writeQueueNums; // 写队列数 + private int perm; // 权限 + private int topicSynFlag; + } + ``` + + ```java + public class BrokerData implements Comparable { + private String cluster; // 集群名 + private String brokerName; // Broker节点名称 + private HashMap brokerAddrs; + } + ``` + +核心方法: + +* selectOneMessageQueue():**选择消息队列**使用 + + ```java + // 参数是上次失败时的 brokerName,可以为 null + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { + if (lastBrokerName == null) { + return selectOneMessageQueue(); + } else { + // 遍历消息队列 + for (int i = 0; i < this.messageQueueList.size(); i++) { + // 获取队列的索引 + int index = this.sendWhichQueue.getAndIncrement(); + // 获取队列的下标位置 + int pos = Math.abs(index) % this.messageQueueList.size(); + if (pos < 0) + pos = 0; + // 获取消息队列 + MessageQueue mq = this.messageQueueList.get(pos); + // 与上次选择的不同就可以返回 + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + ``` + + + + **** @@ -6604,9 +6599,379 @@ NettyRemotingClient 类负责客户端的网络通信 +*** + + + +### 存储端 + +#### 存储机制 + +##### 存储结构 + +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 hash 索引 + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + +**** + + + +##### 存储优化 + +###### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 + +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +###### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +##### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +*** + + + +#### MappedFile + +##### 成员属性 + +MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** + +MappedFile 类成员变量: + +* 内存相关: + + ```java + public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k + private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 + private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 + ``` + +* 数据位点: + + ```java + protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 + protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 + private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 + // flushedPosition-wrotePosition 之间的数据属于脏页 + ``` + +* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue + + ```java + private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 + private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 + private File file; // 文件对象 + ``` + +* 内存映射: + + ```java + protected FileChannel fileChannel; // 文件通道 + private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 + ``` + +ReferenceResource 类成员变量: + +* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 + + ```java + protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 + ``` + +* 存活状态:表示资源的存活状态 + + ```java + protected volatile boolean available = true; + ``` + +* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 + + ```java + protected volatile boolean cleanupOver = false; + ``` + +* 第一次关闭资源的时间:用来记录超时时间 + + ```java + private volatile long firstShutdownTimestamp = 0; + ``` + + + +*** + + + +##### 成员方法 + +MappedFile 类核心方法: + +* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 + + ```java + // 参数一:消息 参数二:追加消息回调 + public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) + ``` + + ```java + // 将字节数组写入到文件通道 + public boolean appendMessage(final byte[] data) + ``` + +* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 + + ```java + public int flush(final int flushLeastPages) + ``` + +* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 + + ```java + public SelectMappedBufferResult selectMappedBuffer(int pos) + ``` + +* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 + + ```java + public boolean destroy(final long intervalForcibly) + ``` + +* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 + + ```java + public boolean cleanup(final long currentRef) + ``` + +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件都加载到内存** + + ```java + public void warmMappedFile(FlushDiskType type, int pages) + ``` + +ReferenceResource 类核心方法: + +* hold():增加引用记数 refCount,方法加锁 + + ```java + public synchronized boolean hold() + ``` + +* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 + + ```java + // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 + public void shutdown(final long intervalForcibly) + ``` + +* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 + + ```java + public void release() + ``` + + + + + + + +*** + + + +#### MapQueue + +MappedFileQueue 用来管理 MappedFile 文件 + +成员变量: + +* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` + + ```java + private final String storePath; + ``` + +* 文件属性: + + ```java + private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 + private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + ``` + +* 数据位点: + + ```java + private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition + private long committedWhere = 0; // 目录的提交位点 + ``` + +* 消息存储: + + ```java + private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + ``` + +* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 + + ```java + private final AllocateMappedFileService allocateMappedFileService; + ``` + +核心方法: + +* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 + + ```java + public boolean load() + ``` + +* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile + + ```java + // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + ``` + +* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere + + ```java + //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 + public boolean flush(final int flushLeastPages) + ``` + +* findMappedFileByOffset():根据偏移量查询对象 + + ```java + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + ``` + +* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 + + ```java + // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 + public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + ``` + +* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 + + ```java + // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; + // 参数二:ConsumerQueue 文件内每个数据单元固定大小 + public int deleteExpiredFileByOffset(long offset, int unitSize) + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +*** +## TEST diff --git a/Java.md b/Java.md index edc5ccd..c9daa70 100644 --- a/Java.md +++ b/Java.md @@ -10218,7 +10218,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 #### 两种方式 -为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 * 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 * 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 diff --git a/Prog.md b/Prog.md index 36c0ed0..dc6f5e3 100644 --- a/Prog.md +++ b/Prog.md @@ -2292,7 +2292,7 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 -存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的 +存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是**不可变**的,就算有缓存,也不会存在不可见的问题 main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止: @@ -3613,7 +3613,7 @@ final 变量的赋值通过 putfield 指令来完成,在这条指令之后也 不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象 -不可变对象线程安全的,因为不存在并发修改,是另一种避免竞争的方式 +不可变对象线程安全的,不存在并发修改和可见性问题,是另一种避免竞争的方式 String 类也是不可变的,该类和类中所有属性都是 final 的 @@ -13752,7 +13752,7 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C 把内存数据传输到网卡然后发送: * 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 -* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 +* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后**中断**(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: @@ -13797,7 +13797,7 @@ read 调用图示:read、write 都是系统调用指令 #### mmap -mmap(Memory Mapped Files)加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** +mmap(Memory Mapped Files)内存映射加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** 用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 Socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 @@ -14902,8 +14902,8 @@ FileChannel 提供 map 方法返回 MappedByteBuffer 对象,把文件映射到 FileChannel 中的成员属性: * MapMode.mode:内存映像文件访问的方式,共三种: - * `MapMode.READ_ONLY`:只读,试图修改得到的缓冲区将导致抛出异常。 - * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 + * `MapMode.READ_ONLY`:只读,修改得到的缓冲区将导致抛出异常 + * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见的 * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为写时复制 * `public final FileLock lock()`:获取此文件通道的排他锁 @@ -14915,7 +14915,7 @@ MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修 MappedByteBuffer 较之 ByteBuffer 新增的三个方法: -- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 +- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改**强制写入文件** - `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 - `final boolean isLoaded()`:如果缓冲区的内容在物理内存中,则返回真,否则返回假 @@ -14924,7 +14924,7 @@ public class MappedByteBufferTest { public static void main(String[] args) throws Exception { // 读写模式 RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); - //获取对应的通道 + // 获取对应的通道 FileChannel channel = ra.getChannel(); /** From 68e76ef7463339f7198bc668aa7b0d017c95e119 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 9 Jan 2022 22:14:49 +0800 Subject: [PATCH 12/78] Update Java Notes --- Frame.md | 2354 ++++++++++++++++++++++++++++++++++++------------------ Java.md | 2 +- 2 files changed, 1589 insertions(+), 767 deletions(-) diff --git a/Frame.md b/Frame.md index b809388..6f84a04 100644 --- a/Frame.md +++ b/Frame.md @@ -3485,6 +3485,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 +参考视频:https://www.bilibili.com/video/BV1L4411y7mn + **** @@ -4494,7 +4496,7 @@ public class Producer { ## 系统特性 -### 工作机制 +### 工作流程 #### 模块介绍 @@ -4533,7 +4535,7 @@ Broker 包含了以下几个重要子模块: -#### 工作流程 +#### 总体流程 RocketMQ 的工作流程: @@ -4573,47 +4575,6 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 -**** - - - -### 消息查询 - -#### Message ID - -RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 - -RocketMQ 中的 MessageId 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset - -实现方式:Client 端从 MessageId 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 - - - -*** - - - -#### Message Key - -按照 Message Key 查询消息,主要是基于 RocketMQ 的 IndexFile 索引文件来实现的,RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - -IndexFile 索引文件为提供了通过 Message Key 查询消息的服务,IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - -整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是真正的索引数据 - -索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - -* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 -* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - -实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 - - - - - *** @@ -4813,9 +4774,27 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** -### 消息重试 +### 消息机制 + +#### 消息查询 + +RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 + +* RocketMQ 中的 MessageID 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset + + 实现方式:Client 端从 MessageID 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 + +* 按照 Message Key 查询消息,IndexFile 索引文件为提供了通过 Message Key 查询消息的服务 -#### 重投机制 + 实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 + + + +*** + + + +#### 消息重投 生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 @@ -4836,7 +4815,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** -#### 重试机制 +#### 消息重试 Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: @@ -5165,7 +5144,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 @Override public void run() { // 扫描 brokerLiveTable 表,将两小时没有活动的 broker 关闭, - //通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 + // 通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 NamesrvController.this.routeInfoManager.scanNotActiveBroker(); } }, 5, 10, TimeUnit.SECONDS); @@ -5696,356 +5675,359 @@ RouteInfoManager#registerBroker:注册 Broker 的信息 + + **** -### 生产者 +### 存储端 -#### 生产者类 +#### 存储机制 -DefaultMQProducer 是生产者的默认实现类 +##### 存储结构 -成员变量: +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 -* 生产者实现类: +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - ```java - protected final transient DefaultMQProducerImpl defaultMQProducerImpl - ``` +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 -* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - ```java - private String producerGroup; - ``` +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** -* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - ```java - private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; - // 值为【TBW102】,Just for testing or demo program - ``` +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 -* 消息重投:系统特性消息重试部分详解了三个参数的作用 - ```java - private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 - private int retryTimesWhenSendAsyncFailed = 2; // 异步 - private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 - ``` -* 消息队列: +**** - ```java - private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 - ``` -* 消息属性: - ```java - private int sendMsgTimeout = 3000; // 发送消息的超时限制 - private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 - private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M - private TraceDispatcher traceDispatcher = null; // 消息轨迹 +##### 存储优化 -构造方法: +###### 内存映射 -* 构造方法: +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - ```java - public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { - this.namespace = namespace; - this.producerGroup = producerGroup; - // 创建生产者实现对象 - defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); - } - ``` +* read:读取本地文件内容 -成员方法: +* write:将读取的内容通过网络发送出去 -* start():启动方法 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - ```java - public void start() throws MQClientException { - // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 - this.setProducerGroup(withNamespace(this.producerGroup)); - // 生产者实现对象启动 - this.defaultMQProducerImpl.start(); - if (null != traceDispatcher) { - // 消息轨迹的逻辑 - traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); - } - } - ``` +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 -* send():**发送消息**: +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 - ```java - public SendResult send(Message msg){ - // 校验消息 - Validators.checkMessage(msg, this); - // 设置消息 Topic - msg.setTopic(withNamespace(msg.getTopic())); - return this.defaultMQProducerImpl.send(msg); - } - ``` +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 -* request():请求方法,**需要消费者回执消息**,又叫回退消息 +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - ```java - public Message request(final Message msg, final MessageQueue mq, final long timeout) { - msg.setTopic(withNamespace(msg.getTopic())); - return this.defaultMQProducerImpl.request(msg, mq, timeout); - } - ``` +*** + + + +###### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + *** -#### 默认实现 +##### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +*** + + + +#### MappedFile ##### 成员属性 -DefaultMQProducerImpl 类是默认的生产者实现类 +MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** -成员变量: +MappedFile 类成员变量: -* 实例对象: +* 内存相关: ```java - private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 - private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k + private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 + private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 ``` -* 主题发布信息映射表:key 是 Topic,value 是发布信息 +* 数据位点: ```java - private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 + protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 + private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 + // flushedPosition-wrotePosition 之间的数据属于脏页 ``` -* 异步发送消息:相关信息 +* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue ```java - private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 - private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 - private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 + private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 + private File file; // 文件对象 ``` -* 定时器:执行定时任务 + **MF 中以物理偏移量作为文件名,可以更好的寻址和进行判断** + +* 内存映射: ```java - private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + protected FileChannel fileChannel; // 文件通道 + private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 ``` -* 状态信息:服务的状态,默认创建状态 +ReferenceResource 类成员变量: + +* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 ```java - private ServiceState serviceState = ServiceState.CREATE_JUST; + protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 ``` -* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 +* 存活状态:表示资源的存活状态 ```java - private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + protected volatile boolean available = true; ``` -* 容错策略:选择队列的容错策略 +* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 ```java - private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + protected volatile boolean cleanupOver = false; ``` -* 钩子:用来进行前置或者后置处理 +* 第一次关闭资源的时间:用来记录超时时间 ```java - ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 - ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 - private final RPCHook rpcHook; // 传递给 NettyRemotingClient + private volatile long firstShutdownTimestamp = 0; ``` -构造方法: + -* 默认构造: +*** - ```java - public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { - // 默认 RPC HOOK 是空 - this(defaultMQProducer, null); - } - ``` -* 有参构造: + +##### 成员方法 + +MappedFile 类核心方法: + +* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 ```java - public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { - // 属性赋值 - this.defaultMQProducer = defaultMQProducer; - this.rpcHook = rpcHook; - - // 创建【异步消息线程池任务队列】,长度是 5w - this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); - // 创建默认的异步消息任务线程池 - this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( - // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... - } + // 参数一:消息 参数二:追加消息回调 + public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) ``` - + ```java + // 将字节数组写入到文件通道 + public boolean appendMessage(final byte[] data) + ``` -**** +* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 + ```java + public int flush(final int flushLeastPages) + ``` +* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 -##### 成员方法 + ```java + public SelectMappedBufferResult selectMappedBuffer(int pos) + ``` -* start():启动方法,参数默认是 true,代表正常的启动路径 +* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 - * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + ```java + public boolean destroy(final long intervalForcibly) + ``` - * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER +* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 - * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + ```java + public boolean cleanup(final long currentRef) + ``` - `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件全部加载到内存** - * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + ```java + public void warmMappedFile(FlushDiskType type, int pages) + ``` - * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 +* mlock():锁住指定的内存区域避免被操作系统调到 **swap 空间**,一次性将一段数据读入到映射内存区域,减少了缺页异常的产生 - * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + ```java + public void mlock() + ``` - * `if (startFactory) `:正常启动路径 + swap space 是磁盘上的一块区域,可以是一个分区或者一个文件或者是组合。当系统物理内存不足时,Linux 会将内存中不常访问的数据保存到 swap 区域上,这样系统就可以有更多的物理内存为各个进程服务,而当系统需要访问 swap 上存储的内容时,需要通过**缺页中断**将 swap 上的数据加载到内存中 - `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 +ReferenceResource 类核心方法: - * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 +* hold():增加引用记数 refCount,方法加锁 - * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) - * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 - - * 生产者 msg 添加信息关联 ID 发送到 Broker - * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 - * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + ```java + public synchronized boolean hold() + ``` -* sendDefaultImpl():发送消息 +* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 ```java - //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 - private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 + public void shutdown(final long intervalForcibly) ``` - * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 - - * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 +* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 - * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 + ```java + public void release() + ``` - * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + - * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 - * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 - `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 - `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 - `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 +*** - * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** - * `return topicPublishInfo`:返回 TBW102 主题的发布信息 - * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 +#### MapQueue - * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name +##### 成员属性 - * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 +MappedFileQueue 用来管理 MappedFile 文件 - * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName +成员变量: - * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** +* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` - * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 - * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 - - * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + ```java + private final String storePath; + ``` - * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** +* 文件属性: - * `sendResult = this.sendKernelImpl`:核心发送方法 + ```java + private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 + private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + ``` - * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 +* 数据位点: - * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + ```java + private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition + private long committedWhere = 0; // 目录的提交位点 + ``` - `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker +* 消息存储: - * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + ```java + private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + ``` -* sendKernelImpl():**核心发送方法** +* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 ```java - //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 - private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + private final AllocateMappedFileService allocateMappedFileService; ``` - * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 - * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel - * `byte[] prevBody = msg.getBody()`:获取消息体 +*** - * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID - `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 - * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 +##### 成员方法 - * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 +核心方法: - * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 +* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 - * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + ```java + public boolean load() + ``` - * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 +* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile - `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + ```java + // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + ``` - * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 - * `request.setBody(msg.getBody())`:**将消息放入请求体** - * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 +* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere -* request():请求方法,消费者回执消息,这种消息是异步消息 + ```java + //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 + public boolean flush(final int flushLeastPages) + ``` - * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 +* findMappedFileByOffset():根据偏移量查询对象 - * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + ```java + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + ``` - * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** +* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 - * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + ```java + // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 + public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + ``` - ```java - public Message waitResponseMessage(final long timeout) throws InterruptedException { - // 请求挂起 - this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); - return this.responseMsg; - } +* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 + + ```java + // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; + // 参数二:ConsumerQueue 文件内每个数据单元固定大小 + public int deleteExpiredFileByOffset(long offset, int unitSize) + ``` - * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 - ```java - public void putResponseMessage(final Message responseMsg) { - this.responseMsg = responseMsg; - this.countDownLatch.countDown(); - } - ``` @@ -6054,276 +6036,229 @@ DefaultMQProducerImpl 类是默认的生产者实现类 -#### 路由信息 +#### CommitLog -TopicPublishInfo 类用来存储路由信息 +##### 成员属性 成员变量: -* 顺序消息: +* 魔数: ```java - private boolean orderTopic = false; + public final static int MESSAGE_MAGIC_CODE = -626843481; // 消息的第一个字段是大小,第二个字段就是魔数 + protected final static int BLANK_MAGIC_CODE = -875286124; // 文件尾消息的魔法值 ``` -* 消息队列: +* MappedFileQueue:用于管理 `../store/commitlog` 目录下的文件 ```java - private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 - private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + protected final MappedFileQueue mappedFileQueue; ``` +* 存储服务: + ```java - // 【消息队列类】 - public class MessageQueue implements Comparable, Serializable { - private String topic; - private String brokerName; - private int queueId;// 队列 ID - } + protected final DefaultMessageStore defaultMessageStore; // 存储模块对象,上层服务 + private final FlushCommitLogService flushCommitLogService; // 刷盘服务,默认实现是异步刷盘 ``` -* 路由数据:主题对应的路由数据 +* 回调器:控制消息的哪些字段添加到 MappedFile ```java - private TopicRouteData topicRouteData; + private final AppendMessageCallback appendMessageCallback; ``` +* 队列偏移量字典表:key 是主题队列 id,value 是偏移量 + ```java - public class TopicRouteData extends RemotingSerializable { - private String orderTopicConf; - private List queueDatas; // 队列数据 - private List brokerDatas; // Broker 数据 - private HashMap/* Filter Server */> filterServerTable; - } + protected HashMap topicQueueTable = new HashMap(1024); ``` +* 锁相关: + ```java - public class QueueData implements Comparable { - private String brokerName; // 节点名称 - private int readQueueNums; // 读队列数 - private int writeQueueNums; // 写队列数 - private int perm; // 权限 - private int topicSynFlag; - } + private volatile long beginTimeInLock = 0; // 写数据时加锁的开始时间 + protected final PutMessageLock putMessageLock; // 写锁,两个实现类:自旋锁和重入锁 ``` +构造方法: + +* 有参构造: + ```java - public class BrokerData implements Comparable { - private String cluster; // 集群名 - private String brokerName; // Broker节点名称 - private HashMap brokerAddrs; + public CommitLog(final DefaultMessageStore defaultMessageStore) { + // 创建 MappedFileQueue 对象 + // 参数1:../store/commitlog; 参数2:【1g】; 参数3:allocateMappedFileService + this.mappedFileQueue = new MappedFileQueue(...); + // 默认 异步刷盘,创建这个对象 + this.flushCommitLogService = new FlushRealTimeService(); + // 控制消息哪些字段追加到 mappedFile,【消息最大是 4M】 + this.appendMessageCallback = new DefaultAppendMessageCallback(...); + // 默认使用自旋锁 + this.putMessageLock = ...; } ``` -核心方法: + + +*** + -* selectOneMessageQueue():**选择消息队列**使用 + +##### 成员方法 + +CommitLog 类核心方法: + +* start():会启动刷盘服务 ```java - // 参数是上次失败时的 brokerName,可以为 null - public MessageQueue selectOneMessageQueue(final String lastBrokerName) { - if (lastBrokerName == null) { - return selectOneMessageQueue(); - } else { - // 遍历消息队列 - for (int i = 0; i < this.messageQueueList.size(); i++) { - // 获取队列的索引 - int index = this.sendWhichQueue.getAndIncrement(); - // 获取队列的下标位置 - int pos = Math.abs(index) % this.messageQueueList.size(); - if (pos < 0) - pos = 0; - // 获取消息队列 - MessageQueue mq = this.messageQueueList.get(pos); - // 与上次选择的不同就可以返回 - if (!mq.getBrokerName().equals(lastBrokerName)) { - return mq; - } - } - return selectOneMessageQueue(); - } - } + public void start() ``` - +* shutdown():关闭刷盘服务 + ```java + public void shutdown() + ``` +* load():加载 CommitLog 目录下的文件 -**** + ```java + public boolean load() + ``` +* getMessage():根据 offset 查询单条信息,返回的结果对象内部封装了一个 ByteBuffer,该 Buffer 表示 `[offset, offset + size]` 区间的 MappedFile 的数据 + ```java + public SelectMappedBufferResult getMessage(final long offset, final int size) + ``` -### 客户端 +* deleteExpiredFile():删除过期文件,方法由 DefaultMessageStore 的定时任务调用 -#### 实例对象 + ```java + public int deleteExpiredFile() + ``` -##### 成员属性 +* asyncPutMessage():**存储消息** -MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + ```java + public CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) + ``` -成员变量: + * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 + * `String topic = msg.getTopic()`:获取主题和队列 ID + * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 -* 配置信息: + * `putMessageLock.lock()`:获取**写锁** + * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 + * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 + * `result = mappedFile.appendMessage(msg, this.appendMessageCallback)`:**消息追加**,核心逻辑在回调器类 + * `putMessageLock.unlock()`:释放写锁 + * `this.defaultMessageStore.unlockMappedFile(..)`:将 MappedByteBuffer 从 lock 切换为 unlock 状态 + * `putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result)`:结果封装 + * `flushResultFuture = submitFlushRequest(result, msg)`:**唤醒刷盘线程** + * `replicaResultFuture = submitReplicaRequest(result, msg)`:HA 消息同步 + +* recoverNormally():正常关机时的恢复方法,存储模块启动时**先恢复所有的 ConsumeQueue 数据,再恢复 CommitLog 数据** ```java - private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 - private final String clientId; // 客户端 ID ip@pid - private final long bootTimestamp; // 客户端的启动时间 - private ServiceState serviceState; // 客户端状态 + // 参数表示恢复阶段 ConsumeQueue 中已知的最大的消息 offset + public void recoverNormally(long maxPhyOffsetOfConsumeQueue) ``` -* 生产者消费者的映射表:key 是组名 + * `int index = mappedFiles.size() - 3`:从倒数第三个 file 开始向后恢复 - ```java - private final ConcurrentMap producerTable - private final ConcurrentMap consumerTable - private final ConcurrentMap adminExtTable - ``` + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次从切片内解析出一条 msg 封装成 DispatchRequest 对象 -* 网络层配置: + * `size = dispatchRequest.getMsgSize()`:获取消息的大小,检查 DispatchRequest 对象的状态 - ```java - private final NettyClientConfig nettyClientConfig; - ``` + 情况 1:正常数据,则 `mappedFileOffset += size` -* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + 情况 2:文件尾数据,处理下一个文件,mappedFileOffset 置为 0,magic_code 表示文件尾 - ```java - private final MQClientAPIImpl mQClientAPIImpl; - ``` + * `processOffset += mappedFileOffset`:计算出正确的数据存储位点,并设置 MappedFileQueue 的目录刷盘位点 -* 本地路由数据:key 是主题名称,value 路由信息 + * `this.mappedFileQueue.truncateDirtyFiles(processOffset)`:调整 MFQ 中文件的刷盘位点 - ```java - private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + * `if (maxPhyOffsetOfConsumeQueue >= processOffset)`:删除冗余数据,将超过全局位点的 CQ 下的文件删除,将包含全局位点的 CQ 下的文件重新定位 -* 锁信息:两把锁,锁不同的数据 +* recoverAbnormally():异常关机时的恢复方法 ```java - private final Lock lockNamesrv = new ReentrantLock(); - private final Lock lockHeartbeat = new ReentrantLock(); + public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) ``` -* 调度线程池:单线程,执行定时任务 + * `int index = mappedFiles.size() - 1`:从尾部开始遍历 MFQ,验证 MF 的第一条消息,找到第一个验证通过的文件对象 + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次解析出一条 msg 封装成 DispatchRequest 对象 + * `this.defaultMessageStore.doDispatch(dispatchRequest)`:重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐 + * 剩余逻辑与正常关机的恢复方法相似 - ```java - private final ScheduledExecutorService scheduledExecutorService; - ``` +消息追加服务 DefaultAppendMessageCallback -* Broker 映射表:key 是 BrokerName +* doAppend() + * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset + * `String msgId`:消息 ID,规则是客户端 IP + 消息偏移量 phyOffset + * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 + * `byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen)`:将 msgStoreItemMemory 中的数据写入 MF 对象的内存映射的 Buffer 中,数据还没落盘 + * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 + * `CommitLog.this.topicQueueTable.put(key, ++queueOffset)`:更新队列偏移量 - ```java - // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port - private final ConcurrentMap> brokerAddrTable; - // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 - ConcurrentMap> brokerVersionTable; - ``` -* **客户端的协议处理器**:用于处理 IO 事件 - ```java - private final ClientRemotingProcessor clientRemotingProcessor; - ``` +**** -* 消息服务: - ```java - private final PullMessageService pullMessageService; // 拉消息服务 - private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 - ``` -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +#### ConsQueue + +##### 成员属性 + +ConsumerQueue 是消息消费队列,存储消息在 CommitLog 的索引,便于快速定位消息 + +成员变量: + +* 数据单元:ConsumerQueueData 数据单元的固定大小是 20 字节,默认申请 20 字节的缓冲区 ```java - private final DefaultMQProducer defaultMQProducer; + public static final int CQ_STORE_UNIT_SIZE = 20; ``` -* 心跳次数统计: +* 文件管理: ```java - private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + private final MappedFileQueue mappedFileQueue; // 文件管理器,管理 CQ 目录下的文件 + private final String storePath; // 目录,比如../store/consumequeue/xxx_topic/0 + private final int mappedFileSize; // 每一个 CCQ 存储文件大小,默认 20 * 30w = 600w byte ``` -* 公共配置类: +* 存储主模块:上层的对象 ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } + private final DefaultMessageStore defaultMessageStore; ``` -构造方法: - -* MQClientInstance 有参构造: +* 消息属性: ```java - public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { - this.clientConfig = clientConfig; - this.instanceIndex = instanceIndex; - // Netty 相关的配置信息 - this.nettyClientConfig = new NettyClientConfig(); - // 平台核心数 - this.nettyClientConfig.setClientCallbackExecutorThreads(...); - this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); - // 【创建客户端协议处理器】 - this.clientRemotingProcessor = new ClientRemotingProcessor(this); - // 创建 API 实现对象 - // 参数一:客户端网络配置 - // 参数二:客户端协议处理器,注册到客户端网络层 - // 参数三:rpcHook,注册到客户端网络层 - // 参数四:客户端配置 - this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); - - //... - // 内部生产者,指定内部生产者的组 - this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); - } + private final String topic; // CQ 主题 + private final int queueId; // CQ 队列,每一个队列都有一个 ConsumeQueue 对象进行管理 + private final ByteBuffer byteBufferIndex; // 临时缓冲区,插新的 CQData 时使用 + private long maxPhysicOffset = -1; // 当前ConsumeQueue内存储的最大消息物理偏移量 + private volatile long minLogicOffset = 0; // 当前ConsumeQueue内存储的最小消息物理偏移量 ``` -* MQClientAPIImpl 有参构造: +构造方法: + +* 有参构造: ```java - public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { - this.clientConfig = clientConfig; - topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); - // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event - this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); - // 业务处理器 - this.clientRemotingProcessor = clientRemotingProcessor; - // 注册 RpcHook - this.remotingClient.registerRPCHook(rpcHook); - // ... - // 注册回退消息的请求码 - this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + public ConsumeQueue() { + // 申请了一个 20 字节大小的 临时缓冲区 + this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); } ``` - + *** @@ -6331,340 +6266,281 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 ##### 成员方法 -* start():启动方法 +ConsumeQueue 启动阶段方法: - * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 - * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 - * `this.startScheduledTask()`:启动定时任务 - * `this.pullMessageService.start()`:启动拉取消息服务 - * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 +* load():第一步,加载 storePath 目录下的文件,初始化 MappedFileQueue +* recover():第二步,恢复 ConsumeQueue 数据 + * 从倒数第三个 MF 文件开始向后遍历,依次读取 MF 中 20 个字节的 CQData 数据,检查 offset 和 size 是否是有效数据 + * 找到无效的 CQData 的位点,该位点就是 CQ 的刷盘点和数据顺序写入点 + * 删除无效的 MF 文件,调整当前顺序写的 MF 文件的数据位点 -* startScheduledTask():**启动定时任务**,调度线程池是单线程 +其他方法: - * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 +* truncateDirtyLogicFiles():CommitLog 恢复阶段调用,将 ConsumeQueue 有效数据文件与 CommitLog 对齐,将超出部分的数据文删除掉,并调整当前文件的数据位点。Broker 启动阶段先恢复 CQ 的数据,再恢复 CL 数据,但是**数据要以 CL 为基准** - * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + ```java + // 参数是最大消息物理偏移量 + public void truncateDirtyLogicFiles(long phyOffet) + ``` - ```java - // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 - MQClientInstance.this.updateTopicRouteInfoFromNameServer(); - ``` +* flush():刷盘,调用 MFQ 的刷盘方法 - * 定时任务 2:周期 30 秒一次,两个任务 + ```java + public boolean flush(final int flushLeastPages) + ``` - * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 - * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 +* deleteExpiredFile():删除过期文件,将小于 offset 的所有 MF 文件删除,offset 是 CommitLog 目录下最小的物理偏移量,小于该值的 CL 文件已经没有了,所以 CQ 也没有存在的必要 - ```java - MQClientInstance.this.cleanOfflineBroker(); - MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); - ``` + ```java + public int deleteExpiredFile(long offset) + ``` - ```java - // 心跳数据 - public class HeartbeatData extends RemotingSerializable { - // 客户端 ID ip@pid - private String clientID; - // 存储客户端所有生产者数据 - private Set producerDataSet = new HashSet(); - // 存储客户端所有消费者数据 - private Set consumerDataSet = new HashSet(); - } - ``` +* putMessagePositionInfoWrapper():**向 CQ 中追加 CQData 数据**,由存储主模块 DefaultMessageStore 内部的异步线程调用,负责构建 ConsumeQueue 文件和 Index 文件的,该线程会持续关注 CommitLog 文件,当 CommitLog 文件内有新数据写入,就读出来封装成 DispatchRequest 对象,转发给 ConsumeQueue 或者 IndexService - * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + ```java + public void putMessagePositionInfoWrapper(DispatchRequest request) + ``` - ```java - MQClientInstance.this.persistAllConsumerOffset(); - ``` +* getIndexBuffer():转换 startIndex 为 offset,获取包含该 offset 的 MappedFile 文件,读取 `[offset%maxSize, mfPos]` 范围的数据,包装成结果对象返回 - * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + ```java + public SelectMappedBufferResult getIndexBuffer(final long startIndex) + ``` - ```java - MQClientInstance.this.adjustThreadPool(); - ``` -* updateTopicRouteInfoFromNameServer():**更新路由数据** - * `if (isDefault && defaultMQProducer != null)`:需要默认数据 +**** - `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 - `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 - * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) +#### IndexFile - * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 +##### 索引机制 - * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 +RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - * `if (changed)`:不一致进入更新逻辑 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 +IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - `Update Pub info`:更新生产者信息 +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** - * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** - * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - `Update sub info`:更新消费者信息 +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 +参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md -**** +*** -#### 网络通信 -##### 成员属性 -NettyRemotingClient 类负责客户端的网络通信 +##### 成员属性 -成员变量: +IndexFile 类成员属性 -* Netty 服务相关属性: +* 哈希: ```java - private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 - private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 - private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + private static int hashSlotSize = 4; // 每个 hash 桶的大小是 4 字节,【用来存放索引的编号】 + private final int hashSlotNum; // hash 桶的个数,默认 500 万 ``` -* Channel 映射表: +* 索引: ```java - private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 - private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + private static int indexSize = 20; // 每个 index 条目的大小是 20 字节 + private static int invalidIndex = 0; // 无效索引编号:0 特殊值 + private final int indexNum; // 默认值:2000w + private final IndexHeader indexHeader; // 索引头 ``` -* 定时器:启动定时任务 +* 映射: ```java - private final Timer timer = new Timer("ClientHouseKeepingService", true) + private final MappedFile mappedFile; // 【索引文件使用的 MF 文件】 + private final FileChannel fileChannel; // 文件通道 + private final MappedByteBuffer mappedByteBuffer;// 从 MF 中获取的内存映射缓冲区 ``` -* 线程池: +构造方法: + +* 有参构造 ```java - private ExecutorService publicExecutor; // 公共线程池 - private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + // endPhyOffset 上个索引文件 最后一条消息的 物理偏移量 + // endTimestamp 上个索引文件 最后一条消息的 存储时间 + public IndexFile(final String fileName, final int hashSlotNum, final int indexNum, + final long endPhyOffset, final long endTimestamp) throws IOException { + // 文件大小 40 + 500w * 4 + 2000w * 20 + int fileTotalSize = + IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize); + // 创建 mf 对象,会在disk上创建文件 + this.mappedFile = new MappedFile(fileName, fileTotalSize); + // 创建 索引头对象,传递 索引文件mf 的切片数据 + this.indexHeader = new IndexHeader(byteBuffer); + //... + } ``` -* 事件监听器:客户端这里是 null - ```java - private final ChannelEventListener channelEventListener; - ``` -* Netty 配置对象: +**** - ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; - } - ``` -构造方法 -* 无参构造: +##### 成员方法 - ```java - public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { - this(nettyClientConfig, null); - } - ``` +IndexFile 类方法 -* 有参构造: +* load():加载 IndexHeader ```java - public NettyRemotingClient(nettyClientConfig, channelEventListener) { - // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 - super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); - this.nettyClientConfig = nettyClientConfig; - this.channelEventListener = channelEventListener; - - // 创建公共线程池 - int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); - if (publicThreadNums <= 0) { - publicThreadNums = 4; - } - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); - - // 创建 Netty IO 线程,1个线程 - this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); - - if (nettyClientConfig.isUseTLS()) { - sslContext = TlsHelper.buildSslContext(true); - } - } + public void load() ``` +* flush():MappedByteBuffer 内的数据强制落盘 + ```java + public void flush() + ``` -**** +* isWriteFull():检查当前的 IndexFile 已写索引数是否 >= indexNum,达到该值则当前 IndexFile 不能继续追加 IndexData 了 + ```java + public boolean isWriteFull() + ``` +* destroy():删除文件时使用的方法 -##### 成员方法 + ```java + public boolean destroy(final long intervalForcibly) + ``` -* start():启动方法 +* putKey():添加索引数据,解决哈希冲突使用**头插法** ```java - public void start() { - // channel pipeline 内的 handler 使用的线程资源,默认 4 个 - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); - // 配置 netty 客户端启动类对象 - Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) - //... - .handler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - // 加几个handler - pipeline.addLast( - // 服务端的数据,都会来到这个 - new NettyClientHandler()); - } - }); - // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 - // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 - this.timer.scheduleAtFixedRate(() -> { - NettyRemotingClient.this.scanResponseTable(); - }, 1000 * 3, 1000); - // 这里是 null,不启动 - if (this.channelEventListener != null) { - this.nettyEventExecutor.start(); - } - } + // 参数一:消息的 key,uniq_key 或者 keys="aaa bbb ccc" 会分别为 aaa bbb ccc 创建索引 + // 参数二:消息的物理偏移量; 参数三:消息存储时间 + public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) ``` -* 单向通信: + * `int slotPos = keyHash % this.hashSlotNum`:对 key 计算哈希后,取模得到对应的哈希槽 slot 下标,然后计算出哈希槽的存储位置 absSlotPos + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,如果是无效值说明没有哈希冲突 + * `timeDiff = timeDiff / 1000`:计算当前 msg 存储时间减去索引文件内第一条消息存储时间的差值,转化为秒进行存储 + * `int absIndexPos`:计算当前索引数据存储的位置,开始填充索引数据到对应的位置 + * `this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount())`:在 slot 放入当前索引的索引编号 + * `if (this.indexHeader.getIndexCount() <= 1)`:索引文件插入的第一条数据,需要设置起始偏移量和存储时间 + * `if (invalidIndex == slotValue)`:没有哈希冲突,说明占用了一个新的 hash slot + * `this.indexHeader`:设置索引头的相关属性 + +* selectPhyOffset():从索引文件查询消息的物理偏移量 ```java - public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { - // 开始时间 - long beginStartTime = System.currentTimeMillis(); - // 获取或者创建客户端与服务端(addr)的通道 channel - final Channel channel = this.getAndCreateChannel(addr); - // 条件成立说明客户端与服务端 channel 通道正常,可以通信 - if (channel != null && channel.isActive()) { - try { - // 执行 rpcHook 拓展点 - doBeforeRpcHooks(addr, request); - // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 - long costTime = System.currentTimeMillis() - beginStartTime; - if (timeoutMillis < costTime) { - throw new RemotingTimeoutException("invokeSync call timeout"); - } - // 参数1:客户端-服务端通道channel - // 参数二:网络层传输对象,封装着请求数据 - // 参数三:剩余的超时限制 - RemotingCommand response = this.invokeSyncImpl(channel, request, ...); - // 后置处理 - doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); - // 返回响应数据 - return response; - } catch (RemotingSendRequestException e) {} - } else { - this.closeChannel(addr, channel); - throw new RemotingConnectException(addr); - } - } + // 参数一:查询结果全部放到该list内; 参数二:查询key; 参数三:结果最大数限制; 参数四五:时间范围 + public void selectPhyOffset(final List phyOffsets, final String key, final int maxNum,final long begin, final long end, boolean lock) ``` - + * `if (this.mappedFile.hold())`: MF 的引用记数 +1,查询期间 MF 资源**不能被释放** + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,可能是无效值或者索引编号,如果是无效值说明查询未命中 + * `int absIndexPos`:计算出索引编号对应索引数据的开始位点 + * `this.mappedByteBuffer`:读取索引数据 + * `long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff`:计算出准确的存储时间 + * `boolean timeMatched = (timeRead >= begin) && (timeRead <= end)`:时间范围的匹配 + * `phyOffsets.add(phyOffsetRead)`:将命中的消息索引的消息偏移量加入到 list 集合中 + * `nextIndexToRead = prevIndexRead`:遍历前驱节点 -*** +**** -### 存储端 -#### 存储机制 -##### 存储结构 +#### IndexServ -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 +##### 成员属性 -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** +IndexService 类用来管理 IndexFile 文件 -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 +成员变量: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) +* 存储主模块: -* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M -* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 hash 索引 + ```java + private final DefaultMessageStore defaultMessageStore; + ``` -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 +* 索引文件存储目录:`../store/index` -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + ```java + private final String storePath; + ``` +* 索引对象集合:目录下的每个文件都有一个 IndexFile 对象 + ```java + private final ArrayList indexFileList = new ArrayList(); + ``` -**** +* 索引文件: + ```java + private final int hashSlotNum; // 每个索引文件包含的 哈希桶数量 :500w + private final int indexNum; // 每个索引文件包含的 索引条目数量 :2000w + ``` -##### 存储优化 -###### 内存映射 +*** -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: -* read:读取本地文件内容 -* write:将读取的内容通过网络发送出去 +##### 成员方法 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) +* load():加载 storePath 目录下的文件,为每个文件创建一个 IndexFile 实例对象,并加载 IndexHeader 信息 -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + ```java + public boolean load(final boolean lastExitOK) + ``` -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 +* deleteExpiredFile():删除过期索引文件 -RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + ```java + // 参数 offset 表示 CommitLog 内最早的消息的 phyOffset + public void deleteExpiredFile(long offset) + ``` -MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + * `this.readWriteLock.readLock().lock()`:加锁判断 + * `long endPhyOffset = this.indexFileList.get(0).getEndPhyOffset()`:获取目录中第一个文件的结束偏移量 + * `if (endPhyOffset < offset)`:索引目录内存在过期的索引文件,并且当前的 IndexFile 都是过期的数据 + * `for (int i = 0; i < (files.length - 1); i++)`:遍历文件列表,删除过期的文件 +* buildIndex():存储主模块 DefaultMessageStore 内部的异步线程调用,构建 Index 数据 + ```java + public void buildIndex(DispatchRequest req) + ``` -*** + * `indexFile = retryGetAndCreateIndexFile()`:获取或者创建顺序写的索引文件对象 + * `buildKey(topic, req.getUniqKey())`:**构建索引 key**,`topic + # + uniqKey` + * `indexFile = putKey()`:插入索引文件 -###### 页缓存 + * `if (keys != null && keys.length() > 0)`:消息存在自定义索引 -页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + `for (int i = 0; i < keyset.length; i++)`:遍历每个索引,为每个 key 调用一次 putKey -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) +* getAndCreateLastIndexFile():获取当前顺序写的 IndexFile,没有就创建 -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + ```java + public IndexFile getAndCreateLastIndexFile() + ``` @@ -6672,175 +6548,236 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 -##### 刷盘机制 +#### MesStore -两种持久化的方案: +##### 生命周期 -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 +DefaultMessageStore 类核心是整个存储服务的调度类 -RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 +* 构造方法: -* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + ```java + public DefaultMessageStore() + ``` -* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** + * `this.indexService.start()`:启动索引服务 -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 +* load():加载资源 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + ```java + public boolean load() + ``` + * `this.commitLog.load()`:先加载 CommitLog + * `this.loadConsumeQueue()`:再加载 ConsumeQueue + * `this.storeCheckpoint`:检查位点对象 + * `this.indexService.load(lastExitOK)`:加载 IndexFile + * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL +* start():核心启动方法 -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + ```java + public void start() + ``` + * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker + * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 -*** + * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile + * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 + * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 -#### MappedFile + * `this.haService.start()`:启动 **HA 服务** -##### 成员属性 + * `this.handleScheduleMessageService()`:启动**消息调度服务** -MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** + * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** -MappedFile 类成员变量: + * `this.commitLog.start()`:启动 **CL 刷盘服务** -* 内存相关: + * `this.storeStatsService.start()`:启动状态存储服务 - ```java - public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k - private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 - private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 - ``` + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** -* 数据位点: + * `this.addScheduleTask()`:添加定时任务 - ```java - protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 - protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 - private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 - // flushedPosition-wrotePosition 之间的数据属于脏页 - ``` + * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 -* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue + * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 + * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 - ```java - private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 - private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 - private File file; // 文件对象 - ``` + * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 -* 内存映射: + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 - ```java - protected FileChannel fileChannel; // 文件通道 - private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 - ``` + * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% -ReferenceResource 类成员变量: + `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 -* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 + * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 - ```java - protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 - ``` + * `this.shutdown = false`:刚启动,设置为 false -* 存活状态:表示资源的存活状态 +* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 ```java - protected volatile boolean available = true; + public void shutdown() ``` -* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 +* destroy():销毁 Broker 的工作目录 ```java - protected volatile boolean cleanupOver = false; + public void destroy() ``` -* 第一次关闭资源的时间:用来记录超时时间 - ```java - private volatile long firstShutdownTimestamp = 0; - ``` - + *** -##### 成员方法 +##### 服务线程 -MappedFile 类核心方法: +ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 -* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 +* run():一般实现方式 ```java - // 参数一:消息 参数二:追加消息回调 - public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) + public void run() { + while (!this.isStopped()) { + // 业务逻辑 + } + } ``` + 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + ```java - // 将字节数组写入到文件通道 - public boolean appendMessage(final byte[] data) + protected volatile boolean stopped = false ``` -* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 +* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 ```java - public int flush(final int flushLeastPages) + public void shutdown() ``` -* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 +* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false ```java - public SelectMappedBufferResult selectMappedBuffer(int pos) + protected void waitForRunning(long interval) ``` -* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 +* wakeup():唤醒线程,设置 hasNotified 为 true ```java - public boolean destroy(final long intervalForcibly) + public void wakeup() ``` -* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 - ```java - public boolean cleanup(final long currentRef) - ``` - -* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件都加载到内存** + +*** + + + +##### 构建服务 + +AllocateMappedFileService 创建 MappedFile 服务 + +* mmapOperation():核心服务 ```java - public void warmMappedFile(FlushDiskType type, int pages) + private boolean mmapOperation() ``` -ReferenceResource 类核心方法: + * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 + * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 + * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 + * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 + * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 + * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** -* hold():增加引用记数 refCount,方法加锁 +* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 ```java - public synchronized boolean hold() + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) ``` -* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 + * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 + * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 + * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 + * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + +ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + +* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 ```java - // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 - public void shutdown(final long intervalForcibly) + public void run() ``` -* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 +* doReput():实现分发的核心逻辑 ```java - public void release() + private void doReput() ``` - + * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 + * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 + * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 + * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** + * `this.reputFromOffset += size`:更新数据范围 + + + +*** + + + +##### 刷盘服务 + +FlushConsumeQueueService 刷盘 CQ 数据 + +* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + + ```java + public void run() + ``` +* doFlush():刷盘 + ```java + private void doFlush(int retryTimes) + ``` + + * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + + * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 + * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 + * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 + * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + +FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + * `int interval`:获取配置中的刷盘时间间隔 + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** @@ -6848,120 +6785,1005 @@ ReferenceResource 类核心方法: -#### MapQueue +##### 清理服务 -MappedFileQueue 用来管理 MappedFile 文件 +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,先清理 CL,再清理 CQ,因为 CQ 依赖于 CL 的数据 -成员变量: +* run():运行方法 -* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CL 文件 ```java - private final String storePath; + private void deleteExpiredFiles() ``` -* 文件属性: + * `long fileReservedTime`:默认 72,代表文件的保留时间 + * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 + * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% + * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 + * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 + * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + +CleanConsumeQueueService 清理过期的 CQ 数据 + +* run():运行方法 ```java - private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 - private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + public void run() ``` -* 数据位点: +* deleteExpiredFiles():删除过期 CQ 文件 ```java - private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition - private long committedWhere = 0; // 目录的提交位点 + private void deleteExpiredFiles() ``` -* 消息存储: + * `int deleteLogicsFilesInterval`:清理 CQ 的时间间隔,默认 100 毫秒 + * `long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()`:获取 CL 文件中最小的物理偏移量 + * `if (minOffset > this.lastPhysicalMinOffset)`:CL 最小的偏移量大于 CQ 最小的,说明有过期数据 + * `this.lastPhysicalMinOffset = minOffset`:更新 CQ 的最小偏移量 + * `for (ConsumeQueue logic : maps.values())`:遍历所有的 CQ 文件 + * `logic.deleteExpiredFile(minOffset)`:调用 MFQ 对象的删除方法 + * `DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset)`:**删除过期的索引文件** + + + +*** + + + +#### Broker + +BrokerStartup 启动方法 + +```java +public static void main(String[] args) { + start(createBrokerController(args)); +} +public static BrokerController start(BrokerController controller) { + controller.start(); // 启动 +} +``` + +BrokerController#start:核心启动方法 + +* `this.messageStore.start()`:**启动存储服务** + +* `this.remotingServer.start()`:启动 Netty 通信服务 + +* `this.fileWatchService.start()`:启动文件监听服务 + +* `this.scheduledExecutorService.scheduleAtFixedRate()`:每隔 30s 向 NameServer 上报 Topic 路由信息,**心跳机制** + + `BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister())` + + + + + +**** + + + +### 生产者 + +#### 生产者类 + +DefaultMQProducer 是生产者的默认实现类 + +成员变量: + +* 生产者实现类: ```java - private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + protected final transient DefaultMQProducerImpl defaultMQProducerImpl ``` -* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 +* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 ```java - private final AllocateMappedFileService allocateMappedFileService; + private String producerGroup; ``` -核心方法: +* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 -* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 + ```java + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + // 值为【TBW102】,Just for testing or demo program + ``` + +* 消息重投:系统特性消息重试部分详解了三个参数的作用 ```java - public boolean load() + private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 + private int retryTimesWhenSendAsyncFailed = 2; // 异步 + private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 ``` -* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile +* 消息队列: ```java - // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile - public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 ``` -* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere +* 消息属性: ```java - //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 - public boolean flush(final int flushLeastPages) + private int sendMsgTimeout = 3000; // 发送消息的超时限制 + private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 + private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M + private TraceDispatcher traceDispatcher = null; // 消息轨迹 + +构造方法: + +* 构造方法: + + ```java + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + // 创建生产者实现对象 + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } ``` -* findMappedFileByOffset():根据偏移量查询对象 +成员方法: + +* start():启动方法 ```java - public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + public void start() throws MQClientException { + // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 + this.setProducerGroup(withNamespace(this.producerGroup)); + // 生产者实现对象启动 + this.defaultMQProducerImpl.start(); + if (null != traceDispatcher) { + // 消息轨迹的逻辑 + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } + } ``` -* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 +* send():**发送消息**: ```java - // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 - public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + public SendResult send(Message msg){ + // 校验消息 + Validators.checkMessage(msg, this); + // 设置消息 Topic + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg); + } ``` -* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 +* request():请求方法,**需要消费者回执消息**,又叫回退消息 ```java - // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; - // 参数二:ConsumerQueue 文件内每个数据单元固定大小 - public int deleteExpiredFileByOffset(long offset, int unitSize) + public Message request(final Message msg, final MessageQueue mq, final long timeout) { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } ``` - +*** + + + +#### 默认实现 + +##### 成员属性 + +DefaultMQProducerImpl 类是默认的生产者实现类 + +成员变量: + +* 实例对象: + + ```java + private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 + private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + ``` +* 主题发布信息映射表:key 是 Topic,value 是发布信息 + ```java + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + ``` +* 异步发送消息:相关信息 + ```java + private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 + private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 + private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + ``` +* 定时器:执行定时任务 + ```java + private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + ``` +* 状态信息:服务的状态,默认创建状态 + ```java + private ServiceState serviceState = ServiceState.CREATE_JUST; + ``` +* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 + ```java + private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + ``` +* 容错策略:选择队列的容错策略 + ```java + private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + ``` +* 钩子:用来进行前置或者后置处理 + ```java + ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 + ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 + private final RPCHook rpcHook; // 传递给 NettyRemotingClient + ``` +构造方法: +* 默认构造: + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + // 默认 RPC HOOK 是空 + this(defaultMQProducer, null); + } + ``` +* 有参构造: + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { + // 属性赋值 + this.defaultMQProducer = defaultMQProducer; + this.rpcHook = rpcHook; + + // 创建【异步消息线程池任务队列】,长度是 5w + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); + // 创建默认的异步消息任务线程池 + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... + } + ``` + +**** +##### 成员方法 +* start():启动方法,参数默认是 true,代表正常的启动路径 + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER + * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID + + * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + + * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 + + * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + + * `if (startFactory) `:正常启动路径 + + `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + + * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 + + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) + * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 + + * 生产者 msg 添加信息关联 ID 发送到 Broker + * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 + * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + +* sendDefaultImpl():发送消息 + + ```java + //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 + private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + ``` + + * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 + + * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 + + * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 + + * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + + * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 + + * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 + + `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 + + `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 + + `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 + + * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** + + * `return topicPublishInfo`:返回 TBW102 主题的发布信息 + + * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 + + * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name + + * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 + + * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName + + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** + + * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 + * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 + + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + + * `sendResult = this.sendKernelImpl`:核心发送方法 + + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 + + * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker + + * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + +* sendKernelImpl():**核心发送方法** + + ```java + //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 + private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + ``` + + * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 + + * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel + + * `byte[] prevBody = msg.getBody()`:获取消息体 + + * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID + + `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + + * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 + + * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 + + * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 + + * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + + * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 + + `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + + * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 + * `request.setBody(msg.getBody())`:**将消息放入请求体** + * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 + +* request():请求方法,消费者回执消息,这种消息是异步消息 + + * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 + + * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + + * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + + ```java + public Message waitResponseMessage(final long timeout) throws InterruptedException { + // 请求挂起 + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + + ```java + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + ``` + + + + +*** + + + +#### 路由信息 + +TopicPublishInfo 类用来存储路由信息 + +成员变量: + +* 顺序消息: + + ```java + private boolean orderTopic = false; + ``` + +* 消息队列: + + ```java + private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + ``` + + ```java + // 【消息队列类】 + public class MessageQueue implements Comparable, Serializable { + private String topic; + private String brokerName; + private int queueId;// 队列 ID + } + ``` + +* 路由数据:主题对应的路由数据 + + ```java + private TopicRouteData topicRouteData; + ``` + + ```java + public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; // 队列数据 + private List brokerDatas; // Broker 数据 + private HashMap/* Filter Server */> filterServerTable; + } + ``` + + ```java + public class QueueData implements Comparable { + private String brokerName; // 节点名称 + private int readQueueNums; // 读队列数 + private int writeQueueNums; // 写队列数 + private int perm; // 权限 + private int topicSynFlag; + } + ``` + + ```java + public class BrokerData implements Comparable { + private String cluster; // 集群名 + private String brokerName; // Broker节点名称 + private HashMap brokerAddrs; + } + ``` + +核心方法: + +* selectOneMessageQueue():**选择消息队列**使用 + + ```java + // 参数是上次失败时的 brokerName,可以为 null + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { + if (lastBrokerName == null) { + return selectOneMessageQueue(); + } else { + // 遍历消息队列 + for (int i = 0; i < this.messageQueueList.size(); i++) { + // 获取队列的索引 + int index = this.sendWhichQueue.getAndIncrement(); + // 获取队列的下标位置 + int pos = Math.abs(index) % this.messageQueueList.size(); + if (pos < 0) + pos = 0; + // 获取消息队列 + MessageQueue mq = this.messageQueueList.get(pos); + // 与上次选择的不同就可以返回 + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + ``` + + + + + +**** + + + +### 消费者 + + + + + + + +*** + + + +### 客户端 + +#### 实例对象 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: + + ```java + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 + ``` + +* 生产者消费者的映射表:key 是组名 + + ```java + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable + ``` + +* 网络层配置: + + ```java + private final NettyClientConfig nettyClientConfig; + ``` + +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + + ```java + private final MQClientAPIImpl mQClientAPIImpl; + ``` + +* 本地路由数据:key 是主题名称,value 路由信息 + + ```java + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + +* 锁信息:两把锁,锁不同的数据 + + ```java + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + ``` + +* 调度线程池:单线程,执行定时任务 + + ```java + private final ScheduledExecutorService scheduledExecutorService; + ``` + +* Broker 映射表:key 是 BrokerName + + ```java + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; + ``` + +* **客户端的协议处理器**:用于处理 IO 事件 + + ```java + private final ClientRemotingProcessor clientRemotingProcessor; + ``` + +* 消息服务: + + ```java + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + ``` + +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 + + ```java + private final DefaultMQProducer defaultMQProducer; + ``` + +* 心跳次数统计: + + ```java + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + ``` + +* 公共配置类: + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +构造方法: + +* MQClientInstance 有参构造: + + ```java + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } + ``` + +* MQClientAPIImpl 有参构造: + + ```java + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } + ``` + + + +*** + + + +##### 成员方法 + +* start():启动方法 + + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* Netty 配置对象: + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + + + + + +*** diff --git a/Java.md b/Java.md index c9daa70..b0a6075 100644 --- a/Java.md +++ b/Java.md @@ -73,7 +73,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - 这种类型主要使用在需要比较大整数的系统上 - 默认值是 **` 0L`** - 例子: `long a = 100000L,Long b = -200000L` - "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩,所以最好大写 + L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩,所以最好大写 **float:** From 3bca1287329a4553f1399ed6d2a3282378100f2c Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 9 Jan 2022 22:50:25 +0800 Subject: [PATCH 13/78] Update Java Notes --- Frame.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Frame.md b/Frame.md index 6f84a04..183cc5f 100644 --- a/Frame.md +++ b/Frame.md @@ -4504,7 +4504,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性,每 10 秒清除一次两小时没有活跃的 Broker +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 是否还存活,每 10 秒清除一次两小时没有活跃的 Broker * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -5589,6 +5589,37 @@ NettyRemotingAbstract#processResponseCommand:处理响应的数据 +#### 路由信息 + +##### 信息管理 + +RouteInfoManager 类负责管理路由信息,NamesrvController 的构造方法中创建该类的实例对象,管理服务端的路由数据 + +```java +public class RouteInfoManager { + // Broker 两个小时不活跃,视为离线,被定时任务删除 + private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; + // 读写锁,保证线程安全 + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + // 主题队列数据,一个主题对应多个队列 + private final HashMap> topicQueueTable; + // Broker 数据列表 + private final HashMap brokerAddrTable; + // 集群 + private final HashMap> clusterAddrTable; + // Broker 存活信息 + private final HashMap brokerLiveTable; + // 服务过滤 + private final HashMap/* Filter Server */> filterServerTable; +} +``` + + + +*** + + + ##### 路由注册 DefaultRequestProcessor REGISTER_BROKER 方法解析: From 411b33b4315e1edc5a5d77749f6028ac442cc01b Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 15 Jan 2022 18:55:18 +0800 Subject: [PATCH 14/78] Update Java Notes --- DB.md | 4 ++++ Frame.md | 8 ++++---- Java.md | 4 ++-- Web.md | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/DB.md b/DB.md index c815155..41db319 100644 --- a/DB.md +++ b/DB.md @@ -4827,6 +4827,10 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); +参考文章:https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ + + + *** diff --git a/Frame.md b/Frame.md index 183cc5f..15ea0fc 100644 --- a/Frame.md +++ b/Frame.md @@ -3617,12 +3617,12 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce 消息发送者步骤分析: -1. 创建消息生产者 producer,并制定生产者组名 +1. 创建消息生产者 Producer,并制定生产者组名 2. 指定 Nameserver 地址 -3. 启动 producer +3. 启动 Producer 4. 创建消息对象,指定主题 Topic、Tag 和消息体 5. 发送消息 -6. 关闭生产者 producer +6. 关闭生产者 Producer 消息消费者步骤分析: @@ -3630,7 +3630,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce 2. 指定 Nameserver 地址 3. 订阅主题 Topic 和 Tag 4. 设置回调函数,处理消息 -5. 启动消费者 consumer +5. 启动消费者 Consumer diff --git a/Java.md b/Java.md index b0a6075..125e703 100644 --- a/Java.md +++ b/Java.md @@ -2551,7 +2551,7 @@ JDK 1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中 * 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) * 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 -JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 +JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的引用;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java public class Demo { @@ -2600,7 +2600,7 @@ String s = new String("ab"); -##### 面试问题 +##### 常见问题 问题一: diff --git a/Web.md b/Web.md index 5262ce7..3236ec1 100644 --- a/Web.md +++ b/Web.md @@ -2235,13 +2235,13 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 Cookie: Idea-b77ddca6=4bc282fe-febf-4fd1-b6c9-72e9e0f381e8 ``` - * 面试题:**Get 和POST比较** + * **Get 和 POST 比较** 作用:GET 用于获取资源,而 POST 用于传输实体主体 - 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 + 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中(GET 也有请求体,POST 也可以通过 URL 传输参数)。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 - 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET方法是安全的,而POST不是,因为 POST 的目的是传送实体主体内容 + 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET 方法是安全的,而 POST 不是,因为 POST 的目的是传送实体主体内容 * 安全的方法除了 GET 之外还有:HEAD、OPTIONS * 不安全的方法除了 POST 之外还有 PUT、DELETE From 3bdd11667b7b40fb65d3f531843fa1c2a575b46b Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 21 Jan 2022 00:03:53 +0800 Subject: [PATCH 15/78] Update Java Notes --- DB.md | 44 +++- Frame.md | 615 ++++++++++++++++++++++++++++++++++++++----------------- Java.md | 91 ++++---- 3 files changed, 508 insertions(+), 242 deletions(-) diff --git a/DB.md b/DB.md index 41db319..61b9c13 100644 --- a/DB.md +++ b/DB.md @@ -5702,19 +5702,18 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ### 隔离级别 +#### 四种级别 + 事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 隔离级别分类: | 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | | ---------------- | -------- | ---------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 可串行化 | 无 | | - -* 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 -* 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 +| Read Uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| Read Committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| Repeatable Read | 可重复读 | 幻读 | MySQL | +| Serializable | 可串行化 | 无 | | 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 @@ -5745,6 +5744,31 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +*** + + + +#### 加锁分析 + +InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 + +* Read Uncommitted 级别,任何操作都不会加锁 + +* Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 + + MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR + +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,加了写锁后其他事务就无法修改数据,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 + +* Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 + + * 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 + * 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 + + + +参考文章:https://tech.meituan.com/2014/08/20/innodb-lock.html + *** @@ -6636,7 +6660,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁 +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会加 MDL 读锁),通过 MVCC 防止冲突 锁的兼容性: @@ -6654,7 +6678,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -**** +*** @@ -6999,6 +7023,8 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 ### 乐观锁 +悲观锁:在整个数据处理过程中,将数据处于锁定状态,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据,修改删除数据时也加锁,其它事务同样无法读取这些数据 + 悲观锁和乐观锁使用前提: - 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 diff --git a/Frame.md b/Frame.md index 15ea0fc..c126bb8 100644 --- a/Frame.md +++ b/Frame.md @@ -3818,7 +3818,7 @@ public class Consumer { - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 - 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 -在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当**发送和消费参与的 queue 只有一个**,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4346,7 +4346,7 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T -##### OP消息 +##### OP 消息 一阶段写入不可见的消息后,二阶段操作: @@ -4579,6 +4579,106 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 + + +### 存储机制 + +#### 存储结构 + +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + +**** + + + +#### 存储优化 + +##### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 + +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +##### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +#### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +**** + + + + + ### 集群设计 #### 集群模式 @@ -4738,8 +4838,6 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 #### 原理解析 -==todo:暂时 copy 官方文档,学习源码后更新,真想搞懂过程还需要研究一下源码== - 在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 Consumer 端实现负载均衡的核心类 **RebalanceImpl** @@ -4770,13 +4868,15 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** + + **** -### 消息机制 +### 消息查询 -#### 消息查询 +#### 查询方式 RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 @@ -4794,6 +4894,35 @@ RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询 +#### 索引机制 + +RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) + +IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 + +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** + +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte + +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 + + + +参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +*** + + + +### 消息重试 + #### 消息重投 生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 @@ -4822,7 +4951,7 @@ Consumer 消费消息失败后,提供了一种重试机制,令消息再消 - 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10秒 后再重试 - 由于依赖的下游应用服务不可用,例如 DB 连接不可用,外系统网络不可达等。这种情况即使跳过当前失败的消息,消费其他消息同样也会报错,这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力 -RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 +RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是**针对消费组**,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 * 顺序消息的重试,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时应用会出现消息消费被阻塞的情况。所以在使用顺序消息时,必须保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生 @@ -5714,100 +5843,6 @@ RouteInfoManager#registerBroker:注册 Broker 的信息 ### 存储端 -#### 存储机制 - -##### 存储结构 - -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 - -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - -* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M -* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** - -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 - - - -**** - - - -##### 存储优化 - -###### 内存映射 - -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - -* read:读取本地文件内容 - -* write:将读取的内容通过网络发送出去 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 - -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 - -RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 - -MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - - - -*** - - - -###### 页缓存 - -页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** - -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) - -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 - - - -*** - - - -##### 刷盘机制 - -两种持久化的方案: - -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 - -RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 - -* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 - -* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 - -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - -*** - - - #### MappedFile ##### 成员属性 @@ -6346,31 +6381,6 @@ ConsumeQueue 启动阶段方法: #### IndexFile -##### 索引机制 - -RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - -IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - -整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** - -索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - -* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 -* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - - - -参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - -*** - - - ##### 成员属性 IndexFile 类成员属性 @@ -7355,6 +7365,223 @@ TopicPublishInfo 类用来存储路由信息 ### 消费者 +#### 消费者类 + +##### 默认消费 + +DefaultMQPushConsumer 类是默认的消费者类 + +成员变量: + +* 消费者实现类: + + ```java + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + ``` + +* 消费属性: + + ```java + private String consumerGroup; // 消费者组 + private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + ``` + +* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag + + ```java + private Map subscription = new HashMap() + ``` + +* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly + + ```java + private MessageListener messageListener; + ``` + +* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 + + ```java + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + ``` + +* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 + + ```java + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + ``` + +* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 + + ```java + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +* 消费进度存储器: + + ```java + private OffsetStore offsetStore; + ``` + +核心方法: + +* start():启动消费者 + + ```java + public void start() + ``` + +* shutdown():关闭消费者 + + ```java + public void shutdown() + ``` + +* registerMessageListener():注册消息监听器 + + ```java + public void registerMessageListener(MessageListener messageListener) + ``` + +* subscribe():添加订阅信息 + + ```java + public void subscribe(String topic, String subExpression) + ``` + +* unsubscribe():删除订阅指定主题的信息 + + ```java + public void unsubscribe(String topic) + ``` + +* suspend():停止消费 + + ```java + public void suspend() + ``` + +* resume():恢复消费 + + ```java + public void resume() + ``` + + + +*** + + + +##### 默认实现 + +DefaultMQPushConsumerImpl 是默认消费者的实现类 + +成员变量: + +* 客户端实例:整个进程内只有一个客户端实例对象 + + ```java + private MQClientInstance mQClientFactory; + ``` + +* 消费者实例:门面对象 + + ```java + private final DefaultMQPushConsumer defaultMQPushConsumer; + ``` + +* **负载均衡**:分配订阅主题的队列给当前消费者,20秒钟一个周期执行 Rebalance 算法(客户端实例触发) + + ```java + private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this); + ``` + +* 消费者信息: + + ```java + private final long consumerStartTimestamp; // 消费者启动时间 + private volatile ServiceState serviceState; // 消费者状态 + private volatile boolean pause = false; // 是否暂停 + private boolean consumeOrderly = false; // 是否顺序消费 + ``` + +* **拉取消息**:封装拉消息的 API,服务器 Broker 返回结果中包含下次 Pull 时推荐的 BrokerId,根据本次请求数据的冷热程度进行推荐 + + ```java + private PullAPIWrapper pullAPIWrapper; + ``` + +* **消息消费**服务:并发消费和顺序消费 + + ```java + private ConsumeMessageService consumeMessageService; + ``` + +* 流控: + + ```java + private long queueFlowControlTimes = 0; // 队列流控次数,默认每1000次流控,进行一次日志打印 + private long queueMaxSpanFlowControlTimes = 0; // 流控使用,控制打印日志 + ``` + +* HOOK:钩子方法 + + ```java + // 过滤消息 hook + private final ArrayList filterMessageHookList; + // 消息执行hook,在消息处理前和处理后分别执行 hook.before hook.after 系列方法 + private final ArrayList consumeMessageHookList; + ``` + +核心方法: + +* start():加锁保证线程安全 + + ```java + public synchronized void start() + ``` + + * `this.checkConfig()`:检查配置,包括组名、消费模式、订阅信息、消息监听器等 + * `this.copySubscription()`:拷贝订阅信息到 RebalanceImpl 对象 + * `this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData)`:将订阅信息加入 rbl 的 map 中 + * `this.messageListenerInner = ...getMessageListener()`:将消息监听器保存到实例对象 + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,广播模式下直接返回 + * `final String retryTopic`:当前**消费者组重试的主题名**,规则 `%RETRY%ConsumerGroup` + * `SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData()`:创建重试主题的订阅数据对象 + * `this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData)`:将创建的重试主题加入到 rbl 对象的 map 中,消息重试时会再次加入到该主题,消费者订阅这个主题之后,就有机会再次拿到该消息进行消费处理 + * `this.mQClientFactory = ...getOrCreateMQClientInstance()`:获取客户端实例对象 + * `this.rebalanceImpl.`:初始化负载均衡对象,设置**队列分配策略对象**到属性中 + * `this.pullAPIWrapper = new PullAPIWrapper()`:创建拉消息 API 对象,内部封装了查询推荐主机算法 + * `this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList)`:将 过滤 Hook 列表注册到该对象内,消息拉取下来之后会执行该 Hook,再**进行一次自定义的过滤** + * `this.offsetStore = new RemoteBrokerOffsetStore()`:默认集群模式下创建消息进度存储器 + * `this.consumeMessageService = ...`:根据消息监听器的类型创建消费服务 + * `this.consumeMessageService.start()`:启动消费服务 + * `boolean registerOK = mQClientFactory.registerConsumer()`:**将消费者注册到客户端实例中**,客户端提供的服务: + * 心跳服务:把订阅数据同步到订阅主题的 Broker + * 拉消息服务:内部 PullMessageService 启动线程,基于 PullRequestQueue 工作,消费者负载均衡分配到队列后会向该队列提交 PullRequest + * 队列负载服务:每 20 秒调用一次 `consumer.doRebalance()` 接口 + * 消息进度持久化 + * 动态调整消费者、消费服务线程池 + * `mQClientFactory.start()`:启动客户端实例 + * ` this.updateTopic`:从 nameserver 获取主题路由数据,生成主题集合放入 rbl 对象的 table + * `this.mQClientFactory.checkClientInBroker()`:检查服务器是否支持消息过滤模式,一般使用 tag 过滤,服务器默认支持 + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:向所有已知的 Broker 节点,发送心跳数据 + * `this.mQClientFactory.rebalanceImmediately()`:唤醒 rbl 线程,触发负载均衡执行 + + + + + + + + + + + + + + + @@ -7367,6 +7594,77 @@ TopicPublishInfo 类用来存储路由信息 ### 客户端 +#### 公共配置 + +公共的配置信息类 + +* ClientConfig 类 + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +* NettyClientConfig + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + + + +*** + + + #### 实例对象 ##### 成员属性 @@ -7442,7 +7740,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 ```java private final PullMessageService pullMessageService; // 拉消息服务 private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` * 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 @@ -7457,38 +7755,6 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) ``` -* 公共配置类: - - ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } - ``` - 构造方法: * MQClientInstance 有参构造: @@ -7550,7 +7816,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 * `this.startScheduledTask()`:启动定时任务 * `this.pullMessageService.start()`:启动拉取消息服务 * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 * startScheduledTask():**启动定时任务**,调度线程池是单线程 @@ -7672,35 +7938,6 @@ NettyRemotingClient 类负责客户端的网络通信 private final ChannelEventListener channelEventListener; ``` -* Netty 配置对象: - - ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; - } - ``` - 构造方法 * 无参构造: diff --git a/Java.md b/Java.md index 125e703..3004ef2 100644 --- a/Java.md +++ b/Java.md @@ -630,8 +630,9 @@ public class Test1 { 可变参数在方法内部本质上就是一个数组。 可变参数的注意事项: - 1.一个形参列表中可变参数只能有一个! - 2.可变参数必须放在形参列表的**最后面**! + +* 一个形参列表中可变参数只能有一个 +* 可变参数必须放在形参列表的**最后面** ```java public static void main(String[] args) { @@ -929,7 +930,7 @@ Java 的参数是以**值传递**的形式传入方法中 public class EnumDemo { public static void main(String[] args){ // 获取索引 - Season s = Season.SPRING;/ + Season s = Season.SPRING; System.out.println(s); //SPRING System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 s.s.doSomething(); @@ -3291,7 +3292,7 @@ BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少 正则表达式的作用:是一些特殊字符组成的校验规则,可以校验信息的正确性,校验邮箱、电话号码、金额等。 -比如检验qq号: +比如检验 qq 号: ```java public static boolean checkQQRegex(String qq){ @@ -3333,7 +3334,7 @@ java.util.regex 包主要包括以下三个类: ##### 特殊字符 -\r\n 是Windows中的文本行结束标签,在Unix/Linux则是 \n +\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | @@ -3342,8 +3343,8 @@ java.util.regex 包主要包括以下三个类: | \n | 换行符 | | \r | 回车符 | | \t | 制表符 | -| \\ | 代表\本身 | -| () | 使用( )定义一个子表达式。子表达式的内容可以当成一个独立元素 | +| \\ | 代表 \ 本身 | +| () | 使用 () 定义一个子表达式。子表达式的内容可以当成一个独立元素 | @@ -3353,12 +3354,11 @@ java.util.regex 包主要包括以下三个类: ##### 标准字符 -标准字符集合 -能够与”多种字符“匹配的表达式。注意区分大小写,大写是相反的意思。只能校验**"单"**个字符。 +能够与多种字符匹配的表达式,注意区分大小写,大写是相反的意思,只能校验**单**个字符。 | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | -| . | 匹配任意一个字符(除了换行符),如果要匹配包括“\n”在内的所有字符,一般用[\s\S] | +| . | 匹配任意一个字符(除了换行符),如果要匹配包括 \n 在内的所有字符,一般用 [\s\S] | | \d | 数字字符,0~9 中的任意一个,等价于 [0-9] | | \D | 非数字字符,等价于 [ ^0-9] | | \w | 大小写字母或数字或下划线,等价于[a-zA-Z_0-9_] | @@ -3376,7 +3376,7 @@ java.util.regex 包主要包括以下三个类: ##### 自定义符 -自定义符号集合,[ ]方括号匹配方式,能够匹配方括号中**任意一个**字符 +自定义符号集合,[ ] 方括号匹配方式,能够匹配方括号中**任意一个**字符 | 元字符 | 说明 | | ------------ | ----------------------------------------- | @@ -3388,10 +3388,10 @@ java.util.regex 包主要包括以下三个类: | [a-z&&[m-p]] | 匹配 a 到 z 并且 m 到 p:[a-dm-p](交集) | | [^] | 取反 | -* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了^,-之外,需要在前面加 \ +* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了 ^,- 之外,需要在前面加 \ * 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。 - 比如:[\d. \ -+]将匹配:数字、小数点、+、- + 比如:[\d. \ -+] 将匹配:数字、小数点、+、- @@ -3403,17 +3403,17 @@ java.util.regex 包主要包括以下三个类: 修饰匹配次数的特殊符号。 -* 匹配次数中的贪婪模式(匹配字符越多越好,默认!),\* 和 + 都是贪婪型元字符。 -* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 "?" 号) +* 匹配次数中的贪婪模式(匹配字符越多越好,默认 !),\* 和 + 都是贪婪型元字符。 +* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 ? 号) -| 元字符 | 说明 | -| ------ | -------------------------------- | -| X? | X一次或一次也没,有相当于 {0,1} | -| X* | X不出现或出现任意次,相当于 {0,} | -| X+ | X至少一次,相当于 {1,} | -| X{n} | X恰好 n 次 | -| {n,} | X至少 n 次 | -| {n,m} | X至少 n 次,但是不超过 m 次 | +| 元字符 | 说明 | +| ------ | --------------------------------- | +| X? | X 一次或一次也没,有相当于 {0,1} | +| X* | X 不出现或出现任意次,相当于 {0,} | +| X+ | X 至少一次,相当于 {1,} | +| X{n} | X 恰好 n 次 | +| {n,} | X 至少 n 次 | +| {n,m} | X 至少 n 次,但是不超过 m 次 | @@ -3443,16 +3443,16 @@ java.util.regex 包主要包括以下三个类: 捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 -在表达式`((A)(B(C)))`,有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右依次为 group(1)...) +在表达式 `((A)(B(C)))`,有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右依次为 group(1)...) * 调用 matcher 对象的 groupCount 方法返回一个 int 值,表示 matcher 对象当前有多个捕获组。 -* 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 +* 特殊的组 group(0)、group(),代表整个表达式,该组不包括在 groupCount 的返回值中。 | 表达式 | 说明 | | ------------------------- | ------------------------------------------------------------ | | \| (分支结构) | 左右两边表达式之间 "或" 关系,匹配左边或者右边 | -| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从1开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | -| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存( )中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | +| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | +| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存 () 中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | @@ -3470,17 +3470,17 @@ java.util.regex 包主要包括以下三个类: * **把匹配到的字符重复一遍在进行匹配** -* 应用1: +* 应用 1: ```java String regex = "((\d)3)\1[0-9](\w)\2{2}"; ``` - * 首先匹配((\d)3),其次\1匹配((\d)3)已经匹配到的内容,\2匹配(\d), {2}指的是\2的值出现两次 - * 实例:23238n22(匹配到2未来就继续匹配2) + * 首先匹配 ((\d)3),其次 \1 匹配 ((\d)3) 已经匹配到的内容,\2 匹配 (\d), {2} 指的是 \2 的值出现两次 + * 实例:23238n22(匹配到 2 未来就继续匹配 2) * 实例:43438n44 -* 应用2:爬虫 +* 应用 2:爬虫 ```java String regex = "<(h[1-6])>\w*?<\/\1>"; @@ -3506,7 +3506,7 @@ java.util.regex 包主要包括以下三个类: * 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度 -* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符。**是对位置的匹配**。 +* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符,**是对位置的匹配** * 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是**零宽度**的。占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果中而言的 @@ -3555,7 +3555,7 @@ Pattern 类: Matcher 类: * `boolean find()`:扫描输入的序列,查找与该模式匹配的下一个子序列 -* `String group()`:返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 +* `String group()`:返回与上一个匹配的输入子序列,同 group(0),匹配整个表达式的子字符串 * `String group(int group)`:返回在上一次匹配操作期间由给定组捕获的输入子序列 * `int groupCount()`:返回此匹配器模式中捕获组的数量 @@ -3604,8 +3604,8 @@ public class Demo02 { } ``` -* 正则表达式改为`"(([a-z]+)(?:[0-9]+))"` 没有group(3) 因为是非捕获组 -* 正则表达式改为`"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 +* 正则表达式改为 `"(([a-z]+)(?:[0-9]+))"` 没有 group(3) 因为是非捕获组 +* 正则表达式改为 `"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 @@ -3695,9 +3695,9 @@ public static void main(String[] args) { -##### 面试问题 +##### 搜索号码 -找出所有189和132开头的手机号 +找出所有 189 和 132 开头的手机号 ```java public class RegexDemo { @@ -5624,15 +5624,15 @@ class LRUCache extends LinkedHashMap { #### TreeMap -TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 - -TreeSet 集合的底层是基于TreeMap,只是键的附属值为空对象而已 +TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点,如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 TreeMap 集合指定大小规则有 2 种方式: -* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法(拓展方式) +* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法 * 直接为集合设置比较器 Comparator 对象,重写比较方法 +说明:TreeSet 集合的底层是基于 TreeMap,只是键的附属值为空对象而已 + 成员属性: * Entry 节点 @@ -5799,8 +5799,7 @@ public class MapDemo{ } ``` -优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常 - 体现的是 Java 的严谨性和规范性,数据类型,经常需要进行统一 +优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 @@ -5812,7 +5811,7 @@ public class MapDemo{ ##### 泛型类 -泛型类:使用了泛型定义的类就是泛型类。 +泛型类:使用了泛型定义的类就是泛型类 泛型类格式: @@ -11642,6 +11641,10 @@ private int hash32; +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c + + + *** From 916bc11bd67e092736ee58eb025258c52e45b556 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 24 Jan 2022 00:23:57 +0800 Subject: [PATCH 16/78] Update Java Notes --- Frame.md | 1475 +++++++++++++++++++++++++++++++++++++++--------------- Java.md | 6 +- 2 files changed, 1086 insertions(+), 395 deletions(-) diff --git a/Frame.md b/Frame.md index c126bb8..bc07592 100644 --- a/Frame.md +++ b/Frame.md @@ -4594,10 +4594,10 @@ RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,Com ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) * CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,**保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset**,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M * IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)针对 Producer 和 Consumer 分别采用了**数据和索引部分相分离**的存储结构,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 @@ -4607,9 +4607,7 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 -#### 存储优化 - -##### 内存映射 +#### 内存映射 操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: @@ -4633,7 +4631,7 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 -##### 页缓存 +#### 页面缓存 页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** @@ -4812,23 +4810,23 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息,提交到消息消费线程池后,又继续向服务器再次尝试拉取消息,如果未拉取到消息,则延迟一下又继续拉取 -在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 ConsumerGroup 中的哪些 Consumer 消费 +在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 Consumer Group 中的哪些 Consumer 消费 -* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 +* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的 queue。 * 在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue 集群模式下,每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡1.png) +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) 还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle,以环状轮流均分 queue 的形式: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡2.png) +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) 集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 -通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要**控制让 queue 的总数量大于等于 Consumer 的数量** @@ -4850,8 +4848,6 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** * 先对 Topic 下的消息消费队列、消费者 ID 排序,然后用消息队列分配策略算法(默认是消息队列的平均分配算法),计算出待拉取的消息队列。平均分配算法类似于分页的算法,将所有 MessageQueue 排好序类似于记录,将所有消费端 Consumer 排好序类似页数,并求出每一页需要包含的平均 size 和每个页面记录的范围 range,最后遍历整个 range 而计算出当前 Consumer 端应该分配到的记录(这里即为 MessageQueue) - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡平均分配算法.png) - * 调用 updateProcessQueueTableInRebalance() 方法,先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡重新平衡算法.png) @@ -4864,7 +4860,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 -消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列 +消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** @@ -5231,6 +5227,10 @@ NamesrvStartup#start:启动 Namesrv 控制器 +源码解析参考视频:https://space.bilibili.com/457326371 + + + **** @@ -5471,7 +5471,7 @@ NettyRemotingServer 类成员变量: 核心方法的解析: -* start():启动方法 +* start():启动方法,**创建 BootStrap,并添加 NettyServerHandler 处理器** ```java public void start() { @@ -5492,7 +5492,7 @@ NettyRemotingServer 类成员变量: .childOption(ChannelOption.TCP_NODELAY, true) // 设置服务器端口 .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) - // 向 channel pipeline 添加了很多 handler,包括 NettyServerHandler + // 向 channel pipeline 添加了很多 handler,【包括 NettyServerHandler】 .childHandler(new ChannelInitializer() {}); // 客户端开启 内存池,使用的内存池是 PooledByteBufAllocator.DEFAULT @@ -5639,7 +5639,7 @@ NettyRemotingServer 类成员变量: ##### 处理方法 -NettyServerHandler 类用来处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** +NettyServerHandler 类用来处理 Channel 上的事件,在 NettyRemotingServer 启动时注册到 Netty 中,可以处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** ```java class NettyServerHandler extends SimpleChannelInboundHandler { @@ -5667,7 +5667,7 @@ public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand ms } ``` -NettyRemotingAbstract#processRequestCommand:处理请求的数据 +NettyRemotingAbstract#processRequestCommand:**处理请求的数据** * `matched = this.processorTable.get(cmd.getCode())`:根据业务请求码获取 Pair 对象,包含**处理器和线程池资源** @@ -5701,7 +5701,7 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `pair.getObject2().submit(requestTask)`:获取处理器对应的线程池,将 task 提交,**从 IO 线程切换到业务线程** -NettyRemotingAbstract#processResponseCommand:处理响应的数据 +NettyRemotingAbstract#processResponseCommand:**处理响应的数据** * `int opaque = cmd.getOpaque()`:获取请求 ID * `responseFuture = responseTable.get(opaque)`:**从响应映射表中获取对应的对象** @@ -6828,7 +6828,7 @@ FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 ##### 清理服务 -CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,先清理 CL,再清理 CQ,因为 CQ 依赖于 CL 的数据 +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 * run():运行方法 @@ -6877,6 +6877,105 @@ CleanConsumeQueueService 清理过期的 CQ 数据 +##### 获取消息 + +PullMessageProcessor#processRequest 方法中调用 getMessage 用于获取消息(提示:建议学习消费者源码时再阅读) + +```java +// offset: 客户端拉消息使用位点; maxMsgNums: 32; messageFilter: 一般这里是 tagCode 过滤 +public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final MessageFilter messageFilter) +``` + +* `if (this.shutdown)`:检查运行状态 + +* `GetMessageResult getResult`:创建查询结果对象 + +* `final long maxOffsetPy = this.commitLog.getMaxOffset()`:**获取 CommitLog 最大物理偏移量** + +* `ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId)`:根据主题和队列 ID 获取 ConsumeQueue对象 + +* `minOffset, maxOffset`:获取当前 ConsumeQueue 的最小 offset 和 最大 offset,**判断是否满足本次 Pull 的 offset** + + `if (maxOffset == 0)`:说明队列内无数据,设置状态为 NO_MESSAGE_IN_QUEUE,外层进行长轮询 + + `else if (offset < minOffset)`:说明 offset 太小了,设置状态为 OFFSET_TOO_SMALL + + `else if (offset == maxOffset)`:消费进度持平,设置状态为 OFFSET_OVERFLOW_ONE,外层进行长轮询 + + `else if (offset > maxOffset)`:说明 offset 越界了,设置状态为 OFFSET_OVERFLOW_BADLY + +* `SelectMappedBufferResult bufferConsumeQueue`:查询 CQData **获取包含该 offset 的 MappedFile 文件**,如果该文件不是顺序写的文件,就读取 `[offset%maxSize, 文件尾]` 范围的数据,反之读取 `[offset%maxSize, 文件名+wrotePosition尾]` + + 先查 CQ 的原因:因为 CQ 时 CL 的索引,通过 CQ 查询 CL 更加快捷 + +* `if (bufferConsumeQueue != null)`:只有再 CQ 删除过期数据的逻辑执行时,条件才不成立,一般都是成立的 + +* `long nextPhyFileStartOffset = Long.MIN_VALUE`:下一个 commitLog 物理文件名,初始值为最小值 + +* `long maxPhyOffsetPulling = 0`:本次拉消息最后一条消息的物理偏移量 + +* `for ()`:**处理数据**,每次处理 20 字节处理字节数大于 16000 时跳出循环 + +* `offsetPy, sizePy, tagsCode`:读取 20 个字节后,获取消息物理偏移量、消息大小、消息 tagCode + +* `boolean isInDisk = checkInDiskByCommitOffset(...)`:**检查消息是热数据还是冷数据**,false 为热数据 + + * `long memory`:Broker 系统 40% 内存的字节数,写数据时内存不够会使用 LRU 算法淘汰数据,将淘汰数据持久化到磁盘 + * `return (maxOffsetPy - offsetPy) > memory`:返回 true 说明数据已经持久化到磁盘,为冷数据 + +* `if (this.isTheBatchFull())`:**控制是否跳出循环** + + * `if (0 == bufferTotal || 0 == messageTotal)`:本次 pull 消息未拉取到任何东西,需要外层 for 循环继续,返回 false + + * `if (maxMsgNums <= messageTotal)`:结果对象内消息数已经超过了最大消息数量,可以结束循环了 + + * `if (isInDisk)`:冷数据 + + `if ((bufferTotal + sizePy) > ...)`:冷数据一次 pull 请求最大允许获取 64kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取8 条消息 + + * `else`:热数据 + + `if ((bufferTotal + sizePy) > ...)`:热数据一次 pull 请求最大允许获取 256kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取32 条消息 + +* `if (messageFilter != null)`:按照消息 tagCode 进行过滤 + +* `selectResult = this.commitLog.getMessage(offsetPy, sizePy)`:根据 CQ 消息物理偏移量和消息大小**到 commitLog 中查询这条 msg** + +* `if (null == selectResult)`:条件成立说明 commitLog 执行了删除过期文件的定时任务,因为是先清理的 CL,所以 CQ 还有该索引数据 + +* `nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy)`:获取包含该 offsetPy 的下一个数据文件的文件名 + +* `getResult.addMessage(selectResult)`:**将本次循环查询出来的 msg 加入到 getResult 内** + +* `status = GetMessageStatus.FOUND`:查询状态设置为 FOUND + +* `nextPhyFileStartOffset = Long.MIN_VALUE`:设置为最小值,跳过期 CQData 数据的逻辑 + +* `nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE)`:计算客户端下一次 pull 时使用的位点信息 + +* `getResult.setSuggestPullingFromSlave(diff > memory)`:**选择主从节点的建议** + + * `diff > memory => true`:表示本轮查询最后一条消息为冷数据,Broker 建议客户端下一次 pull 时到 slave 节点 + * `diff > memory => false`:表示本轮查询最后一条消息为热数据,Broker 建议客户端下一次 pull 时到 master 节点 + +* `getResult.setStatus(status)`:设置结果状态 + +* `getResult.setNextBeginOffset(nextBeginOffset)`:设置客户端下一次 pull 时的 offset + +* `getResult.setMaxOffset(maxOffset)`:设置 queue 的最大 offset 和最小 offset + +* `return getResult`:返回结果对象 + + + +*** + + + #### Broker BrokerStartup 启动方法 @@ -6914,6 +7013,8 @@ BrokerController#start:核心启动方法 #### 生产者类 +##### 生产者类 + DefaultMQProducer 是生产者的默认实现类 成员变量: @@ -7017,9 +7118,7 @@ DefaultMQProducer 是生产者的默认实现类 -#### 默认实现 - -##### 成员属性 +##### 实现者类 DefaultMQProducerImpl 类是默认的生产者实现类 @@ -7111,7 +7210,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 -##### 成员方法 +##### 实现方法 * start():启动方法,参数默认是 true,代表正常的启动路径 @@ -7337,7 +7436,7 @@ TopicPublishInfo 类用来存储路由信息 } else { // 遍历消息队列 for (int i = 0; i < this.messageQueueList.size(); i++) { - // 获取队列的索引 + // 【获取队列的索引,+1】 int index = this.sendWhichQueue.getAndIncrement(); // 获取队列的下标位置 int pos = Math.abs(index) % this.messageQueueList.size(); @@ -7355,115 +7454,219 @@ TopicPublishInfo 类用来存储路由信息 } ``` - - -**** +*** -### 消费者 -#### 消费者类 +#### 公共配置 -##### 默认消费 +公共的配置信息类 -DefaultMQPushConsumer 类是默认的消费者类 +* ClientConfig 类 -成员变量: + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` -* 消费者实现类: +* NettyClientConfig ```java - protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } ``` -* 消费属性: + + +*** + + + +#### 客户端类 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: ```java - private String consumerGroup; // 消费者组 - private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 ``` -* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag +* 生产者消费者的映射表:key 是组名 ```java - private Map subscription = new HashMap() + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable ``` -* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly +* 网络层配置: ```java - private MessageListener messageListener; + private final NettyClientConfig nettyClientConfig; ``` -* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) ```java - private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + private final MQClientAPIImpl mQClientAPIImpl; ``` -* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 +* 本地路由数据:key 是主题名称,value 路由信息 ```java - private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); ``` -* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 +* 锁信息:两把锁,锁不同的数据 ```java - private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); ``` -* 消费进度存储器: +* 调度线程池:单线程,执行定时任务 ```java - private OffsetStore offsetStore; + private final ScheduledExecutorService scheduledExecutorService; ``` -核心方法: - -* start():启动消费者 +* Broker 映射表:key 是 BrokerName ```java - public void start() + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; ``` -* shutdown():关闭消费者 +* **客户端的协议处理器**:用于处理 IO 事件 ```java - public void shutdown() + private final ClientRemotingProcessor clientRemotingProcessor; ``` -* registerMessageListener():注册消息监听器 +* 消息服务: ```java - public void registerMessageListener(MessageListener messageListener) + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` -* subscribe():添加订阅信息 +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 ```java - public void subscribe(String topic, String subExpression) + private final DefaultMQProducer defaultMQProducer; ``` -* unsubscribe():删除订阅指定主题的信息 +* 心跳次数统计: ```java - public void unsubscribe(String topic) + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) ``` -* suspend():停止消费 +构造方法: + +* MQClientInstance 有参构造: ```java - public void suspend() + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } ``` -* resume():恢复消费 +* MQClientAPIImpl 有参构造: ```java - public void resume() + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } ``` @@ -7472,15 +7675,371 @@ DefaultMQPushConsumer 类是默认的消费者类 -##### 默认实现 - -DefaultMQPushConsumerImpl 是默认消费者的实现类 - -成员变量: +##### 成员方法 -* 客户端实例:整个进程内只有一个客户端实例对象 +* start():启动方法 - ```java + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + + + + +**** + + + +### 消费者 + +#### 消费者类 + +##### 默认消费 + +DefaultMQPushConsumer 类是默认的消费者类 + +成员变量: + +* 消费者实现类: + + ```java + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + ``` + +* 消费属性: + + ```java + private String consumerGroup; // 消费者组 + private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + ``` + +* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag + + ```java + private Map subscription = new HashMap() + ``` + +* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly + + ```java + private MessageListener messageListener; + ``` + +* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 + + ```java + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + ``` + +* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 + + ```java + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + ``` + +* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 + + ```java + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +* 消费进度存储器: + + ```java + private OffsetStore offsetStore; + ``` + +核心方法: + +* start():启动消费者 + + ```java + public void start() + ``` + +* shutdown():关闭消费者 + + ```java + public void shutdown() + ``` + +* registerMessageListener():注册消息监听器 + + ```java + public void registerMessageListener(MessageListener messageListener) + ``` + +* subscribe():添加订阅信息 + + ```java + public void subscribe(String topic, String subExpression) + ``` + +* unsubscribe():删除订阅指定主题的信息 + + ```java + public void unsubscribe(String topic) + ``` + +* suspend():停止消费 + + ```java + public void suspend() + ``` + +* resume():恢复消费 + + ```java + public void resume() + ``` + + + +*** + + + +##### 默认实现 + +DefaultMQPushConsumerImpl 是默认消费者的实现类 + +成员变量: + +* 客户端实例:整个进程内只有一个客户端实例对象 + + ```java private MQClientInstance mQClientFactory; ``` @@ -7570,488 +8129,618 @@ DefaultMQPushConsumerImpl 是默认消费者的实现类 +*** + + + +#### 负载均衡 + +##### 实现方式 + +MQClientInstance#start 中会启动负载均衡服务: + +```java +public void run() { + // 检查停止标记 + while (!this.isStopped()) { + // 休眠 20 秒,防止其他线程饥饿,所以【每 20 秒负载均衡一次】 + this.waitForRunning(waitInterval); + // 调用客户端实例的负载均衡方法,底层【会遍历所有消费者,调用消费者的负载均衡】 + this.mqClientFactory.doRebalance(); + } +} +``` + +RebalanceImpl 类成员变量: + +* 分配给当前消费者的处理队列:处理消息队列集合,ProcessQueue 是 MQ 队列在消费者端的快照 + ```java + protected final ConcurrentMap processQueueTable; + ``` +* 消费者订阅主题的队列信息: + ```java + protected final ConcurrentMap> topicSubscribeInfoTable; + ``` +* 订阅数据: + ```java + protected final ConcurrentMap subscriptionInner; + ``` +* 队列分配策略: + ```java + protected AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` +成员方法: +* doRebalance():负载均衡方法 + ```java + public void doRebalance(final boolean isOrder) { + // 获取当前消费者的订阅数据 + Map subTable = this.getSubscriptionInner(); + if (subTable != null) { + // 遍历所有的订阅主题 + for (final Entry entry : subTable.entrySet()) { + // 获取订阅的主题 + final String topic = entry.getKey(); + // 按照主题进行负载均衡 + this.rebalanceByTopic(topic, isOrder); + } + } + // 将分配到当前消费者的队列进行过滤,不属于当前消费者订阅主题的直接移除 + this.truncateMessageQueueNotMyTopic(); + } + ``` + * `Set mqSet = this.topicSubscribeInfoTable.get(topic)`:获取当前主题的全部队列信息 + * `cidAll = this...findConsumerIdList(topic, consumerGroup)`:从服务器获取消费者组下的全部消费者 ID + * `Collections.sort(mqAll)`:主题 MQ 队列和消费者 ID 都进行排序,保证每个消费者的视图一致性 + * `strategy = this.allocateMessageQueueStrategy`:获取队列分配策略对象 + * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:负载均衡,更新队列处理集合 + * `boolean changed = false`:当前消费者的消费队列是否有变化 -*** + * `while (it.hasNext())`:遍历当前消费者的所有处理队列 + * `if (!mqSet.contains(mq))`:该 MQ 经过 rbl 计算之后,**被分配到其它 consumer 节点** + `pq.setDropped(true)`:将删除状态设置为 true -### 客户端 + `if (this.removeUnnecessaryMessageQueue(mq, pq))`:在 MQ 归属的 broker 节点持久化消费进度,并删除该 MQ 在本地的消费进度 -#### 公共配置 + `it.remove()`:从 processQueueTable 移除该 MQ -公共的配置信息类 + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑、 -* ClientConfig 类 + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配**到当前节点的队列 - ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } - ``` + * `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** -* NettyClientConfig + * `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + + * `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + + * `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + + * `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + + * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + + + + +*** + + + +##### 队列分配 + +AllocateMessageQueueStrategy 类是队列的分配策略 + +* 平均分配:AllocateMessageQueueAveragely 类 ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; + // 参数一:消费者组 参数二:当前消费者id + // 参数三:主题的全部队列,包括所有 broker 上该主题的 mq 参数四:全部消费者id集合 + public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { + // 获取当前消费者在全部消费者中的位置,【全部消费者是已经排序好的,排在前面的优先分配更多的队列】 + int index = cidAll.indexOf(currentCID); + // 平均分配完以后,还剩余的待分配的 mq 的数量 + int mod = mqAll.size() % cidAll.size(); + // 首先判断整体的 mq 的数量是否小于消费者的数量,小于消费者的数量就说明不够分的,先分一个 + int averageSize = mqAll.size() <= cidAll.size() ? 1 : + // 成立需要多分配一个队列,因为更靠前 + (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size()); + // 获取起始的分配位置 + int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod; + // 防止索引越界 + int range = Math.min(averageSize, mqAll.size() - startIndex); + // 开始分配,【挨着分配,是直接就把当前的 消费者分配完成】 + for (int i = 0; i < range; i++) { + result.add(mqAll.get((startIndex + i) % mqAll.size())); + } + return result; } ``` + 队列排序后:Q1 → Q2 → Q3,消费者排序后 C1 → C2 → C3 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) + +* 轮流分配:AllocateMessageQueueAveragelyByCircle + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) + +* 指定机房平均分配:AllocateMessageQueueByMachineRoom,前提是 Broker 的命名规则为 `机房名@BrokerName` + + + *** -#### 实例对象 +#### 消息拉取 -##### 成员属性 +##### 实现方式 -MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** +MQClientInstance#start 中会启动消息拉取服务:PullMessageService + +```java +public void run() { + // 检查停止标记 + while (!this.isStopped()) { + try { + // 从阻塞队列中获取拉消息请求 + PullRequest pullRequest = this.pullRequestQueue.take(); + // 拉取消息,获取请求对应的使用当前消费者组中的哪个消费者,调用消费者的 pullMessage 方法 + this.pullMessage(pullRequest); + } catch (Exception e) { + log.error("Pull Message Service Run Method exception", e); + } + } +} +``` + +DefaultMQPushConsumerImpl#pullMessage: + +* `ProcessQueue processQueue = pullRequest.getProcessQueue()`:获取请求对应的快照队列,并判断是否是删除状态 + +* `this.executePullRequestLater()`:如果当前消费者不是运行状态,则拉消息任务延迟 3 秒后执行,如果是暂停状态延迟 1 秒 + +* **流控的逻辑**: + + `long cachedMessageCount = processQueue.getMsgCount().get()`:获取消费者本地该 queue 快照内缓存的消息数量,如果大于 1000 条,进行流控,延迟 50 毫秒 + + `long cachedMessageSizeInMiB`: 消费者本地该 queue 快照内缓存的消息容量 size,超过 100m 消息未被消费进行流控 + + `if(processQueue.getMaxSpan() > 2000)`:消费者本地缓存消息第一条消息最后一条消息跨度超过 2000 进行流控 + +* `SubscriptionData subscriptionData`:本次拉消息请求订阅的主题数据,如果调用了 `unsubscribe(主题)` 将会获取为 null + +* `PullCallback pullCallback = new PullCallback()`:**拉消息处理回调对象**, + + * `pullResult = ...processPullResult()`:预处理 PullResult 结果 + + * `case FOUND`:正常拉取到消息 + + `pullRequest.setNextOffset(pullResult.getNextBeginOffset())`:更新 pullRequest 对象下一次拉取消息的位点 + + `if (pullResult.getMsgFoundList() == null...)`:消息过滤导致消息全部被过滤掉,需要立马发起下一次拉消息 + + `boolean .. = processQueue.putMessage()`:将服务器拉取的消息集合**加入到消费者本地**的 processQueue 内 + + `DefaultMQPushConsumerImpl...submitConsumeRequest()`:**提交消费任务**,分为顺序消费和并发消费 + + `Defaul..executePullRequestImmediately(pullRequest)`:将更新过 nextOffset 字段的 PullRequest 对象,再次放到 pullMessageService 的阻塞队列中,**形成闭环** + + * `case NO_NEW_MSG ||NO_MATCHED_MSG`:表示本次 pull 没有新的可消费的信息 + + `pullRequest.setNextOffset()`:更新更新 pullRequest 对象下一次拉取消息的位点 + + `Defaul..executePullRequestImmediately(pullRequest)`:再次拉取请求 + + * `case OFFSET_ILLEGAL`:本次 pull 时使用的 offset 是无效的,即 offset > maxOffset || offset < minOffset + + `pullRequest.setNextOffset()`:调整pullRequest nextOffset 为 正确的 offset + + `pullRequest.getProcessQueue().setDropped(true)`:设置该 processQueue 为删除状态,如果有该 queue 的消费任务,该消费任务会马上停止任务 + + `DefaultMQPushConsumerImpl.this.executeTaskLater()`:提交异步任务,10 秒后去执行 + + * `DefaultMQPushConsumerImpl...updateOffset()`:更新 offsetStore 该 MQ 的 offset 为正确值,内部直接替换 + + * `DefaultMQPushConsumerImpl...persist()`:持久化该 messageQueue 的 offset 到 Broker 端 + + * `DefaultMQPushConsumerImpl...removeProcessQueue()`: 删除该消费者该 messageQueue 对应的 processQueue + + * 这里没有再次提交 pullRequest 到 pullMessageService 的队列,那该队列不再拉消息了吗? + + 负载均衡 rbl 程序会重建该队列的 processQueue,重建完之后会为该队列创建新的 PullRequest 对象 + +* `int sysFlag = PullSysFlag.buildSysFlag()`:构建标志对象,sysFlag 高 4 位未使用,低 4 位使用,从左到右为 0000 0011 + + * 第一位:表示是否提交消费者本地该队列的 offset,一般是 1 + * 第二位:表示是否允许服务器端进行长轮询,一般是 1 + * 第三位:表示是否提交消费者本地该主题的订阅数据,一般是 0 + * 第四位:表示是否为类过滤,一般是 0 + +* `this.pullAPIWrapper.pullKernelImpl()`:拉取消息的核心方法 + + + +*** + + + +##### 封装对象 + +PullAPIWrapper 类封装了拉取消息的 API 成员变量: -* 配置信息: +* 推荐拉消息使用的主机 ID: ```java - private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 - private final String clientId; // 客户端 ID ip@pid - private final long bootTimestamp; // 客户端的启动时间 - private ServiceState serviceState; // 客户端状态 + private ConcurrentMap pullFromWhichNodeTable ``` -* 生产者消费者的映射表:key 是组名 +成员方法: - ```java - private final ConcurrentMap producerTable - private final ConcurrentMap consumerTable - private final ConcurrentMap adminExtTable - ``` +* pullKernelImpl():拉消息 -* 网络层配置: + * `FindBrokerResult findBrokerResult`:查询指定 BrokerName 的地址信息,主节点或者推荐节点 - ```java - private final NettyClientConfig nettyClientConfig; - ``` + * `if (null == findBrokerResult)`:查询不到,就到 Namesrv 获取指定 topic 的路由数据 -* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + * `if (findBrokerResult.isSlave())`:成立说明 findBrokerResult 表示的主机为 slave 节点,**slave 不存储 offset 信息** - ```java - private final MQClientAPIImpl mQClientAPIImpl; - ``` + `sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner)`:将 sysFlag 标记位中 CommitOffset 的位置为 0 -* 本地路由数据:key 是主题名称,value 路由信息 + * `PullMessageRequestHeader requestHeader`:创建请求头对象,封装所有的参数 - ```java - private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + * `PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage()`:调用客户端实例的方法,核心逻辑就是**将业务数据转化为 RemotingCommand 通过 NettyRemotingClient 的 IO 进行通信** -* 锁信息:两把锁,锁不同的数据 + * `RemotingCommand request`:创建网络层传输对象 RemotingCommand 对象,**请求 ID 为 `PULL_MESSAGE = 11`** - ```java - private final Lock lockNamesrv = new ReentrantLock(); - private final Lock lockHeartbeat = new ReentrantLock(); - ``` + * `return this.pullMessageSync(...)`:此处是异步调用,**处理结果放入 ResponseFuture 中**,参考服务端小节的处理器类 `NettyServerHandler#processMessageReceived` 方法 -* 调度线程池:单线程,执行定时任务 + * `RemotingCommand response = responseFuture.getResponseCommand()`:获取服务器端响应数据 response + * `PullResult pullResult`:从 response 内提取出来拉消息结果对象,将响应头 PullMessageResponseHeader 对象中信息**填充到 PullResult 中**,列出两个重要的字段: + * `private Long suggestWhichBrokerId`:服务端建议客户端下次 Pull 时选择的 BrokerID + * `private Long nextBeginOffset`:客户端下次 Pull 时使用的 offset 信息 - ```java - private final ScheduledExecutorService scheduledExecutorService; - ``` + * `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 -* Broker 映射表:key 是 BrokerName +* processPullResult():预处理拉消息结果,**更新推荐 Broker 和过滤消息** - ```java - // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port - private final ConcurrentMap> brokerAddrTable; - // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 - ConcurrentMap> brokerVersionTable; - ``` + * `this.updatePullFromWhichNode()`:更新 pullFromWhichNodeTable 内该 MQ 的下次查询推荐 BrokerID + * `if (PullStatus.FOUND == pullResult.getPullStatus())`:条件成立说明拉取成功 + * `List msgList`:**将获取的消息进行解码** + * `if (!subscriptionData... && !subscriptionData.isClassFilterMode())`:客户端按照 tag 值进行过滤 + * `pullResultExt.setMsgFoundList(msgListFilterAgain)`:将再次过滤后的消息集合,保存到 pullResult + * `pullResultExt.setMessageBinary(null)`:设置为 null,帮助 GC + + + +*** + + + +#### 通信处理 + +##### 处理器 -* **客户端的协议处理器**:用于处理 IO 事件 +BrokerStartup#createBrokerController 方法中创建了 BrokerController 并进行初始化,调用 `registerProcessor()` 方法将处理器 PullMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `PULL_MESSAGE = 11`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest 方法 - ```java - private final ClientRemotingProcessor clientRemotingProcessor; - ``` +```java +// 参数一:服务器与客户端 netty 通道; 参数二:客户端请求; 参数三:是否允许服务器端长轮询,默认 true +private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) +``` -* 消息服务: +* `RemotingCommand response`:创建响应对象,设置为响应类型的请求,响应头是 PullMessageResponseHeader - ```java - private final PullMessageService pullMessageService; // 拉消息服务 - private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 - ``` +* `final PullMessageResponseHeader responseHeader`:获取响应对象的 header -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +* `final PullMessageRequestHeader requestHeader`:解析出请求头 PullMessageRequestHeader - ```java - private final DefaultMQProducer defaultMQProducer; - ``` +* `response.setOpaque(request.getOpaque())`:设置 opaque 属性,客户端需要根据该字段**获取 ResponseFuture** -* 心跳次数统计: +* 进行一些鉴权的逻辑:是否允许长轮询、提交 offset、topicConfig 是否是空、队列 ID 是否合理 - ```java - private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) - ``` +* `ConsumerGroupInfo consumerGroupInfo`:获取消费者组信息,包好全部的消费者和订阅数据 -构造方法: +* `subscriptionData = consumerGroupInfo.findSubscriptionData()`:**获取指定主题的订阅数据** -* MQClientInstance 有参构造: +* `if (!ExpressionType.isTagType()`:表达式匹配 - ```java - public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { - this.clientConfig = clientConfig; - this.instanceIndex = instanceIndex; - // Netty 相关的配置信息 - this.nettyClientConfig = new NettyClientConfig(); - // 平台核心数 - this.nettyClientConfig.setClientCallbackExecutorThreads(...); - this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); - // 【创建客户端协议处理器】 - this.clientRemotingProcessor = new ClientRemotingProcessor(this); - // 创建 API 实现对象 - // 参数一:客户端网络配置 - // 参数二:客户端协议处理器,注册到客户端网络层 - // 参数三:rpcHook,注册到客户端网络层 - // 参数四:客户端配置 - this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); - - //... - // 内部生产者,指定内部生产者的组 - this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); - } - ``` +* `MessageFilter messageFilter`:创建消息过滤器,一般是通过 tagCode 进行过滤 -* MQClientAPIImpl 有参构造: +* `DefaultMessageStore.getMessage()`:**查询消息的核心逻辑**,在 Broker 端查询消息(存储端笔记详解了该源码) - ```java - public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { - this.clientConfig = clientConfig; - topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); - // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event - this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); - // 业务处理器 - this.clientRemotingProcessor = clientRemotingProcessor; - // 注册 RpcHook - this.remotingClient.registerRPCHook(rpcHook); - // ... - // 注册回退消息的请求码 - this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); - } - ``` +* `response.setRemark()`:设置此次响应的状态 +* `responseHeader.set..`:设置响应头对象的一些字段 +* `switch (this.brokerController.getMessageStoreConfig().getBrokerRole())`:如果当前主机节点角色为 slave 并且**从节点读**并未开启的话,直接给客户端 一个状态 `PULL_RETRY_IMMEDIATELY` -*** +* `if (this.brokerController.getBrokerConfig().isSlaveReadEnable())`:消费太慢,下次从另一台机器拉取 +* `switch (getMessageResult.getStatus())`:根据 getMessageResult 的状态设置 response 的 code + ```java + public enum GetMessageStatus { + FOUND, // 查询成功 + NO_MATCHED_MESSAGE, // 未查询到到消息,服务端过滤 tagCode + MESSAGE_WAS_REMOVING, // 查询时赶上 CommitLog 清理过期文件,导致查询失败,立刻尝试 + OFFSET_FOUND_NULL, // 查询时赶上 ConsumerQueue 清理过期文件,导致查询失败,【进行长轮询】 + OFFSET_OVERFLOW_BADLY, // pullRequest.offset 越界 maxOffset + OFFSET_OVERFLOW_ONE, // pullRequest.offset == CQ.maxOffset,【进行长轮询】 + OFFSET_TOO_SMALL, // pullRequest.offset 越界 minOffset + NO_MATCHED_LOGIC_QUEUE, // 没有匹配到逻辑队列 + NO_MESSAGE_IN_QUEUE, // 空队列,创建队列也是因为查询导致,【进行长轮询】 + } + ``` -##### 成员方法 +* `switch (response.getCode())`:根据 response 状态做对应的业务处理 -* start():启动方法 + `case ResponseCode.SUCCESS`:查询成功 - * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 - * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 - * `this.startScheduledTask()`:启动定时任务 - * `this.pullMessageService.start()`:启动拉取消息服务 - * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 + * `final byte[] r = this.readGetMessageResult()`:本次 pull 出来的全部消息导入 byte 数组 + * `response.setBody(r)`:将消息的 byte 数组保存到 response body 字段 -* startScheduledTask():**启动定时任务**,调度线程池是单线程 + `case ResponseCode.PULL_NOT_FOUND`:产生这种情况大部分原因是 `pullRequest.offset == queue.maxOffset`,说明已经没有需要获取的消息,此时如果直接返回给客户端,客户端会立刻重新请求,还是继续返回该状态,频繁拉取服务器导致服务器压力大,所以此处**需要长轮询** - * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + * `if (brokerAllowSuspend && hasSuspendFlag)`:brokerAllowSuspend = true,当长轮询结束再次执行 processRequest 时该参数为 false,所以**每次 Pull 请求至多在服务器端长轮询控制一次** + * `PullRequest pullRequest = new PullRequest()`:创建长轮询 PullRequest 对象 + * `this.brokerController...suspendPullRequest(topic, queueId, pullRequest)`:将长轮询请求对象交给长轮询服务 + * `String key = this.buildKey(topic, queueId)`:构建一个 `topic@queueId` 的 key + * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:从拉请求表中获取对象 + * `mpr.addPullRequest(pullRequest)`:将 PullRequest 对象放入到 ManyPullRequest 的请求集合中 + * `response = null`:响应设置为 null 内部的 callBack 就不会给客户端发送任何数据,**不进行通信**,否则就又开始重新请求 - * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 +* `boolean storeOffsetEnable`:允许长轮询、sysFlag 表示提交消费者本地该队列的offset、当前 broker 节点角色为 master 节点三个条件成立,才**在 Broker 端存储消费者组内该主题的指定 queue 的消费进度** - ```java - // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 - MQClientInstance.this.updateTopicRouteInfoFromNameServer(); - ``` +* `return response`:返回 response,不为 null 时外层 requestTask 的 callback 会将数据写给客户端 - * 定时任务 2:周期 30 秒一次,两个任务 - * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 - * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 - ```java - MQClientInstance.this.cleanOfflineBroker(); - MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); - ``` +*** - ```java - // 心跳数据 - public class HeartbeatData extends RemotingSerializable { - // 客户端 ID ip@pid - private String clientID; - // 存储客户端所有生产者数据 - private Set producerDataSet = new HashSet(); - // 存储客户端所有消费者数据 - private Set consumerDataSet = new HashSet(); - } - ``` - * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 - ```java - MQClientInstance.this.persistAllConsumerOffset(); - ``` +##### 长轮询 - * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 +PullRequestHoldService 类负责长轮询,BrokerController#start 方法中调用了 `this.pullRequestHoldService.start()` 启动该服务 - ```java - MQClientInstance.this.adjustThreadPool(); - ``` +核心方法: -* updateTopicRouteInfoFromNameServer():**更新路由数据** +* run():核心运行方法 - * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + ```java + public void run() { + // 循环运行 + while (!this.isStopped()) { + if (this.brokerController.getBrokerConfig().isLongPollingEnable()) { + // 服务器开启长轮询开关:每次循环休眠5秒 + this.waitForRunning(5 * 1000); + } else { + // 服务器关闭长轮询开关:每次循环休眠1秒 + this.waitForRunning(...); + } + // 检查持有的请求 + this.checkHoldRequest(); + // ..... + } + } + ``` - `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 +* checkHoldRequest():检查所有的请求 - `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + * `for (String key : this.pullRequestTable.keySet())`:**处理所有的 topic@queueId 的逻辑** + * `String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR)`:key 按照 @ 拆分,得到 topic 和 queueId + * `long offset = this...getMaxOffsetInQueue(topic, queueId)`: 到存储模块查询该 ConsumeQueue 的**最大 offset** + * `this.notifyMessageArriving(topic, queueId, offset)`:通知消息到达 - * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) +* notifyMessageArriving():通知消息到达的逻辑,ReputMessageService 消息分发服务也会调用该方法 - * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:获取对应的的manyPullRequest对象 + * `List requestList`:获取该队列下的所有 PullRequest,并进行遍历 + * `List replayList`:当某个 pullRequest 不超时,并且对应的 `CQ.maxOffset <= pullRequest.offset`,就将该 PullRequest 再放入该列表 + * `long newestOffset`:该值为 CQ 的 maxOffset + * `if (newestOffset > request.getPullFromThisOffset())`:说明该请求对应的队列内可以 pull 消息了,**结束长轮询** + * `boolean match`:进行过滤匹配 + * `this.brokerController...executeRequestWhenWakeup()`:将满足条件的 pullRequest 再次提交到线程池内执行 + * `final RemotingCommand response`:执行 processRequest 方法,并且**不会触发长轮询** + * `channel.writeAndFlush(response).addListene()`:**将结果数据发送给客户端** + * `if (System.currentTimeMillis() >= ...)`:判断该 pullRequest 是否超时,超时后的也是重新提交到线程池,并且不进行长轮询 + * `mpr.addPullRequest(replayList)`:将未满足条件的 PullRequest 对象再次添加到 ManyPullRequest 属性中 - * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 - * `if (changed)`:不一致进入更新逻辑 - `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 +*** - `Update Pub info`:更新生产者信息 - * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** - * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 - `Update sub info`:更新消费者信息 +##### 结果类 - `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 +GetMessageResult 类成员信息: + +```java +public class GetMessageResult { + // 查询消息时,最底层都是 mappedFile 支持的查询,查询时返回给外层一个 SelectMappedBufferResult, + // mappedFile 每查询一次都会 refCount++ ,通过SelectMappedBufferResult持有mappedFile,完成资源释放的句柄 + private final List messageMapedList = + new ArrayList(100); + + // 该List内存储消息,每一条消息都被转成 ByteBuffer 表示了 + private final List messageBufferList = new ArrayList(100); + // 查询结果状态 + private GetMessageStatus status; + // 客户端下次再向当前Queue拉消息时,使用的 offset + private long nextBeginOffset; + // 当前queue最小offset + private long minOffset; + // 当前queue最大offset + private long maxOffset; + // 消息总byte大小 + private int bufferTotalSize = 0; + // 服务器建议客户端下次到该 queue 拉消息时是否使用 【从节点】 + private boolean suggestPullingFromSlave = false; +} +``` -**** +*** -#### 网络通信 +#### 队列快照 ##### 成员属性 -NettyRemotingClient 类负责客户端的网络通信 +ProcessQueue 类是消费队列的快照 成员变量: -* Netty 服务相关属性: +* 属性字段: ```java - private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 - private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 - private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + private final AtomicLong msgCount = new AtomicLong(); // 队列中消息数量 + private final AtomicLong msgSize = new AtomicLong(); // 消息总大小 + private volatile long queueOffsetMax = 0L; // 快照中最大 offset + private volatile boolean dropped = false; // 快照是否移除 + private volatile long lastPullTimestamp = current; // 上一次拉消息的时间 + private volatile long lastConsumeTimestamp = current; // 上一次消费消息的时间 + private volatile long lastLockTimestamp = current; // 上一次获取锁的时间 ``` -* Channel 映射表: +* **消息容器**:key 是消息偏移量,val 是消息 ```java - private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 - private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + private final TreeMap msgTreeMap = new TreeMap(); ``` -* 定时器:启动定时任务 +* **顺序消费临时容器**: ```java - private final Timer timer = new Timer("ClientHouseKeepingService", true) + private final TreeMap consumingMsgOrderlyTreeMap = new TreeMap(); ``` -* 线程池: +* 锁: ```java - private ExecutorService publicExecutor; // 公共线程池 - private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + private final ReadWriteLock lockTreeMap; // 读写锁 + private final Lock lockConsume; // 重入锁,【顺序消费使用】 ``` -* 事件监听器:客户端这里是 null +* 顺序消费状态: ```java - private final ChannelEventListener channelEventListener; + private volatile boolean locked = false; // 是否是锁定状态 + private volatile boolean consuming = false; // 是否是消费中 ``` -构造方法 + -* 无参构造: +**** - ```java - public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { - this(nettyClientConfig, null); - } - ``` -* 有参构造: + +##### 成员方法 + +核心成员方法 + +* putMessage():将 Broker 拉取下来的 msgs 存储到快照队列内,返回为 true 表示提交顺序消费任务,false 表示不提交 ```java - public NettyRemotingClient(nettyClientConfig, channelEventListener) { - // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 - super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); - this.nettyClientConfig = nettyClientConfig; - this.channelEventListener = channelEventListener; - - // 创建公共线程池 - int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); - if (publicThreadNums <= 0) { - publicThreadNums = 4; - } - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); - - // 创建 Netty IO 线程,1个线程 - this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); - - if (nettyClientConfig.isUseTLS()) { - sslContext = TlsHelper.buildSslContext(true); - } - } + public boolean putMessage(final List msgs) ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `for (MessageExt msg : msgs)`:遍历 msgs 全部加入 msgTreeMap,key 是消息的 queueOffset -**** + * `if (!msgTreeMap.isEmpty() && !this.consuming)`:**消息容器中存在未处理的消息,并且不是消费中的状态** + `dispatchToConsume = true`:代表需要提交顺序消费任务 + `this.consuming = true`:设置为顺序消费执行中的状态 -##### 成员方法 + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* start():启动方法 +* removeMessage():移除已经消费的消息,参数是已经消费的消息集合 ```java - public void start() { - // channel pipeline 内的 handler 使用的线程资源,默认 4 个 - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); - // 配置 netty 客户端启动类对象 - Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) - //... - .handler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - // 加几个handler - pipeline.addLast( - // 服务端的数据,都会来到这个 - new NettyClientHandler()); - } - }); - // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 - // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 - this.timer.scheduleAtFixedRate(() -> { - NettyRemotingClient.this.scanResponseTable(); - }, 1000 * 3, 1000); - // 这里是 null,不启动 - if (this.channelEventListener != null) { - this.nettyEventExecutor.start(); - } - } + public long removeMessage(final List msgs) ``` -* 单向通信: + * `long result = -1`:结果初始化为 -1 + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `if (!msgTreeMap.isEmpty())`:判断消息容器是否是空,**是空直接返回 -1** + * `result = this.queueOffsetMax + 1`:设置结果,**删除完后消息容器为空时返回** + * `for (MessageExt msg : msgs)`:将已经消费的消息全部从 msgTreeMap 移除 + * `if (!msgTreeMap.isEmpty())`:移除后容器内还有待消费的消息,**获取第一条消息 offset 返回** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* takeMessages():获取一批消息 ```java - public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { - // 开始时间 - long beginStartTime = System.currentTimeMillis(); - // 获取或者创建客户端与服务端(addr)的通道 channel - final Channel channel = this.getAndCreateChannel(addr); - // 条件成立说明客户端与服务端 channel 通道正常,可以通信 - if (channel != null && channel.isActive()) { - try { - // 执行 rpcHook 拓展点 - doBeforeRpcHooks(addr, request); - // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 - long costTime = System.currentTimeMillis() - beginStartTime; - if (timeoutMillis < costTime) { - throw new RemotingTimeoutException("invokeSync call timeout"); - } - // 参数1:客户端-服务端通道channel - // 参数二:网络层传输对象,封装着请求数据 - // 参数三:剩余的超时限制 - RemotingCommand response = this.invokeSyncImpl(channel, request, ...); - // 后置处理 - doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); - // 返回响应数据 - return response; - } catch (RemotingSendRequestException e) {} - } else { - this.closeChannel(addr, channel); - throw new RemotingConnectException(addr); - } - } + public List takeMessages(final int batchSize) ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `for (int i = 0; i < batchSize; i++)`:从头节点开始获取消息 + * `result.add(entry.getValue())`:将消息放入结果集合 + * `consumingMsgOrderlyTreeMap.put()`:将消息加入顺序消费容器中 + * `if (result.isEmpty())`:条件成立说明顺序消费容器本地快照内的消息全部处理完了,**当前顺序消费任务需要停止** + * `consuming = false`:消费状态置为 false + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* commit():处理完一批消息后调用 + ```java + public long commit() + ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `Long offset = this.consumingMsgOrderlyTreeMap.lastKey()`:获取顺序消费临时容器最后一条数据的 key + * `msgCount, msgSize`:更新顺序消费相关的字段 + * `this.consumingMsgOrderlyTreeMap.clear()`:清空顺序消费容器的数据 + * `return offset + 1`:**消费者下一条消费的位点** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 +* cleanExpiredMsg():清除过期消息 + + ```java + public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) + ``` + * `if (pushConsumer.getDefaultMQPushConsumerImpl().isConsumeOrderly()) `:顺序消费不执行过期清理逻辑 + * `int loop = msgTreeMap.size() < 16 ? msgTreeMap.size() : 16`:最多循环 16 次 + * `if (!msgTreeMap.isEmpty() &&)`:如果容器中第一条消息的消费开始时间与当前系统时间差值 > 15min,则取出该消息 + * `else`:直接跳出循环,因为**快照队列内的消息是有顺序的**,第一条消息不过期,其他消息都不过期 + * `pushConsumer.sendMessageBack(msg, 3)`:**消息回退**到服务器,设置该消息的延迟级别为 3 + * `if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey())`:条件成立说明消息回退期间,该目标消息并没有被消费任务成功消费 + * `removeMessage(Collections.singletonList(msg))`:从 treeMap 将该回退成功的 msg 删除 -*** @@ -8061,6 +8750,8 @@ NettyRemotingClient 类负责客户端的网络通信 + + ## TEST diff --git a/Java.md b/Java.md index 3004ef2..b932420 100644 --- a/Java.md +++ b/Java.md @@ -5013,7 +5013,7 @@ HashMap继承关系如下图所示: 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: @@ -5076,7 +5076,7 @@ HashMap继承关系如下图所示: * treeifyBin() - 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: + 节点添加完成之后判断此时节点个数是否大于 TREEIFY_THRESHOLD 临界值 8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: ```java if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st @@ -5084,7 +5084,7 @@ HashMap继承关系如下图所示: treeifyBin(tab, hash); ``` - 1. 如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64)就去扩容,而不是将节点变为红黑树 + 1. 如果当前数组为空或者数组的长度小于进行树形化的阈 MIN_TREEIFY_CAPACITY = 64 就去扩容,而不是将节点变为红黑树 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 From 159ce03e152acfd33a068eebd59f64a3a9614b80 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 27 Jan 2022 22:35:25 +0800 Subject: [PATCH 17/78] Update Java Notes --- Frame.md | 713 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 691 insertions(+), 22 deletions(-) diff --git a/Frame.md b/Frame.md index bc07592..ee3ce2c 100644 --- a/Frame.md +++ b/Frame.md @@ -3816,7 +3816,7 @@ public class Consumer { 顺序消息分为全局顺序消息与分区顺序消息, - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 -- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 Sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当**发送和消费参与的 queue 只有一个**,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4000,7 +4000,7 @@ Broker 可以配置 messageDelayLevel,该属性是 Broker 的属性,不属 - 1<=level<=maxLevel:消息延迟特定时间,例如 level==1,延迟 1s - level > maxLevel:则 level== maxLevel,例如 level==20,延迟 2h -定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识`queueId = delayTimeLevel – 1`,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic +定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic 注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高。 @@ -6212,9 +6212,14 @@ CommitLog 类核心方法: ``` * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 - * `String topic = msg.getTopic()`:获取主题和队列 ID + * `msg.setBodyCRC(UtilAll.crc32(msg.getBody()))`:获取消息的 CRC 值 + * `topic、queueId`:获取主题和队列 ID + * `if (msg.getDelayTimeLevel() > 0) `:获取消息的延迟级别 + * `topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC`:**修改消息的主题为 `SCHEDULE_TOPIC_XXXX`** + * `queueId = ScheduleMessageService.delayLevel2QueueId()`:队列 ID 为延迟级别 -1 + * `MessageAccessor.putProperty`:将原来的消息主题和 ID 存入消息的属性 `REAL_TOPIC` 中 + * `msg.setTopic(topic)`:修改主题 * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 - * `putMessageLock.lock()`:获取**写锁** * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 @@ -6813,12 +6818,22 @@ FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 ``` * `while (!this.isStopped())`:stopped为 true 才跳出循环 + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + * `int interval`:获取配置中的刷盘时间间隔 + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** @@ -6989,6 +7004,12 @@ public static BrokerController start(BrokerController controller) { } ``` +BrokerStartup#createBrokerController:构造控制器,并初始化 + +* `final BrokerController controller()`:创建实例对象 +* `boolean initResult = controller.initialize()`:控制器初始化 + * `this.registerProcessor()`:**注册了处理器,包括发送消息、拉取消息、查询消息等核心处理器** + BrokerController#start:核心启动方法 * `this.messageStore.start()`:**启动存储服务** @@ -7102,7 +7123,7 @@ DefaultMQProducer 是生产者的默认实现类 } ``` -* request():请求方法,**需要消费者回执消息**,又叫回退消息 +* request():请求方法,**需要消费者回执消息** ```java public Message request(final Message msg, final MessageQueue mq, final long timeout) { @@ -7336,7 +7357,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 - * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息,有回调函数** * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 @@ -7611,7 +7632,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回退消息 ```java private final DefaultMQProducer defaultMQProducer; @@ -7918,6 +7939,236 @@ NettyRemotingClient 类负责客户端的网络通信 +*** + + + +#### 延迟消息 + +##### 消息处理 + +BrokerStartup 初始化 BrokerController 调用 `registerProcessor()` 方法将 SendMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `SEND_MESSAGE = 10`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest + +```java +// 参数一:处理通道的事件; 参数二:客户端 +public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) { + RemotingCommand response = null; + response = asyncProcessRequest(ctx, request).get(); + return response; +} +``` + +SendMessageProcessor#asyncConsumerSendMsgBack:异步发送消费者的回调消息 + +* `final RemotingCommand response`:创建一个服务器响应对象 + +* `final ConsumerSendMsgBackRequestHeader requestHeader`:解析出客户端请求头信息,几个**核心字段**: + + * `private Long offset`:回退消息的 CommitLog offset + * `private Integer delayLevel`:延迟级别,一般是 0 + * `private String originMsgId, originTopic`:原始的消息 ID,主题 + * `private Integer maxReconsumeTimes`:最大重试次数,默认是 16 次 + +* `if ()`:鉴权,是否找到订阅组配置、Broker 是否支持写请求、订阅组是否支持消息重试 + +* `String newTopic = MixAll.getRetryTopic(...)`:获取**消费者组的重试主题**,规则是 `%RETRY%GroupName` + +* `int queueIdInt = Math.abs()`:充实主题下的队列 ID 是 0 + +* `TopicConfig topicConfig`:获取重试主题的配置信息 + +* `MessageExt msgExt`:根据消息的物理 offset 到存储模块查询,内部先查询出这条消息的 size,然后再根据 offset 和 size 查询出整条 msg + +* `final String retryTopic`:获取消息的原始主题 + +* `if (null == retryTopic)`:条件成立说明**当前消息是第一次被回退**, 添加 `RETRY_TOPIC` 属性 + +* `msgExt.setWaitStoreMsgOK(false)`:异步刷盘 + +* `if (msgExt...() >= maxReconsumeTimes || delayLevel < 0)`:消息重试次数超过最大次数,不支持重试 + + `newTopic = MixAll.getDLQTopic()`:获取消费者的死信队列,规则是 `%DLQ%GroupName` + + `queueIdInt, topicConfig`:死信队列 ID 为 0,创建死信队列的配置 + +* `if (0 == delayLevel)`:说明延迟级别由 Broker 控制 + + `delayLevel = 3 + msgExt.getReconsumeTimes()`:**延迟级别默认从 3 级开始**,每重试一次,延迟级别 +1 + +* `msgExt.setDelayTimeLevel(delayLevel)`:**将延迟级别设置进消息属性**,存储时会检查该属性,该属性值 > 0 会将消息的主题和队列再次修改,修改为调度主题和调度队列 ID + +* `MessageExtBrokerInner msgInner`:创建一条空消息,消息属性从 offset 查询出来的 msg 中拷贝 + +* `msgInner.setReconsumeTimes)`:重试次数设置为原 msg 的次数 +1 + +* `UtilAll.isBlank(originMsgId)`:判断消息是否是初次返回到服务器 + + * true:说明 msgExt 消息是第一次被返回到服务器,此时使用该 msg 的 id 作为 originMessageId + * false:说明原始消息已经被重试不止 1 次,此时使用 offset 查询出来的 msg 中的 originMessageId + +* `CompletableFuture putMessageResult = ..asyncPutMessage(msgInner)`:调用存储模块存储消息 + + `DefaultMessageStore#asyncPutMessage`: + + * `PutMessageResult result = this.commitLog.asyncPutMessage(msg)`:**将新消息存储到 CommitLog 中** + + + +*** + + + +##### 调度服务 + +DefaultMessageStore 中有成员属性 ScheduleMessageService,在 start 方法中会启动该调度服务 + +成员变量: + +* 延迟级别属性表: + + ```java + // 存储延迟级别对应的 延迟时间长度 (单位:毫秒) + private final ConcurrentMap delayLevelTable; + // 存储延迟级别 queue 的消费进度 offset,该 table 每 10 秒钟,会持久化一次,持久化到本地磁盘 + private final ConcurrentMap offsetTable; + ``` + +* 最大延迟级别: + + ```java + private int maxDelayLevel; + ``` + +* 模块启动状态: + + ```java + private final AtomicBoolean started = new AtomicBoolean(false); + ``` + +* 定时器:内部有线程资源,可执行调度任务 + + ```java + private Timer timer; + ``` + +成员方法: + +* load():加载调度消息,初始化 delayLevelTable 和 offsetTable + + ```java + public boolean load() + ``` + +* start():启动消息调度服务 + + ```java + public void start() + ``` + + * `if (started.compareAndSet(false, true))`:将启动状态设为 true + + * `this.timer`:创建定时器对象 + + * `for (... : this.delayLevelTable.entrySet())`:为**每个延迟级别创建一个延迟任务**提交到 timer ,延迟 1 秒后执行 + + * `this.timer.scheduleAtFixedRate()`:提交周期型任务,延迟 10 秒执行,周期为 10 秒,持久化延迟队列消费进度任务 + + `ScheduleMessageService.this.persist()`:持久化消费进度 + + + +*** + + + +##### 调度任务 + +DeliverDelayedMessageTimerTask 是一个任务类 + +成员变量: + +* 延迟级别:延迟队列任务处理的延迟级别 + + ```java + private final int delayLevel; + ``` + +* 消费进度:延迟队列任务处理的延迟队列的消费进度 + + ```java + private final long offset; + ``` + +成员方法: + +* run():执行任务 + + ```java + public void run() { + if (isStarted()) { + this.executeOnTimeup(); + } + ``` + +* executeOnTimeup():执行任务 + + ```java + public void executeOnTimeup() + ``` + + * `ConsumeQueue cq`:获取出该延迟队列任务处理的延迟队列 ConsumeQueue + + * `SelectMappedBufferResult bufferCQ`:根据消费进度查询出 SMBR 对象 + + * `for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE)`:每次读取 20 各字节的数据 + + * `offsetPy, sizePy`:延迟消息的物理偏移量和消息大小 + + * `long tagsCode`:延迟消息的交付时间,在 ReputMessageService 转发时根据消息的 DELAY 属性是否 >0 ,会在 tagsCode 字段存储交付时间 + + * `long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode)`:**延迟交付时间** + + * `long maxTimestamp`:当前时间 + 延迟级别对应的延迟毫秒值的时间戳 + * `if (deliverTimestamp > maxTimestamp)`:条件成立说明延迟时间过长,调整为当前时间立刻执行 + * `return result`:一般情况 result 就是 deliverTimestamp + + * `long countdown = deliverTimestamp - now`:计算差值 + + * `if (countdown <= 0)`:消息已经到达交付时间了 + + `MessageExt msgExt`:根据物理偏移量和消息大小获取这条消息 + + `MessageExtBrokerInner msgInner`:**构建一条新消息**,将原消息的属性拷贝过来 + + * `long tagsCodeValue`:不再是交付时间了 + * `MessageAccessor.clearProperty(msgInner, DELAY..)`:清理新消息的 DELAY 属性,避免存储时重定向到延迟队列 + * `msgInner.setTopic()`:修改主题为原始的主题 `%RETRY%GroupName` + * `String queueIdStr`:修改队列 ID 为原始的 ID + + `PutMessageResult putMessageResult`:**将新消息存储到 CommitLog**,消费者订阅的是目标主题,会再次消费该消息 + + * `else`:消息还未到达交付时间 + + `ScheduleMessageService.this.timer.schedule()`:创建该延迟级别的任务,延迟 countDown 毫秒之后再执行 + + `ScheduleMessageService.this.updateOffset()`:更新延迟级别队列的消费进度 + + * `PutMessageResult putMessageResult` + + * `bufferCQ == null`:说明通过消费进度没有获取到数据 + + `if (offset < cqMinOffset)`:如果消费进度比最小位点都小,说明是过期数据,重置为最小位点 + + * `ScheduleMessageService.this.timer.schedule()`:重新提交该延迟级别对应的延迟队列任务,延迟 100 毫秒后执行 + + + + + + + + + **** @@ -8209,7 +8460,7 @@ RebalanceImpl 类成员变量: * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue - * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:负载均衡,更新队列处理集合 + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:**更新队列处理集合** * `boolean changed = false`:当前消费者的消费队列是否有变化 @@ -8219,25 +8470,70 @@ RebalanceImpl 类成员变量: `pq.setDropped(true)`:将删除状态设置为 true - `if (this.removeUnnecessaryMessageQueue(mq, pq))`:在 MQ 归属的 broker 节点持久化消费进度,并删除该 MQ 在本地的消费进度 + `if (this.removeUnnecessaryMessageQueue(mq, pq))`:删除不需要的 MQ 队列 - `it.remove()`:从 processQueueTable 移除该 MQ + * `this...getOffsetStore().persist(mq)`:在 MQ 归属的 Broker 节点持久化消费进度 - * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑、 + * `this...getOffsetStore().removeOffset(mq)`:删除该 MQ 在本地的消费进度 + * `if (this.defaultMQPushConsumerImpl.isConsumeOrderly() &&)`:是否是**顺序消费**和集群模式 + + `if (pq.getLockConsume().tryLock(1000, ..))`: 获取锁成功,说明顺序消费任务已经停止消费工作 + + `return this.unlockDelay(mq, pq)`:**释放锁 Broker 端的队列锁** + + * `if (pq.hasTempMessage())`:队列中有消息,延迟 20 秒释放队列分布式锁,确保全局范围内只有一个消费任务 运行中 + * `else`:当前消费者本地该消费任务已经退出,直接释放锁 + + `else`:顺序消费任务正在消费一批消息,不可打断,增加尝试获取锁的次数 + + `it.remove()`:从 processQueueTable 移除该 MQ + + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑 + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配**到当前节点的队列 + + `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** + + `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + + `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + + `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + + `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + + * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + +* lockAll():续约锁,对消费者的所有队列进行续约 - * `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** + ```java + public void lockAll() + ``` - * `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + * `HashMap> brokerMqs`:将分配给当前消费者的全部 MQ,按照 BrokerName 分组 - * `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + * `while (it.hasNext())`:遍历所有的分组 - * `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + * `final Set mqs`:获取该 Broker 上分配给当前消费者的 queue 集合 - * `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + * `FindBrokerResult findBrokerResult`:查询 Broker 主节点信息 - * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + * `LockBatchRequestBody requestBody`:创建请求对象,填充属性 + + * `Set lockOKMQSet`:**向 Broker 发起批量续约锁的同步请求**,返回成功的队列集合 + + * `for (MessageQueue mq : lockOKMQSet)`:遍历续约锁成功的 MQ + + `processQueue.setLocked(true)`:分布式锁状态设置为 true,**表示允许顺序消费** + + `processQueue.setLastLockTimestamp(System.currentTimeMillis())`:设置上次获取锁的时间为当前时间 + + * `for (MessageQueue mq : mqs)`:遍历当前 Broker 上的所有队列集合 + + `if (!lockOKMQSet.contains(mq))`:条件成立说明续约锁失败 + + `processQueue.setLocked(false)`:分布式锁状态设置为 false,表示不允许顺序消费 @@ -8294,7 +8590,7 @@ AllocateMessageQueueStrategy 类是队列的分配策略 -#### 消息拉取 +#### 拉取服务 ##### 实现方式 @@ -8441,7 +8737,7 @@ PullAPIWrapper 类封装了拉取消息的 API -#### 通信处理 +#### 拉取处理 ##### 处理器 @@ -8684,7 +8980,7 @@ ProcessQueue 类是消费队列的快照 * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* removeMessage():移除已经消费的消息,参数是已经消费的消息集合 +* removeMessage():移除已经消费的消息,参数是已经消费的消息集合,并发消费使用 ```java public long removeMessage(final List msgs) @@ -8699,7 +8995,7 @@ ProcessQueue 类是消费队列的快照 * `if (!msgTreeMap.isEmpty())`:移除后容器内还有待消费的消息,**获取第一条消息 offset 返回** * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* takeMessages():获取一批消息 +* takeMessages():获取一批消息,顺序消费使用 ```java public List takeMessages(final int batchSize) @@ -8714,7 +9010,7 @@ ProcessQueue 类是消费队列的快照 * `consuming = false`:消费状态置为 false * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* commit():处理完一批消息后调用 +* commit():处理完一批消息后调用,顺序消费使用 ```java public long commit() @@ -8743,6 +9039,379 @@ ProcessQueue 类是消费队列的快照 +**** + + + +#### 并发消费 + +##### 成员属性 + +ConsumeMessageConcurrentlyService 负责并发消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerConcurrently messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + private final ScheduledExecutorService cleanExpireMsgExecutors; // 清理过期消息任务线程池,15min 一次 + ``` + + + +*** + + + +##### 成员方法 + +ConsumeMessageConcurrentlyService 并发消费核心方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() { + // 提交“清理过期消息任务”任务,延迟15min之后执行,之后每15min执行一次 + this.cleanExpireMsgExecutors.scheduleAtFixedRate(() -> cleanExpireMsg()}, + 15, 15, TimeUnit.MINUTES); + } + ``` + +* cleanExpireMsg():清理过期消息任务 + + ```java + private void cleanExpireMsg() + ``` + + * `Iterator> it `:获取分配给当前消费者的队列 + * `while (it.hasNext())`:遍历所有的队列 + * `pq.cleanExpiredMsg(this.defaultMQPushConsumer)`:调用队列快照 ProcessQueue 清理过期消息的方法 + +* submitConsumeRequest():提交消费请求 + + ```java + // 参数一:从服务器 pull 下来的这批消息 + // 参数二:消息归属 mq 在消费者端的 processQueue,提交消费任务之前,msgs已经加入到该pq内了 + // 参数三:消息归属队列 + // 参数四:并发消息此参数无效 + public void submitConsumeRequest(List msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispatchToConsume) + ``` + + * `final int consumeBatchSize`:**一个消费任务**可消费的消息数量,默认为 1 + + * `if (msgs.size() <= consumeBatchSize)`:判断一个消费任务是否可以提交 + + `ConsumeRequest consumeRequest`:封装为消费请求 + + `this.consumeExecutor.submit(consumeRequest)`:提交消费任务,异步执行消息的处理 + + * `else`:说明消息较多,需要多个消费任务 + + `for (int total = 0; total < msgs.size(); )`:将消息拆分成多个消费任务 + +* processConsumeResult():处理消费结果 + + ```java + // 参数一:消费结果状态; 参数二:消费上下文; 参数三:当前消费任务 + public void processConsumeResult(status, context, consumeRequest) + ``` + + * `switch (status)`:根据消费结果状态进行处理 + + * `case CONSUME_SUCCESS`:消费成功 + + `if (ackIndex >= consumeRequest.getMsgs().size())`:消费成功的话,ackIndex 设置成 `消费消息数 - 1` 的值,比如有 5 条消息,这里就设置为 4 + + `ok, failed`:ok 设置为消息数量,failed 设置为 0 + + * `case RECONSUME_LATER`:消费失败 + + `ackIndex = -1`:设置为 -1 + + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,默认是**集群模式** + + * `for (int i = ackIndex + 1; i < msgs.size(); i++)`:当消费失败时 ackIndex 为 -1,i 的起始值为 0,该消费任务内的全部消息都会尝试回退给服务器 + + * `MessageExt msg`:提取一条消息 + + * `boolean result = this.sendMessageBack(msg, context)`:发送**消息回退** + + * `String brokerAddr`:根据 brokerName 获取 master 节点地址 + * `his.mQClientFactory...consumerSendMessageBack()`:发送回退消息 + * `RemotingCommand request`:创建请求对象 + * `RemotingCommand response = this.remotingClient.invokeSync()`:**同步请求** + + * `if (!result)`:回退失败的消息,将**消息的重试属性加 1**,并加入到回退失败的集合 + + * `if (!msgBackFailed.isEmpty())`:回退失败集合不为空 + + `consumeRequest.getMsgs().removeAll(msgBackFailed)`:将回退失败的消息从当前消费任务的 msgs 集合内移除 + + `this.submitConsumeRequestLater()`:回退失败的消息会再次提交消费任务,延迟 5 秒钟后**再次尝试消费** + + * `long offset = ...removeMessage(msgs)`:从 pq 中删除已经消费成功的消息,返回 offset + + * `this...getOffsetStore().updateOffset()`:更新消费者本地该 mq 的**消费进度** + + + +*** + + + +##### 消费请求 + +ConsumeRequest 是 ConsumeMessageConcurrentlyService 的内部类,是一个 Runnable 任务对象 + +成员变量: + +* 分配到该消费任务的消息: + + ```java + private final List msgs; + ``` + +* 消息队列: + + ```java + private final ProcessQueue processQueue; // 消息处理队列 + private final MessageQueue messageQueue; // 消息队列 + ``` + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `if (this.processQueue.isDropped())`:条件成立说明该 queue 经过 rbl 算法分配到其他的 consumer + * `MessageListenerConcurrently listener`:获取消息监听器 + * `ConsumeConcurrentlyContext context`:创建消费上下文对象 + * `defaultMQPushConsumerImpl.resetRetryAndNamespace()`:重置重试标记 + * `final String groupTopic`:获取当前消费者组的重试主题 `%RETRY%GroupName` + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `String retryTopic = msg.getProperty(...)`:原主题,一般消息没有该属性,只有被重复消费的消息才有 + * `if (retryTopic != null && groupTopic.equals(...))`:条件成立说明该消息是被重复消费的消息 + * `msg.setTopic(retryTopic)`:将被**重复消费的消息主题修改回原主题** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:前置处理 + * `boolean hasException = false`:消费过程中,是否向外抛出异常 + * `MessageAccessor.setConsumeStartTimeStamp()`:给每条消息设置消费开始时间 + * `status = listener.consumeMessage(Collections.unmodifiableList(msgs), context)`:**消费消息** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:后置处理 + * `...processConsumeResult(status, context, this)`:**处理消费结果** + + + +**** + + + +#### 顺序消费 + +##### 成员属性 + +ConsumeMessageOrderlyService 负责顺序消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerOrderly messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + private volatile boolean stopped = false; // 消费停止状态 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + ``` + +* 队列锁:消费者本地 MQ 锁,确保本地对于需要顺序消费的 MQ 同一时间只有一个任务在执行 + + ```java + private final MessageQueueLock messageQueueLock = new MessageQueueLock(); + ``` + + ```java + public class MessageQueueLock { + private ConcurrentMap mqLockTable = new ConcurrentHashMap(); + // 获取本地队列锁对象 + public Object fetchLockObject(final MessageQueue mq) { + Object objLock = this.mqLockTable.get(mq); + if (null == objLock) { + objLock = new Object(); + Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock); + if (prevLock != null) { + objLock = prevLock; + } + } + return objLock; + } + } + ``` + + 已经获取了 Broker 端该 Queue 的独占锁,为什么还要获取本地队列锁对象?(这里我也没太懂,先记录下来) + + * Broker queue 占用锁的角度是 Client 占用,Client 从 Broker 的某个占用了锁的 queue 拉取下来消息以后,将消息存储到消费者本地的 ProcessQueue 中,快照对象的 consuming 属性置为 true,表示本地的队列正在消费处理中。 + * ProcessQueue 调用 takeMessages 方法时会获取下一批待处理的消息,获取不到会修改 `consuming = false`,本消费任务马上停止。 + * 如果此时 Pull 再次拉取一批当前 ProcessQueue 的 msg,会再次向顺序消费服务提交消费任务,此时需要本地队列锁对象同步本地线程 + + + +*** + + + +##### 成员方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() + ``` + + * `this.scheduledExecutorService.scheduleAtFixedRate()`:提交锁续约任务,延迟 1 秒执行,周期为 20 秒钟 + * `ConsumeMessageOrderlyService.this.lockMQPeriodically()`:**锁续约任务** + * `this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll()`:对消费者的所有队列进行续约 + +* submitConsumeRequest():**提交消费任务请求** + + ```java + // 参数:true 表示创建消费任务并提交,false不创建消费任务,说明消费者本地已经有消费任务在执行了 + public void submitConsumeRequest(...., final boolean dispathToConsume) { + if (dispathToConsume) { + // 当前进程内不存在 顺序消费任务,创建新的消费任务,【提交到消费任务线程池】 + ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue); + this.consumeExecutor.submit(consumeRequest); + } + } + ``` + +* processConsumeResult():消费结果处理 + + ```java + // 参数1:msgs 本轮循环消费的消息集合 参数2:status 消费状态 + // 参数3:context 消费上下文 参数4:消费任务 + // 返回值:boolean 决定是否继续循环处理pq内的消息 + public boolean processConsumeResult(final List msgs, final ConsumeOrderlyStatus status, final ConsumeOrderlyContext context, final ConsumeRequest consumeRequest) + ``` + + * `if (context.isAutoCommit()) `:默认自动提交 + + * `switch (status)`:根据消费状态进行不同的处理 + + * `case SUCCESS`:消费成功 + + `commitOffset = ...commit()`:调用 pq 提交方法,会将本次循环处理的消息从顺序消费 map 删除,并且返回消息进度 + + * `case SUSPEND_CURRENT_QUEUE_A_MOMENT`:挂起当前队列 + + `consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs)`:回滚消息 + + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset())`:从顺序消费临时容器中移除 + * `this.msgTreeMap.put(msg.getQueueOffset(), msg)`:添加到消息容器 + + * `this.submitConsumeRequestLater()`:再次提交消费任务,1 秒后执行 + + * `continueConsume = false`:设置为 false,**外层会退出本次的消费任务** + + * `this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(...)`:更新本地消费进度 + + + +**** + + + +##### 消费请求 + +ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnable 任务对象 + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `final Object objLock`:获取本地锁对象 + + * `synchronized (objLock)`:本地队列锁,确保每个 MQ 的消费任务只有一个在执行,**确保顺序消费** + + * `if(.. || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())))`:当前队列持有分布式锁,并且锁未过期,持锁时间超过 30 秒算过期 + + * `final long beginTime`:消费开始时间 + + * `for (boolean continueConsume = true; continueConsume; )`:根据是否继续消费的标记判断是否继续 + + * `final int consumeBatchSize`:获取每次循环处理的消息数量,一般是 1 + + * `List msgs = this...takeMessages(consumeBatchSize)`:到**处理队列获取一批消息** + + * `if (!msgs.isEmpty())`:获取到了待消费的消息 + + `final ConsumeOrderlyContext context`:创建消费上下文对象 + + `this.processQueue.getLockConsume().lock()`:**获取 lockConsume 锁**,与 RBL 线程同步使用 + + `status = messageListener.consumeMessage(...)`:监听器处理消息 + + `this.processQueue.getLockConsume().unlock()`:**释放 lockConsume 锁** + + `if (null == status)`:处理消息状态返回 null,设置状态为挂起当前队列 + + `continueConsume = ...processConsumeResult()`:消费结果处理 + + * `else`:获取到的消息是空 + + `continueConsume = false`:结束任务循环 + + * `else`:当前队列未持有分布式锁,或者锁过期 + + `ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume()`:重新提交任务,根据是否获取到队列锁,选择延迟 10 毫秒或者 300 毫秒 + + + + + + + + + + + + + + + From cbfb2fb2c4eea14ac2443a2fbd2a926fdf565062 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 30 Jan 2022 22:34:01 +0800 Subject: [PATCH 18/78] Update Java Notes --- Frame.md | 1236 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 1069 insertions(+), 167 deletions(-) diff --git a/Frame.md b/Frame.md index ee3ce2c..cec0168 100644 --- a/Frame.md +++ b/Frame.md @@ -4227,7 +4227,7 @@ ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消费队列结构.png) -* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 +* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 * SQL92 过滤:工作流程和 Tag 过滤大致一样,只是在 Store 层的具体过滤方式不一样。真正的 SQL expression 的构建和执行由 rocketmq-filter 模块负责,每次过滤都去执行 SQL 表达式会影响效率,所以 RocketMQ 使用了 BloomFilter 来避免了每次都去执行 @@ -4309,16 +4309,16 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 1. 事务消息发送及提交: * 发送消息(Half 消息) - - * 服务端响应消息写入结果 - +* 服务端响应消息写入结果 * 根据发送结果执行本地事务(如果写入失败,此时 Half 消息对业务不可见,本地逻辑不执行) - * 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) +* 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) 2. 补偿流程: - * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次回查 - * Producer 收到回查消息,检查回查消息对应的本地事务的状态 + * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),服务端根据根据半消息的生产者组,到 ProducerManager 中获取生产者的会话通道,发起一次回查(**单向请求**) + * Producer 收到回查消息,检查事务消息状态表内对应的本地事务的状态 * 根据本地事务状态,重新 Commit 或者 Rollback @@ -4370,7 +4370,7 @@ RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中 如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ 采用了一种补偿机制,称为回查 -Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 +Broker 服务端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 @@ -4447,7 +4447,7 @@ public class TransactionListenerImpl implements TransactionListener { } ``` -使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后,需要根据执行结果对消息队列进行回复 ```java public class Producer { @@ -4787,7 +4787,7 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载均衡和 Consumer 端订阅消息的负载均衡 -Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 selectOneMessageQueue() 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 +Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 `selectOneMessageQueue()` 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: @@ -4798,7 +4798,7 @@ Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublish * 如果开启,会在随机递增取模的基础上,再过滤掉 not available 的 Broker 代理 * 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 -latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L +LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L @@ -5695,7 +5695,7 @@ NettyRemotingAbstract#processRequestCommand:**处理请求的数据** `DefaultRequestProcessor.processRequest`:**根据业务码处理请求,执行对应的操作** - `ClientRemotingProcessor.processRequest`:处理回退消息,需要消费者回执一条消息给生产者 + `ClientRemotingProcessor.processRequest`:处理事务回查消息,或者回执消息,需要消费者回执一条消息给生产者 * `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 @@ -6264,9 +6264,22 @@ CommitLog 类核心方法: * `this.defaultMessageStore.doDispatch(dispatchRequest)`:重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐 * 剩余逻辑与正常关机的恢复方法相似 -消息追加服务 DefaultAppendMessageCallback + + +*** + + + +##### 服务线程 + +AppendMessageCallback 消息追加服务实现类为 DefaultAppendMessageCallback * doAppend() + + ```java + public AppendMessageResult doAppend() + ``` + * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset * `String msgId`:消息 ID,规则是客户端 IP + 消息偏移量 phyOffset * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 @@ -6274,6 +6287,71 @@ CommitLog 类核心方法: * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 * `CommitLog.this.topicQueueTable.put(key, ++queueOffset)`:更新队列偏移量 +FlushRealTimeService 刷盘 CL 数据,默认是异步刷盘类 FlushRealTimeService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + +同步刷盘类 GroupCommitService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + `this.waitForRunning(10)`:线程休眠 10 毫秒,最后调用 `onWaitEnd()` 进行请求的交换 `swapRequests()` + + `this.doCommit()`:做提交逻辑 + + * `if (!this.requestsRead.isEmpty()) `:读请求集合不为空 + + `for (GroupCommitRequest req : this.requestsRead)`:遍历所有的读请求,请求中的属性: + + * `private final long nextOffset`:本条消息存储之后,下一条消息开始的 offset + * `private CompletableFuture flushOKFuture`:Future 对象 + + `boolean flushOK = ...`:当前请求关注的数据是否全部落盘,**落盘成功唤醒消费者线程** + + `for (int i = 0; i < 2 && !flushOK; i++)`:尝试进行两次强制刷盘,保证刷盘成功 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + `req.wakeupCustomer(flushOK ? ...)`:设置 Future 结果,在 Future 阻塞的线程在这里会被唤醒 + + `this.requestsRead.clear()`:清理 reqeustsRead 列表,方便交换时 成为 requestsWrite 使用 + + * `else`:读请求集合为空 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + * `this.swapRequests()`:交换读写请求 + + * `this.doCommit()`:交换后做一次提交 + **** @@ -6594,136 +6672,189 @@ IndexService 类用来管理 IndexFile 文件 -#### MesStore +#### HAService -##### 生命周期 +##### HAService -DefaultMessageStore 类核心是整个存储服务的调度类 +###### Service -* 构造方法: +HAService 类成员变量: + +* 主节点属性: ```java - public DefaultMessageStore() + // master 节点当前有多少个 slave 节点与其进行数据同步 + private final AtomicInteger connectionCount = new AtomicInteger(0); + // master 节点会给每个发起连接的 slave 节点的通道创建一个 HAConnection,控制 master 端向 slave 端传输数据 + private final List connectionList = new LinkedList<>(); + // master 向 slave 节点推送的最大的 offset,表示数据同步的进度 + private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0) ``` - * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** - * `this.indexService.start()`:启动索引服务 - -* load():加载资源 +* 内部类属性: ```java - public boolean load() + // 封装了绑定服务器指定端口,监听 slave 的连接的逻辑,没有使用 Netty,使用了原生态的 NIO 去做 + private final AcceptSocketService acceptSocketService; + // 控制生产者线程阻塞等待的逻辑 + private final GroupTransferService groupTransferService; + // slave 节点的客户端对象,【slave 端才会正常运行该实例】 + private final HAClient haClient; ``` - * `this.commitLog.load()`:先加载 CommitLog - * `this.loadConsumeQueue()`:再加载 ConsumeQueue - * `this.storeCheckpoint`:检查位点对象 - * `this.indexService.load(lastExitOK)`:加载 IndexFile - * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL - -* start():核心启动方法 +* 线程通信对象: ```java - public void start() + private final WaitNotifyObject waitNotifyObject = new WaitNotifyObject() ``` - * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker - - * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 - - * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile +成员方法: - * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 +* start():启动高可用服务 - * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 + ```java + public void start() throws Exception { + // 监听从节点 + this.acceptSocketService.beginAccept(); + // 启动监听服务 + this.acceptSocketService.start(); + // 启动转移服务 + this.groupTransferService.start(); + // 启动从节点客户端实例 + this.haClient.start(); + } + ``` - * `this.haService.start()`:启动 **HA 服务** - * `this.handleScheduleMessageService()`:启动**消息调度服务** - * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** +**** - * `this.commitLog.start()`:启动 **CL 刷盘服务** - * `this.storeStatsService.start()`:启动状态存储服务 - * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** +###### Accept - * `this.addScheduleTask()`:添加定时任务 +AcceptSocketService 类用于监听从节点的连接,创建 HAConnection 连接对象 - * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 +成员变量: - * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 - * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 +* 端口信息:Master 绑定监听的端口信息 - * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 + ```java + private final SocketAddress socketAddressListen; + ``` - * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 +* 服务端通道: - * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% + ```java + private ServerSocketChannel serverSocketChannel; + ``` - `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 +* 多路复用器: - * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 + ```java + private Selector selector; + ``` - * `this.shutdown = false`:刚启动,设置为 false +成员方法: -* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 +* beginAccept():开始监听连接,**NIO** 标准模板 ```java - public void shutdown() + public void beginAccept() ``` -* destroy():销毁 Broker 的工作目录 + * `this.serverSocketChannel = ServerSocketChannel.open()`:获取服务端 SocketChannel + * `this.selector = RemotingUtil.openSelector()`:获取多路复用器 + * `this.serverSocketChannel.socket().setReuseAddress(true)`:开启通道可重用 + * `this.serverSocketChannel.socket().bind(this.socketAddressListen)`:绑定连接端口 + * `this.serverSocketChannel.configureBlocking(false)`:设置非阻塞 + * `this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT)`:将通道注册到多路复用器上,关注 `OP_ACCEPT` 事件 + +* run():服务启动 ```java - public void destroy() + public void run() ``` + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟 + * `Set selected = this.selector.selectedKeys()`:获取选择器中所有注册的通道中已经就绪好的事件 + * `for (SelectionKey k : selected)`:遍历所有就绪的事件 + * `if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0)`:说明 `OP_ACCEPT` 事件就绪 + * `SocketChannel sc = ((ServerSocketChannel) k.channel()).accept()`:**获取到客户端连接的通道** + * `HAConnection conn = new HAConnection(HAService.this, sc)`:**为每个连接 master 服务器的 slave 创建连接对象** + * `conn.start()`:**启动 HAConnection 对象**,内部启动两个服务为读数据服务、写数据服务 + * `HAService.this.addConnection(conn)`:加入到 HAConnection 集合内 +**** -*** +###### Group -##### 服务线程 +GroupTransferService 用来控制数据同步 -ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 +成员方法: -* run():一般实现方式 +* doWaitTransfer():等待主从数据同步 ```java - public void run() { - while (!this.isStopped()) { - // 业务逻辑 - } - } + private void doWaitTransfer() ``` - 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + * `if (!this.requestsRead.isEmpty())`:读请求不为空 + * `boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset()`:主从同步是否完成 + * `req.wakeupCustomer(transferOK ? ...)`:唤醒消费者 + * `this.requestsRead.clear()`:清空读请求 + + + +**** + + + +##### HAClient + +###### 成员属性 + +HAClient 是 slave 端运行的代码,用于和 master 服务器建立长连接,上报本地同步进度,消费服务器发来的 msg 数据 + +成员变量: + +* 缓冲区: ```java - protected volatile boolean stopped = false + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4; // 默认大小:4 MB + private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); ``` -* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 +* 主节点地址:格式为 `ip:port` ```java - public void shutdown() + private final AtomicReference masterAddress = new AtomicReference<>() ``` -* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false +* NIO 属性: ```java - protected void waitForRunning(long interval) + private final ByteBuffer reportOffset; // 通信使用NIO,所以消息使用块传输,上报 slave offset 使用 + private SocketChannel socketChannel; // 客户端与 master 的会话通道 + private Selector selector; // 多路复用器 ``` -* wakeup():唤醒线程,设置 hasNotified 为 true +* 通信时间:上次会话通信时间,用于控制 socketChannel 是否关闭的 + + ````java + private long lastWriteTimestamp = System.currentTimeMillis(); + ```` + +* 进度信息: ```java - public void wakeup() + private long currentReportedOffset = 0; // slave 当前的进度信息 + private int dispatchPosition = 0; // 控制 byteBufferRead position 指针 ``` @@ -6732,151 +6863,710 @@ ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象 -##### 构建服务 - -AllocateMappedFileService 创建 MappedFile 服务 +###### 成员方法 -* mmapOperation():核心服务 +* run():启动方法 ```java - private boolean mmapOperation() + public void run() ``` - * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 - * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 - * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 - * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 - * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 - * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** + * `if (this.connectMaster())`:连接主节点,连接失败会休眠 5 秒 -* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 + * `String addr = this.masterAddress.get()`:获取 master 暴露的 HA 地址端口信息 + * `this.socketChannel = RemotingUtil.connect(socketAddress)`:建立连接 + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:注册到多路复用器,**关注读事件** + * `this.currentReportedOffset`: 初始化上报进度字段为 slave 的 maxPhyOffset - ```java - public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) - ``` + * `if (this.isTimeToReportOffset())`:slave 每 5 秒会上报一次 slave 端的同步进度信息给 master - * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 - * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 - * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 - * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + `boolean result = this.reportSlaveMaxOffset()`:上报同步信息,上报失败关闭连接 -ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,**获取到就绪事件或者超时后结束** -* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 + * `boolean ok = this.processReadEvent()`:处理读事件 + + * `if (!reportSlaveMaxOffsetPlus())`:检查是否重新上报同步进度 + +* reportSlaveMaxOffset():上报 slave 同步进度 ```java - public void run() + private boolean reportSlaveMaxOffset(final long maxOffset) ``` -* doReput():实现分发的核心逻辑 + * 首先向缓冲区写入 slave 端最大偏移量,写完以后切换为指定置为初始状态 + + * `for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++)`:尝试三次写数据 + + `this.socketChannel.write(this.reportOffset)`:**写数据** + + * `return !this.reportOffset.hasRemaining()`:写成功之后 pos = limit + +* processReadEvent():处理 master 发送给 slave 数据,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 haClient ```java - private void doReput() + private boolean processReadEvent() ``` - * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 - * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 - * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 - * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** - * `this.reputFromOffset += size`:更新数据范围 + * `int readSizeZeroTimes = 0`:控制 while 循环的一个条件变量,当值为 3 时跳出循环 + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** -*** + * `if (readSize > 0)`:加载成功,有新数据 + `readSizeZeroTimes = 0`:置为 0 + `boolean result = this.dispatchReadRequest()`:处理数据的核心逻辑 -##### 刷盘服务 + * `else if (readSize == 0) `:无新数据 -FlushConsumeQueueService 刷盘 CQ 数据 + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 -* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + +* dispatchReadRequest():**处理数据的核心逻辑**,master 与 slave 传输的数据格式 `{[phyOffset][size][data...]}`,phyOffset 表示数据区间的开始偏移量,data 代表数据块,最大 32kb,可能包含多条消息的数据 ```java - public void run() + private boolean dispatchReadRequest() ``` -* doFlush():刷盘 + * `final int msgHeaderSize = 8 + 4`:协议头大小 12 - ```java - private void doFlush(int retryTimes) - ``` + * `int readSocketPos = this.byteBufferRead.position()`:记录缓冲区处理数据前的 pos 位点,用于恢复指针 - * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + * `int diff = ...`:当前 byteBufferRead 还剩多少 byte 未处理,每处理一条帧数据都会更新 dispatchPosition - * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 - * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 - * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 - * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + * `if (diff >= msgHeaderSize)`:缓冲区还有完整的协议头 header 数据 -FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + * `long masterPhyOffset, int bodySize`:读取 header 信息 -* run():运行方法 + * `long slavePhyOffset`:获取 slave 端最大的物理偏移量 - ```java - public void run() - ``` + * `if (slavePhyOffset != masterPhyOffset)`:正常情况两者是相等的,因为是一帧一帧同步的 - * `while (!this.isStopped())`:stopped为 true 才跳出循环 - - * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 - - * `int interval`:获取配置中的刷盘时间间隔 - - * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 - - * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 - - * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 - - * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** - - * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, - - `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + * `if (diff >= (msgHeaderSize + bodySize))`:说明**缓冲区内是包含当前帧的全部数据的**,开始处理帧数据 + `byte[] bodyData = new byte[bodySize]`:提取帧内的 body 数据 + `this.byteBufferRead.position(this.dispatchPosition + msgHeaderSize)`:**设置 pos 为当前帧的 body 起始位置** -*** + `this.byteBufferRead.get(bodyData)`:读取数据到 bodyData + `HAService...appendToCommitLog(masterPhyOffset, bodyData)`:存储数据到 CommitLog + `this.byteBufferRead.position(readSocketPos)`:恢复 byteBufferRead 的 pos 指针 -##### 清理服务 + `this.dispatchPosition += msgHeaderSize + bodySize`:**加一帧数据长度** -CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 + `if (!reportSlaveMaxOffsetPlus())`:上报 slave 同步信息 -* run():运行方法 + * `if (!this.byteBufferRead.hasRemaining())`:缓冲区写满了 - ```java - public void run() - ``` + `this.reallocateByteBuffer()`:重新分配缓冲区 -* deleteExpiredFiles():删除过期 CL 文件 +* reallocateByteBuffer():重新分配缓冲区 ```java - private void deleteExpiredFiles() + private void reallocateByteBuffer() ``` - * `long fileReservedTime`:默认 72,代表文件的保留时间 - * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 - * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% - * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 - * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 - * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + * `int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition`:表示缓冲区尚未处理过的字节数量 -CleanConsumeQueueService 清理过期的 CQ 数据 + * `if (remain > 0)`:条件成立,说明缓冲区**最后一帧数据是半包数据**,但是不能丢失数据 -* run():运行方法 + `this.byteBufferBackup.put(this.byteBufferRead)`:**将半包数据拷贝到 backup 缓冲区** - ```java - public void run() - ``` + * `this.swapByteBuffer()`:交换 backup 成为 read -* deleteExpiredFiles():删除过期 CQ 文件 + * `this.byteBufferRead.position(remain)`:设置 pos 为 remain ,后续加载数据 pos 从remain 开始向后移动 - ```java - private void deleteExpiredFiles() - ``` + * `this.dispatchPosition = 0`:当前缓冲区交换之后,相当于是一个全新的 byteBuffer,所以分配指针归零 + + + +*** + + + +##### HAConn + +###### Connection + +HAConnection 类成员变量: + +* 会话通道:master 和 slave 之间通信的 SocketChannel + + ```java + private final SocketChannel socketChannel; + ``` + +* 客户端地址: + + ```java + private final String clientAddr; + ``` + +* 服务类: + + ```java + private WriteSocketService writeSocketService; // 写数据服务 + private ReadSocketService readSocketService; // 读数据服务 + ``` + +* 请求位点:在 slave上报本地的进度之后被赋值,该值大于 0 后同步逻辑才会运行,master 如果不知道 slave 节点当前消息的存储进度,就无法给 slave 推送数据 + + ```java + private volatile long slaveRequestOffset = -1; + ``` + +* 应答位点: 保存最新的 slave 上报的 offset 信息,slaveAckOffset 之前的数据都可以认为 slave 已经同步完成 + + ```java + private volatile long slaveAckOffset = -1; + ``` + +核心方法: + +* 构造方法: + + ```java + public HAConnection(final HAService haService, final SocketChannel socketChannel) { + // 初始化一些东西 + // 设置 socket 读写缓冲区为 64kb 大小 + this.socketChannel.socket().setReceiveBufferSize(1024 * 64); + this.socketChannel.socket().setSendBufferSize(1024 * 64); + // 创建读写服务 + this.writeSocketService = new WriteSocketService(this.socketChannel); + this.readSocketService = new ReadSocketService(this.socketChannel); + // 自增 + this.haService.getConnectionCount().incrementAndGet(); + } + ``` + +* 启动方法: + + ```java + public void start() { + this.readSocketService.start(); + this.writeSocketService.start(); + } + ``` + + + +*** + + + +###### ReadSocket + +ReadSocketService 类是一个任务对象,slave 向 master 传输的帧格式为 `[long][long][long]`,上报的是 slave 本地的同步进度,同步进度是一个 long 值 + +成员变量: + +* 读缓冲: + + ```java + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024; // 默认大小 1MB + private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:缓冲区处理位点 + + ```java + private int processPosition = 0; + ``` + +* 上次读操作的时间: + + ```java + private volatile long lastReadTimestamp = System.currentTimeMillis(); + ``` + +核心方法: + +* 构造方法: + + ```java + public ReadSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:通道注册到多路复用器,关注读事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `boolean ok = this.processReadEvent()`:**读数据的核心方法**,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 HAConnection 对象 + + * `int readSizeZeroTimes = 0`:控制 while 循环,当连续从 Socket 读取失败 3 次(未加载到数据)跳出循环 + + * `if (!this.byteBufferRead.hasRemaining())`:byteBufferRead 已经全部使用完,需要清理数据并更新位点 + + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** + + * `if (readSize > 0)`:加载成功,有新数据 + + `readSizeZeroTimes = 0`:置为 0 + + `if ((byteBufferRead.position() - processPosition) >= 8)`:缓冲区的可读数据最少包含一个数据帧 + + * `int pos = ...`:**获取可读帧数据中最后一个完整的帧数据的位点**,后面的数据丢弃 + * `long readOffset = ...byteBufferRead.getLong(pos - 8)`:读取最后一帧数据,slave 端当前的同步进度信息 + * `this.processPosition = pos`:更新处理位点 + * `HAConnection.this.slaveAckOffset = readOffset`:更新应答位点 + * `if (HAConnection.this.slaveRequestOffset < 0)`:条件成立**给 slaveRequestOffset 赋值** + * `HAConnection...notifyTransferSome(slaveAckOffset)`:**唤醒阻塞的生产者线程** + + * `else if (readSize == 0) `:无新数据 + + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + + * `if (interval > 20)`:超过 20 秒未发生通信,直接结束循环 + + + +*** + + + +###### WriteSocket + +WriteSocketService 类是一个任务对象,master向 slave 传输的数据帧格式为 `{[phyOffset][size][data...]}{[phyOffset][size][data...]}`,上报的是 slave 本地的同步进度,同步进度是一个 long 值 + +* phyOffset:数据区间的开始偏移量,并不表示某一条具体的消息,表示的数据块开始的偏移量位置 +* size:同步的数据块的大小 +* data:数据块,最大 32kb,可能包含多条消息的数据 + +成员变量: + +* 协议头: + + ```java + private final int headerSize = 8 + 4; // 协议头大小:12 + private final ByteBuffer byteBufferHeader; // 帧头缓冲区 + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:下一次传输同步数据的位置信息,master 给当前 slave 同步的位点 + + ```java + private long nextTransferFromWhere = -1; + ``` + +* 上次写操作: + + ```java + private boolean lastWriteOver = true; // 上一轮数据是否传输完毕 + private long lastWriteTimestamp = System.currentTimeMillis(); // 上次写操作的时间 + ``` + +核心方法: + +* 构造方法: + + ```java + public WriteSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_WRITE)`:通道注册到多路复用器,关注写事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `if (-1 == HAConnection.this.slaveRequestOffset)`:**等待 slave 同步完数据** + + * `if (-1 == this.nextTransferFromWhere)`:条件成立,需要初始化该变量 + + `if (0 == HAConnection.this.slaveRequestOffset)`:slave 是一个全新节点,从正在顺序写的 MF 开始同步数据 + + `long masterOffset = ...`:获取 master 最大的 offset,并计算归属的 mappedFile 文件的开始 offset + + `this.nextTransferFromWhere = masterOffset`:**赋值给下一次传输同步数据的位置信息** + + `this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset`:大部分情况走这个赋值逻辑 + + * `if (this.lastWriteOver)`:上一次待发送数据全部发送完成 + + `if (interval > 5)`:超过 5 秒未同步数据,发送一个 header 数据包,维持长连接 + + * `else`:上一轮的待发送数据未全部发送,需要同步数据到 slave 节点 + + * `SelectMappedBufferResult selectResult`:到 CommitLog 中查询 nextTransferFromWhere 开始位置的数据 + + * `if (size > 32k)`:**一次最多同步 32k 数据** + + * `this.nextTransferFromWhere += size`:增加 size,下一轮传输跳过本帧数据 + + * `selectResult.getByteBuffer().limit(size)`:设置 byteBuffer 可访问数据区间为 [pos, size] + + * `this.selectMappedBufferResult = selectResult`:**待发送的数据** + + * `this.byteBufferHeader.put`:**构建帧头数据** + + * `this.lastWriteOver = this.transferData()`:处理数据,返回是否处理完成 + +* 同步方法:**同步数据到 slave 节点**,返回 true 表示本轮数据全部同步完成,false 表示本轮同步未完成(Header 和 Body 其中一个未同步完成都会返回 false) + + ```java + private boolean transferData() + ``` + + * `int writeSizeZeroTimes= 0`:控制 while 循环,当写失败连续 3 次时,跳出循环)跳出循环 + + * `while (this.byteBufferHeader.hasRemaining())`:**帧头数据缓冲区有待发送的数据** + + * `int writeSize = this.socketChannel.write(this.byteBufferHeader)`:向通道写帧头数据 + + * `if (writeSize > 0)`:写数据成功 + + `writeSizeZeroTimes = 0`:控制变量置为 0 + + * `else if (readSize == 0)`:写失败 + + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `if (null == this.selectMappedBufferResult)`:说明是心跳数据,返回心跳数据是否发送完成 + + * `writeSizeZeroTimes = 0`:控制变量置为 0 + + * `if (!this.byteBufferHeader.hasRemaining())`:**Header写成功之后,才进行写 Body** + + * `while (this.selectMappedBufferResult.getByteBuffer().hasRemaining())`:**数据缓冲区有待发送的数据** + + * `int writeSize = this.socketChannel.write(this.selectMappedBufferResult...)`:向通道写帧头数据 + + * `if (writeSize > 0)`:写数据成功,但是不代表 SMBR 中的数据全部写完成 + + `writeSizeZeroTimes = 0`:控制变量置为 0 + + * `else if (readSize == 0)`:写失败,因为 Socket 写缓冲区写满了 + + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `boolean result`:判断是否发送完成,返回该值 + + + + + +**** + + + +#### MesStore + +##### 生命周期 + +DefaultMessageStore 类核心是整个存储服务的调度类 + +* 构造方法: + + ```java + public DefaultMessageStore() + ``` + + * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** + * `this.indexService.start()`:启动索引服务 + +* load():加载资源 + + ```java + public boolean load() + ``` + + * `this.commitLog.load()`:先加载 CommitLog + * `this.loadConsumeQueue()`:再加载 ConsumeQueue + * `this.storeCheckpoint`:检查位点对象 + * `this.indexService.load(lastExitOK)`:加载 IndexFile + * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL + +* start():核心启动方法 + + ```java + public void start() + ``` + + * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker + + * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 + + * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile + + * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 + + * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 + + * `this.haService.start()`:启动 **HA 服务** + + * `this.handleScheduleMessageService()`:启动**消息调度服务** + + * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** + + * `this.commitLog.start()`:启动 **CL 刷盘服务** + + * `this.storeStatsService.start()`:启动状态存储服务 + + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** + + * `this.addScheduleTask()`:添加定时任务 + + * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 + + * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 + * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 + + * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 + + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 + + * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% + + `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 + + * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 + + * `this.shutdown = false`:刚启动,设置为 false + +* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 + + ```java + public void shutdown() + ``` + +* destroy():销毁 Broker 的工作目录 + + ```java + public void destroy() + ``` + + + + + +*** + + + +##### 服务线程 + +ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 + +* run():一般实现方式 + + ```java + public void run() { + while (!this.isStopped()) { + // 业务逻辑 + } + } + ``` + + 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + + ```java + protected volatile boolean stopped = false + ``` + +* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 + + ```java + public void shutdown() + ``` + +* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false + + ```java + protected void waitForRunning(long interval) + ``` + +* wakeup():唤醒线程,设置 hasNotified 为 true + + ```java + public void wakeup() + ``` + + + +*** + + + +##### 构建服务 + +AllocateMappedFileService 创建 MappedFile 服务 + +* mmapOperation():核心服务 + + ```java + private boolean mmapOperation() + ``` + + * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 + * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 + * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 + * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 + * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 + * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** + +* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 + + ```java + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) + ``` + + * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 + * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 + * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 + * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + +ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + +* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 + + ```java + public void run() + ``` + +* doReput():实现分发的核心逻辑 + + ```java + private void doReput() + ``` + + * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 + * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 + * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 + * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** + * `this.reputFromOffset += size`:更新数据范围 + + + +*** + + + +##### 刷盘服务 + +FlushConsumeQueueService 刷盘 CQ 数据 + +* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + + ```java + public void run() + ``` + +* doFlush():刷盘 + + ```java + private void doFlush(int retryTimes) + ``` + + * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + + * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 + * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 + * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 + * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + +FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + + + +*** + + + +##### 清理服务 + +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CL 文件 + + ```java + private void deleteExpiredFiles() + ``` + + * `long fileReservedTime`:默认 72,代表文件的保留时间 + * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 + * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% + * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 + * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 + * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + +CleanConsumeQueueService 清理过期的 CQ 数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CQ 文件 + + ```java + private void deleteExpiredFiles() + ``` * `int deleteLogicsFilesInterval`:清理 CQ 的时间间隔,默认 100 毫秒 * `long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()`:获取 CL 文件中最小的物理偏移量 @@ -6894,7 +7584,7 @@ CleanConsumeQueueService 清理过期的 CQ 数据 ##### 获取消息 -PullMessageProcessor#processRequest 方法中调用 getMessage 用于获取消息(提示:建议学习消费者源码时再阅读) +DefaultMessageStore#getMessage 用于获取消息,在 PullMessageProcessor#processRequest 方法中被调用 (提示:建议学习消费者源码时再阅读) ```java // offset: 客户端拉消息使用位点; maxMsgNums: 32; messageFilter: 一般这里是 tagCode 过滤 @@ -7024,8 +7714,6 @@ BrokerController#start:核心启动方法 - - **** @@ -8163,9 +8851,223 @@ DeliverDelayedMessageTimerTask 是一个任务类 +**** + + + +#### 事务消息 + +##### 生产者类 + +TransactionMQProducer 类发送事务消息时使用 + +成员变量: + +* 事务回查线程池资源: + + ```java + private ExecutorService executorService; + +* 事务监听器: + + ```java + private TransactionListener transactionListener; + ``` + +核心方法: + +* start():启动方法 + + ```java + public void start() + ``` + + * `this.defaultMQProducerImpl.initTransactionEnv()`:初始化生产者实例和回查线程池资源 + * `super.start()`:启动生产者实例 + +* sendMessageInTransaction():发送事务消息 + + ```java + public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) { + msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic())); + // 调用实现类的发送方法 + return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg); + } + ``` + + * `TransactionListener transactionListener = getCheckListener()`:获取监听器 + + * `if (null == localTransactionExecuter && null == transactionListener)`:两者都为 null 抛出异常 + + * `Validators.checkMessage(msg, this.defaultMQProducer)`:检查消息 + + * `MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true")`:**设置事务标志** + + * `sendResult = this.send(msg)`:发送消息 + + * `switch (sendResult.getSendStatus())`:**判断发送消息的结果状态** + + * `case SEND_OK`:消息发送成功 + + `msg.setTransactionId(transactionId)`:设置事务 ID 为消息的 UNIQ_KEY 属性 + + `localTransactionState = ...executeLocalTransactionBranch(msg, arg)`:**执行本地事务** + + * `case SLAVE_NOT_AVAILABLE`:其他情况都需要回滚事务 + + `localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE`:**事务状态设置为回滚** + * `this.endTransaction(sendResult, ...)`:结束事务 + * `EndTransactionRequestHeader requestHeader`:构建事务结束头对象 + * `this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway()`:向 Broker 发起事务结束的单向请求 + + + +*** + + + +##### 回查处理 + +ClientRemotingProcessor 用于处理到客户端的请求,创建 MQClientAPIImpl 时将该处理器注册到 Netty 中,`processRequest()` 方法根据请求的命令码,进行不同的处理,事务回查的处理命令码为 `CHECK_TRANSACTION_STATE` + +成员方法: + +* checkTransactionState():检查事务状态 + + ```java + public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, RemotingCommand request) + ``` + + * `final CheckTransactionStateRequestHeader requestHeader`:解析出请求头对象 + * `final MessageExt messageExt`:从请求 body 中解析出服务器回查的事务消息 + * `String transactionId`:提取 UNIQ_KEY 字段属性值赋值给事务 ID + * `final String group`:提取生产者组名 + * `MQProducerInner producer = this...selectProducer(group)`:根据生产者组获取生产者对象 + * `String addr = RemotingHelper.parseChannelRemoteAddr()`:解析出要回查的 Broker 服务器的地址 + * `producer.checkTransactionState(addr, messageExt, requestHeader)`:生产者的事务回查 + * `Runnable request = new Runnable()`:**创建回查事务状态任务对象** + * 获取生产者的 TransactionCheckListener 和 TransactionListener,选择一个不为 null 的监听器进行事务状态回查 + * `this.processTransactionState()`:处理回查状态 + * `EndTransactionRequestHeader thisHeader`:构建 EndTransactionRequestHeader 对象 + * `DefaultMQProducerImpl...endTransactionOneway()`:向 Broker 发起结束事务单向请求,**二阶段提交** + * `this.checkExecutor.submit(request)`:提交到线程池运行 + + + +参考图:https://www.processon.com/view/link/61c8257e0e3e7474fb9dcbc0 + +参考视频:https://space.bilibili.com/457326371 + + + +*** + + + +##### 接受消息 + +SendMessageProcessor 是服务端处理客户端发送来的消息的处理器,`processRequest()` 方法处理请求 + +核心方法: + +* `asyncProcessRequest()`:处理请求 + + ```java + public CompletableFuture asyncProcessRequest(ChannelHandlerContext ctx, + RemotingCommand request) { + final SendMessageContext mqtraceContext; + switch (request.getCode()) { + // 回调消息回退 + case RequestCode.CONSUMER_SEND_MSG_BACK: + return this.asyncConsumerSendMsgBack(ctx, request); + default: + // 解析出请求头对象 + SendMessageRequestHeader requestHeader = parseRequestHeader(request); + if (requestHeader == null) { + return CompletableFuture.completedFuture(null); + } + // 创建上下文对象 + mqtraceContext = buildMsgContext(ctx, requestHeader); + // 前置处理器 + this.executeSendMessageHookBefore(ctx, request, mqtraceContext); + // 判断是否是批量消息 + if (requestHeader.isBatch()) { + return this.asyncSendBatchMessage(ctx, request, mqtraceContext, requestHeader); + } else { + return this.asyncSendMessage(ctx, request, mqtraceContext, requestHeader); + } + } + } + ``` + +* asyncSendMessage():异步处理发送消息 + + ```java + private CompletableFuture asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request, SendMessageContext mqtraceContext, SendMessageRequestHeader requestHeader) + ``` + + * `RemotingCommand response`:创建响应对象 + + * `SendMessageResponseHeader responseHeader`:获取响应头,此时为 null + + * `byte[] body = request.getBody()`:获取请求体 + + * `MessageExtBrokerInner msgInner = new MessageExtBrokerInner()`:创建 msgInner 对象,并赋值相关的属性,主题和队列 ID 都是请求头中的 + + * `String transFlag`:获取**事务属性** + + * `if (transFlag != null && Boolean.parseBoolean(transFlag))`:判断事务属性是否是 true,走事务消息的存储流程 + + * `putMessageResult = ...asyncPrepareMessage(msgInner)`:事务消息处理流程 + + ```java + public CompletableFuture asyncPutHalfMessage(MessageExtBrokerInner messageInner) { + // 调用存储模块,将修改后的 msg 存储进 Broker + return store.asyncPutMessage(parseHalfMessageInner(messageInner)); + } + ``` + + TransactionalMessageBridge#parseHalfMessageInner: + + * `MessageAccessor.putProperty(...)`:将消息的原主题和队列 ID 放入消息的属性中 + * `msgInner.setSysFlag(...)`:消息设置为非事务状态 + * `msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic())`:**消息主题设置为半消息主题** + * `msgInner.setQueueId(0)`:**队列 ID 设置为 0** + + * `else`:普通消息存储 + + + +*** + + + +##### 事务提交 + +EndTransactionProcessor 类用来处理客户端发来的提交或者回滚请求 + +* processRequest():处理请求 + + ```java + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + ``` + * `EndTransactionRequestHeader requestHeader`:从请求中解析出 EndTransactionRequestHeader + * `result = this.brokerController...commitMessage(requestHeader)`:根据 commitLogOffset 提取出 halfMsg 消息 + * `MessageExtBrokerInner msgInner`:根据 result 克隆出一条新消息 + * `msgInner.setTopic(msgExt.getUserProperty(...))`:**设置回原主题** + * `msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))`:**设置回原队列 ID** + * `MessageAccessor.clearProperty()`:清理上面的两个属性 + * `MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED)`:清理事务属性 + * `RemotingCommand sendResult = sendFinalMessage(msgInner)`:调用存储模块存储至 Broker + * `this.brokerController...deletePrepareMessage(result.getPrepareMessage())`:向删除(OP)队列添加消息,消息体的数据是 halfMsg 的 queueOffset,表示半消息队列指定的 offset 的消息已被删除 + * `if (this...putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG))`:**添加一条 OP 数据** + * `MessageQueue messageQueue`:新建一个消息队列,OP 队列 + * `return addRemoveTagInTransactionOp(messageExt, messageQueue)`:添加数据 + * `Message message`:创建消息 + * `writeOp(message, messageQueue)`:写入消息 From 29d649175caf40b0ec337619219737713ce46cf4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 1 Feb 2022 22:07:40 +0800 Subject: [PATCH 19/78] Update Java Note --- Frame.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Frame.md b/Frame.md index cec0168..5cf9cd6 100644 --- a/Frame.md +++ b/Frame.md @@ -10314,16 +10314,3 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl - - - -*** - - - - - -## TEST - - - From 83dba99c2261bc88b2a549fe9f90a58976b516f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 12 Feb 2022 14:23:24 +0800 Subject: [PATCH 20/78] Update Java Note --- Tool.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Tool.md b/Tool.md index d81b3ce..77b4fef 100644 --- a/Tool.md +++ b/Tool.md @@ -2210,7 +2210,7 @@ pstree -A #查看所有进程树 -### 进程ID +### 进程 ID 进程号: @@ -2218,11 +2218,14 @@ pstree -A #查看所有进程树 * 进程号为 1 是 init 进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 -父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 +父进程 ID 为 0 的进程通常是内核进程,作为系统**自举过程**的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 -主存 = RAM + BIOS 部分的 ROM +* 主存 = RAM + BIOS 部分的 ROM +* DISK:存放 OS 和 Bootloader +* BIOS:基于 I/O 处理系统 +* Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) @@ -2277,7 +2280,7 @@ pstree -A #查看所有进程树 - 得到 SIGCHLD 信号 - waitpid() 或者 wait() 调用会返回 -子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息 +子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时进程描述符不会立即释放,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息,释放子进程的 PCB From cac33c723439189b778f4a61e81ecce5491e116f Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 13 Feb 2022 18:27:23 +0800 Subject: [PATCH 21/78] Update Java Note --- DB.md | 11 ++++++----- Java.md | 4 ++-- Prog.md | 8 ++++---- Tool.md | 21 ++++++++------------- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/DB.md b/DB.md index 61b9c13..f51e5c2 100644 --- a/DB.md +++ b/DB.md @@ -5644,7 +5644,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 - 查看事务提交方式 ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 + SELECT @@AUTOCOMMIT; -- 会话,1 代表自动提交 0 代表手动提交 + SELECT @@GLOBAL.AUTOCOMMIT; -- 系统 ``` - 修改事务提交方式 @@ -5732,8 +5733,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 * 查询数据库隔离级别 ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; + SELECT @@TX_ISOLATION; -- 会话 + SELECT @@GLOBAL.TX_ISOLATION; -- 系统 ``` * 修改数据库隔离级别 @@ -5758,7 +5759,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR -* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,加了写锁后其他事务就无法修改数据,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 * Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 @@ -6660,7 +6661,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会加 MDL 读锁),通过 MVCC 防止冲突 +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 锁的兼容性: diff --git a/Java.md b/Java.md index b932420..91ff8a8 100644 --- a/Java.md +++ b/Java.md @@ -12677,7 +12677,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** -字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以 C 执行效率比 Java 高 +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html @@ -13860,7 +13860,7 @@ public static int invoke(Object... args) { - 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 -- **Java 是静态类型语言**(尽管 lambda 表达式为其增加了动态特性),js,python是动态类型语言 +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 ```java String s = "abc"; //Java diff --git a/Prog.md b/Prog.md index dc6f5e3..c09c693 100644 --- a/Prog.md +++ b/Prog.md @@ -50,8 +50,8 @@ * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 - * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe 文件 - * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持**半双工通信** + * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件,该文件同一时间只允许一个进程访问,所以只支持**半双工通信** + * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信 * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供**全双工通信**,对比管道: * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 @@ -59,9 +59,9 @@ 不同计算机之间的**进程通信**,需要通过网络,并遵守共同的协议,例如 HTTP - * 套接字:与其它通信机制不同的是,它可用于不同机器间的互相通信 + * 套接字:与其它通信机制不同的是,可用于不同机器间的互相通信 -* 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 +* 线程通信相对简单,因为线程之间共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 **Java 中的通信机制**:volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer diff --git a/Tool.md b/Tool.md index 77b4fef..bf5c965 100644 --- a/Tool.md +++ b/Tool.md @@ -1614,19 +1614,14 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 * -v:显示不包含匹配文本的所有行 * --color=auto :可以将找到的关键词部分加上颜色的显示 -**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理。 +**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理 -`grep aaaa Filename `:显示存在关键字 aaaa 的行 - -`grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 - -`grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 - -`grep -v aaaa Filename`:显示存在关键字aaaa的所有行 - -`ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 - -` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 +* `grep aaaa Filename `:显示存在关键字 aaaa 的行 +* `grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 +* `grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 +* `grep -v aaaa Filename`:显示存在关键字aaaa的所有行 +* `ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 +* ` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 @@ -2225,7 +2220,7 @@ pstree -A #查看所有进程树 * BIOS:基于 I/O 处理系统 * Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),**用来加载操作系统**。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) From e8ee01e5d103823e10507f8aa1f91aec42c1f10d Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 22:35:52 +0800 Subject: [PATCH 22/78] Update Java Note --- DB.md | 8 +- Java.md | 468 ++++++++++++++++++++++++++------------------------------ Prog.md | 2 +- Web.md | 4 +- 4 files changed, 222 insertions(+), 260 deletions(-) diff --git a/DB.md b/DB.md index f51e5c2..bdd5d85 100644 --- a/DB.md +++ b/DB.md @@ -5757,7 +5757,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 * Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 - MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR + MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR * Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 @@ -6135,7 +6135,7 @@ RC、RR 级别下的 InnoDB 快照读区别 - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 给变为当前的事务 id,所以对当前事务就是可见的 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6790,9 +6790,9 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以保护当前记录和前面的间隙 * 加锁遵循左开右闭原则 -* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会对 (10,11] 加锁 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷),锁住索引 11 会对 (10,11] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 diff --git a/Java.md b/Java.md index 91ff8a8..be48f40 100644 --- a/Java.md +++ b/Java.md @@ -104,7 +104,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - char 类型是一个单一的 16 位两个字节的 Unicode 字符 - 最小值是 **`\u0000`**(即为 0) - 最大值是 **`\uffff`**(即为 65535) -- char 数据类型可以**存储任何字符** +- char 数据类型可以存储任何字符 - 例子:`char c = 'A'`,`char c = '张'` 上下转型 @@ -187,8 +187,8 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: * 把字符串类型的数值转换成对应的基本数据类型的值(**重要**) - 1. Xxx.parseXxx("字符串类型的数值") → Integer.parseInt(numStr) - 2. Xxx.valueOf("字符串类型的数值") → Integer.valueOf(numStr) (推荐使用) + 1. Xxx.parseXxx("字符串类型的数值") → `Integer.parseInt(numStr)` + 2. Xxx.valueOf("字符串类型的数值") → `Integer.valueOf(numStr)` (推荐使用) ```java public class PackageClass02 { @@ -219,7 +219,35 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: } ``` - + + + +*** + + + +##### 类型对比 + +* 有了基本数据类型,为什么还要引用数据类型? + + > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 + > + > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 + +* 引用数据类型那么好,为什么还用基本数据类型? + + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 + +* Java 集合不能存放基本数据类型,只存放对象的引用? + + > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) + +* == + + > == 比较基本数据类型:比较的是具体的值 + > == 比较引用数据类型:比较的是对象地址值 + + *** @@ -242,8 +270,8 @@ public class PackegeClass { Integer c = 100 ; int c1 = c ; // 自动拆箱 - Integer it = Integer.valueOf(12); // 手工装箱! - // Integer it1 = new Integer(12); // 手工装箱! + Integer it = Integer.valueOf(12); // 手工装箱! + // Integer it1 = new Integer(12); // 手工装箱! Integer it2 = 12; Integer it3 = 111 ; @@ -257,6 +285,7 @@ public class PackegeClass { ```java public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) + // 【缓存池】,本质上是一个数组 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } @@ -304,10 +333,10 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 - Integer values between -128 and 127 - Character in the range \u0000 to \u007F (0 and 127) -在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 jvm 的时候,通过 AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 +在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 JVM 时通过 `AutoBoxCacheMax=` 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 ```java -Integer x = 100; //自动装箱,底层调用 Integer.valueOf(1) +Integer x = 100; // 自动装箱,底层调用 Integer.valueOf(1) Integer y = 100; System.out.println(x == y); // true @@ -335,8 +364,8 @@ System.out.println(x == y); // true,因为 y 会调用 intValue 自动拆箱 一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 -Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` -print:`PrintStream.write()` +* Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` +* print:`PrintStream.write()` > 使用引用数据类型的API @@ -351,31 +380,6 @@ public static void main(String[] args) { -*** - - - -#### 面试题 - -* 有了基本数据类型,为什么还要引用数据类型? - - > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 - > - > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 - -* 引用数据类型那么好,为什么还用基本数据类型? - - > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 - -* Java 集合不能存放基本数据类型,只存放对象的引用? - - > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) - -* == - - > == 比较基本数据类型:比较的是具体的值 - > == 比较引用数据类型:比较的是对象地址值 - **** @@ -386,12 +390,12 @@ public static void main(String[] args) { #### 初始化 -数组就是存储数据长度固定的容器,存储多个数据的**数据类型要一致**,数组也是一个对象 +数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致,**数组也是一个对象** 创建数组: -* 数据类型[] 数组名:`int[] arr;` (常用) -* 数据类型 数组名[]:`int arr[];` +* 数据类型[] 数组名:`int[] arr` (常用) +* 数据类型 数组名[]:`int arr[]` 静态初始化: @@ -406,9 +410,9 @@ public static void main(String[] args) { #### 元素访问 -* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 -* **访问格式**:数组名[索引] `arr[0]` +* **访问格式**:数组名[索引],`arr[0]` * **赋值:**`arr[0] = 10` @@ -419,19 +423,19 @@ public static void main(String[] args) { #### 内存分配 -内存是计算机中的重要原件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的。必须放进内存中才能运行,运行完毕后会清空内存。 Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。 +内存是计算机中的重要器件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。 Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。 -| 区域名称 | 作用 | -| ---------- | -------------------------------------------------------- | -| 寄存器 | 给CPU使用,和我们开发无关 | -| 本地方法栈 | JVM在使用操作系统功能的时候使用,和我们开发无关 | -| 方法区 | 存储可以运行的class文件 | -| 堆内存 | 存储对象或者数组,new来创建的,都存储在堆内存 | -| 方法栈 | 方法运行时使用的内存,比如main方法运行,进入方法栈中执行 | +| 区域名称 | 作用 | +| ---------- | ---------------------------------------------------------- | +| 寄存器 | 给 CPU 使用 | +| 本地方法栈 | JVM 在使用操作系统功能的时候使用 | +| 方法区 | 存储可以运行的 class 文件 | +| 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | +| 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | -**内存分配图**: +**内存分配图**:Java 内存分配 -* Java内存分配-一个数组内存图 +* 一个数组内存图 ![](https://gitee.com/seazean/images/raw/master/Java/数组内存分配-一个数组内存图.png) @@ -464,7 +468,7 @@ public static void main(String[] args) { } ``` - arr = null,表示变量arr将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。 + arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码。 解决方案:给数组一个真正的堆内存空间引用即可! @@ -482,14 +486,14 @@ public static void main(String[] args) { * 动态初始化: - 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3];` + 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3]` * m 表示这个二维数组,可以存放多少个一维数组,行 * n 表示每一个一维数组,可以存放多少个元素,列 * 静态初始化 * 数据类型[][] 变量名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...} - * 数据类型[][] 变量名 = { {元素1, 元素2...} , {元素1, 元素2...} ...} - * `int[][] arr = {{11,22,33}, {44,55,66}};` + * 数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...} + * `int[][] arr = {{11,22,33}, {44,55,66}}` 遍历: @@ -525,13 +529,13 @@ public class Test1 { ### 运算 -* i++ 与++i 的区别? +* i++ 与 ++i 的区别? i++ 表示先将 i 放在表达式中运算,然后再加 1 ++i 表示先将 i 加 1,然后再放在表达式中运算 * || 和 |,&& 和& 的区别,逻辑运算符 - **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 + **& 和| 称为布尔运算符,位运算符。&& 和 || 称为条件布尔运算符,也叫短路运算符**。 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** @@ -570,14 +574,14 @@ public class Test1 { ``` * 负数: - 原码:最高位为1,其余位置和正数相同 + 原码:最高位为 1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 补码:保证符号位不变,其余位置取反加 1,即反码 +1 ```java - -100原码: 10000000 00000000 00000000 01100100 //32位 - -100反码: 11111111 11111111 11111111 10011011 - -100补码: 11111111 11111111 11111111 10011100 + -100 原码: 10000000 00000000 00000000 01100100 //32位 + -100 反码: 11111111 11111111 11111111 10011011 + -100 补码: 11111111 11111111 11111111 10011100 ``` 补码 → 原码:符号位不变,其余位置取反加 1 @@ -621,13 +625,11 @@ public class Test1 { #### 可变参数 -可变参数用在形参中可以接收多个数据。 +可变参数用在形参中可以接收多个数据,在方法内部**本质上就是一个数组** -可变参数的格式:数据类型... 参数名称 +格式:数据类型... 参数名称 -可变参数的作用:传输参数非常灵活,方便。可以不传输参数、传输一个参数、或者传输一个数组。 - -可变参数在方法内部本质上就是一个数组。 +作用:传输参数非常灵活,方便,可以不传输参数、传输一个参数、或者传输一个数组。 可变参数的注意事项: @@ -667,7 +669,7 @@ public static void sum(int... nums){ 在方法内部定义的叫局部变量,局部变量不能加 static,包括 protected、private、public 这些也不能加 -原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以在局部变量前不能加 static 关键字,静态变量是定义在类中,又叫类变量 +原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以**在局部变量前不能加 static 关键字**,静态变量是定义在类中,又叫类变量 @@ -677,7 +679,7 @@ public static void sum(int... nums){ #### 定义调用 -定义格式 +定义格式: ```java public static 返回值类型 方法名(参数) { @@ -686,11 +688,10 @@ public static 返回值类型 方法名(参数) { } ``` -调用格式 +调用格式: ```java -数据类型 变量名 = 方法名 ( 参数 ) ; -//注意:方法的返回值通常会使用变量接收,否则该返回值将无意义 +数据类型 变量名 = 方法名 (参数) ; ``` * 方法名:调用方法时候使用的标识 @@ -700,10 +701,10 @@ public static 返回值类型 方法名(参数) { 如果方法操作完毕 -* void 类型的方法,直接调用即可,而且方法体中一般不写return +* void 类型的方法,直接调用即可,而且方法体中一般不写 return * 非 void 类型的方法,推荐用变量接收调用 -原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失。 +原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失 @@ -785,9 +786,9 @@ public class MethodDemo { 重载的方法在编译过程中即可完成识别,方法调用时 Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: -* 在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 -* 如果第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 -* 如果第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 +* 一阶段:在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 +* 二阶段:如果第一阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 +* 三阶段:如果第二阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,**一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现**: @@ -796,7 +797,7 @@ public class MethodDemo { void invoke(Object obj, Object... args) { ... } void invoke(String s, Object obj, Object... args) { ... } - invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 + invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 invoke(null, 1, 2); // 调用第二个invoke方法,匹配第一个和第二个,但String是Object的子类 invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,才能调用第一个invoke方法 @@ -932,7 +933,7 @@ Java 的参数是以**值传递**的形式传入方法中 // 获取索引 Season s = Season.SPRING; System.out.println(s); //SPRING - System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 + System.out.println(s.ordinal()); // 0,该值代表索引,summer 就是 1 s.s.doSomething(); // 获取全部枚举 Season[] ss = Season.values(); @@ -988,15 +989,15 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 ### 概述 -**Java是一种面向对象的高级编程语言。** +**Java 是一种面向对象的高级编程语言。** **三大特征:封装,继承,多态** 面向对象最重要的两个概念:类和对象 -* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物 -* 对象:是真实存在的实例, 实例==对象,**对象是类的实例化** -* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象 +* 类:相同事物共同特征的描述,类只是学术上的一个概念并非真实存在的,只能描述一类事物 +* 对象:是真实存在的实例, 实例 == 对象,**对象是类的实例化** +* 结论:有了类和对象就可以描述万千世界所有的事物,必须先有类才能有对象 @@ -1017,7 +1018,7 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode 2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 -3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public修饰的类名必须成为当前Java代码的文件名称** +3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public 修饰的类名必须成为当前 Java 代码的文件名称** ```java 类中的成分:有且仅有五大成分 @@ -1086,6 +1087,7 @@ public class ClassDemo { ### 封装 封装的哲学思维:合理隐藏,合理暴露 + 封装最初的目的:提高代码的安全性和复用性,组件化 封装的步骤: @@ -1120,9 +1122,7 @@ this 关键字的作用: #### 基本介绍 -Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的。 - -static 静态修饰的成员(方法和成员变量)属于类本身的。 +Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的 按照有无 static 修饰,成员变量和方法可以分为: @@ -1140,7 +1140,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 -#### static用法 +#### static 用法 成员变量的访问语法: @@ -1175,8 +1175,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 inAddr(); // b.对象.实例方法 // Student.eat(); // 报错了! - Student zbj = new Student(); - zbj.eat(); + Student sea = new Student(); + sea.eat(); } } ``` @@ -1199,14 +1199,14 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 访问问题: -* 实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象 -* 实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问 -* 实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象 -* 实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问 -* 静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! -* 静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 -* 静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! -* 静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! +* 实例方法是否可以直接访问实例成员变量?可以,因为它们都属于对象 +* 实例方法是否可以直接访问静态成员变量?可以,静态成员变量可以被共享访问 +* 实例方法是否可以直接访问实例方法? 可以,实例方法和实例方法都属于对象 +* 实例方法是否可以直接访问静态方法?可以,静态方法可以被共享访问 +* 静态方法是否可以直接访问实例变量? 不可以,实例变量必须用对象访问!! +* 静态方法是否可以直接访问静态变量? 可以,静态成员变量可以被共享访问。 +* 静态方法是否可以直接访问实例方法? 不可以,实例方法必须用对象访问!! +* 静态方法是否可以直接访问静态方法?可以,静态方法可以被共享访问!! @@ -1220,8 +1220,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 继承是 Java 中一般到特殊的关系,是一种子类到父类的关系 -* 被继承的类称为:父类/超类。 -* 继承父类的类称为:子类。 +* 被继承的类称为:父类/超类 +* 继承父类的类称为:子类 继承的作用: @@ -1235,7 +1235,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 2. **单继承**:一个类只能继承一个直接父类 3. 多层继承:一个类可以间接继承多个父类(家谱) 4. 一个类可以有多个子类 -5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,Object 类是 Java 中的祖宗类 +5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,**Object 类是 Java 中的祖宗类** 继承的格式: @@ -1356,7 +1356,7 @@ class Animal{ -#### 面试问题 +#### 常见问题 * 为什么子类构造器会先调用父类构造器? @@ -1365,17 +1365,18 @@ class Animal{ 3. 参考 JVM → 类加载 → 对象创建 ```java - class Animal{ - public Animal(){ + class Animal { + public Animal() { System.out.println("==父类Animal的无参数构造器=="); } } - class Tiger extends Animal{ - public Tiger(){ + + class Tiger extends Animal { + public Tiger() { super(); // 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的无参数构造器=="); } - public Tiger(String name){ + public Tiger(String name) { //super(); 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的有参数构造器=="); } @@ -1421,8 +1422,8 @@ class Animal{ 总结与拓展: -* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 -* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 +* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)** 可以根据参数匹配访问本类其他构造器 +* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器 **注意:** @@ -1461,7 +1462,7 @@ class Student{ this.age = age; this.schoolName = schoolName; } -// .......get + set + // .......get + set } ``` @@ -1481,7 +1482,7 @@ final 用于修饰:类,方法,变量 * final 可以修饰方法,方法就不能被重写 * final 修饰变量总规则:变量有且仅能被赋值一次 -**面试题**:final 和 abstract 的关系是互斥关系,不能同时修饰类或者同时修饰方法! +final 和 abstract 的关系是**互斥关系**,不能同时修饰类或者同时修饰方法 @@ -1505,7 +1506,7 @@ final 修饰静态成员变量可以在哪些地方赋值: ```java public class FinalDemo { -//常量:public static final修饰,名称字母全部大写,下划线连接。 + //常量:public static final修饰,名称字母全部大写,下划线连接。 public static final String SCHOOL_NAME = "张三" ; public static final String SCHOOL_NAME1; @@ -1565,9 +1566,9 @@ public class FinalDemo { > 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 -抽象方法:没有方法体,只有方法签名,必须用**abstract**修饰的方法就是抽象方法 +抽象方法:没有方法体,只有方法签名,必须用 abstract 修饰的方法就是抽象方法 -抽象类:拥有抽象方法的类必须定义成抽象类,必须用**abstract**修饰,抽象类是为了被继承 +抽象类:拥有抽象方法的类必须定义成抽象类,必须用 abstract 修饰,**抽象类是为了被继承** 一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 @@ -1597,12 +1598,11 @@ abstract class Animal{ -#### 面试问题 +#### 常见问题 一、抽象类是否有构造器,是否可以创建对象? -答:抽象类作为类一定有构造器,而且必须有构造器,提供给子类继承后调用父类构造器使用的 -* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 +* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备,构造器提供给子类继承后调用父类构造器使用 * 抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** > 抽象在学术上本身意味着不能实例化 @@ -1678,9 +1678,9 @@ abstract class Template{ #### 基本介绍 -接口,是 Java 语言中一种引用类型,是方法的集合。 +接口是 Java 语言中一种引用类型,是方法的集合。 -接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分,jdk1.8 前 +接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分 ```java 修饰符 interface 接口名称{ @@ -1691,11 +1691,11 @@ abstract class Template{ } ``` -* 抽象方法:接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写 +* 抽象方法:接口中的抽象方法默认会加上 public abstract 修饰,所以可以省略不写 * 静态方法:静态方法必须有方法体 -* 常量:常量是public static final修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接。public static final可以省略不写。 +* 常量:是 public static final 修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接,public static final 可以省略不写 ```java public interface InterfaceDemo{ @@ -1717,14 +1717,11 @@ abstract class Template{ #### 实现接口 -作用:**接口是用来被类实现的。** +**接口是用来被类实现的。** -类与类是继承关系:一个类只能直接继承一个父类,单继承 -类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 -接口与接口继承关系:**多继承** - ->子类 继承 父类 ->实现类 实现 接口 +* 类与类是继承关系:一个类只能直接继承一个父类,单继承 +* 类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 +* 接口与接口继承关系:**多继承** ```java 修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{ @@ -1741,9 +1738,9 @@ abstract class Template{ 2. 当一个类实现多个接口时,多个接口中存在同名的默认方法,实现类必须重写这个方法 -3. 当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类**就近选择执行父类**的成员方法 +3. 当一个类既继承一个父类,又实现若干个接口时,父类中成员方法与接口中默认方法重名,子类**就近选择执行父类**的成员方法 -4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象!! +4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象 ```java public class InterfaceDemo { @@ -1783,10 +1780,12 @@ jdk1.8 以后新增的功能: * 默认方法(就是普通实例方法) * 必须用 default 修饰,默认会 public 修饰 * 必须用接口的实现类的对象来调用 + * 必须有默认实现 * 静态方法 * 默认会 public 修饰 * 接口的静态方法必须用接口的类名本身来调用 * 调用格式:ClassName.method() + * 必须有默认实现 * 私有方法:JDK 1.9 才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 ```java @@ -1846,7 +1845,7 @@ interface InterfaceJDK8{ | 实现 | 子类使用 **extends** 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 **implements** 来实现接口。它需要提供接口中所有声明的方法的实现 | | 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | | 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通 Java 类没有任何区别 | 接口是完全不同的类型 | -| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口方法默认修饰符是 **public**,别的修饰符需要有方法体 | +| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口默认修饰符是 **public**,别的修饰符需要有方法体 | | main方法 | 抽象方法可以有 main 方法并且我们可以运行它 | jdk8 以前接口没有 main 方法,不能运行;jdk8 以后接口可以有 default 和 static 方法,可以运行 main 方法 | | 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | | 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | @@ -1928,21 +1927,22 @@ class Animal{ #### 上下转型 >基本数据类型的转换: -> 1.小范围类型的变量或者值可以直接赋值给大范围类型的变量。 -> 2.大范围类型的变量或者值必须强制类型转换给小范围类型的变量。 +> +>1. 小范围类型的变量或者值可以直接赋值给大范围类型的变量 +>2. 大范围类型的变量或者值必须强制类型转换给小范围类型的变量 -引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。 +引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量 **父类引用指向子类对象** -- **向上转型(upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 -- **向下转型(downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 +- **向上转型 (upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 +- **向下转型 (downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 ```java public class PolymorphicDemo { public static void main(String[] args){ - Animal a = new Cat();//向上转型 - Cat c = (Cat)a;//向下转型 + Animal a = new Cat(); // 向上转型 + Cat c = (Cat)a; // 向下转型 } } class Animal{} @@ -2007,7 +2007,7 @@ class Animal{} #### 静态内部类 -定义:有static修饰,属于外部类本身,会加载一次 +定义:有 static 修饰,属于外部类本身,会加载一次 静态内部类中的成分研究: @@ -2017,7 +2017,7 @@ class Animal{} 静态内部类的访问格式:外部类名称.内部类名称 -静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器; +静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器 静态内部类的访问拓展: @@ -2050,22 +2050,20 @@ static class Outter{ #### 实例内部类 -定义:无static修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 +定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 实例内部类的访问格式:外部类名称.内部类名称 -创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器; +创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器 -* `Outter.Inner in = new Outter().new Inner();` +* `Outter.Inner in = new Outter().new Inner()` -拓展:**实例内部类可以访问外部类的全部成员** +**实例内部类可以访问外部类的全部成员** -> * 实例内部类中是否可以直接访问外部类的静态成员? -> 可以,外部类的静态成员可以被共享访问! -> * 实例内部类中是否可以访问外部类的实例成员? -> 可以,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员! +* 实例内部类中可以直接访问外部类的静态成员,外部类的静态成员可以被共享访问 +* 实例内部类中可以访问外部类的实例成员,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员 @@ -2075,7 +2073,7 @@ static class Outter{ #### 局部内部类 -局部内部类:定义在方法中,在构造器中,代码块中,for循环中定义的内部类。 +局部内部类:定义在方法中,在构造器中,代码块中,for 循环中定义的内部类 局部内部类中的成分特点:只能定义实例成员,不能定义静态成员 @@ -2101,7 +2099,6 @@ public class InnerClass{ #### 匿名内部类 匿名内部类:没有名字的局部内部类 -作用:简化代码,是开发中常用的形式 匿名内部类的格式: @@ -2114,7 +2111,7 @@ new 类名|抽象类|接口(形参){ * 匿名内部类不能定义静态成员 * 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 -* **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** +* **匿名内部类的对象的类型相当于是当前 new 的那个的类型的子类类型** * 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) ```java @@ -2149,7 +2146,7 @@ abstract class Animal{ ### 权限符 权限修饰符:有四种**(private -> 缺省 -> protected - > public )** -可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制! +可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制 | 四种修饰符访问权限 | private | 缺省 | protected | public | | ------------------ | :-----: | :--: | :-------: | :----: | @@ -2184,12 +2181,12 @@ static { ``` * 静态代码块特点: - * 必须有static修饰 + * 必须有 static 修饰 * 会与类一起优先加载,且自动触发执行一次 * 只能访问静态资源 * 静态代码块作用: * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 - * **先执行静态代码块,在执行main函数里的操作** + * **先执行静态代码块,在执行 main 函数里的操作** ```java public class CodeDemo { @@ -2234,7 +2231,7 @@ main方法被执行 ``` * 实例代码块的特点: - * 无static修饰,属于对象 + * 无 static 修饰,属于对象 * 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次 * 实例代码块的代码在底层实际上是提取到每个构造器中去执行的 @@ -2305,7 +2302,7 @@ public boolean equals(Object o) { **面试题**:== 和 equals 的区别 * == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作 -* Object 类中的方法,默认比较两个对象的引用,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 +* Object 类中的方法,**默认比较两个对象的引用**,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 hashCode 的作用: @@ -2331,7 +2328,7 @@ Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(), * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 +Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括 clone),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` @@ -2343,7 +2340,7 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( 浅克隆:Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy - * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是**不可以被改变的对象**,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) @@ -2376,12 +2373,12 @@ SDP → 创建型 → 原型模式 ### Objects -Objects 类与 Object 是继承关系。 +Objects 类与 Object 是继承关系 -Objects的方法: +Objects 的方法: -* `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! +* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同。 + 底层进行非空判断,从而可以避免空指针异常,更安全,推荐使用! ```java public static boolean equals(Object a, Object b) { @@ -2389,11 +2386,11 @@ Objects的方法: } ``` -* `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! +* `public static boolean isNull(Object obj)`:判断变量是否为 null ,为 null 返回 true -* `public static String toString(对象)` : 返回参数中对象的字符串表示形式 +* `public static String toString(对象)`:返回参数中对象的字符串表示形式 -* `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 +* `public static String toString(对象, 默认字符串)`:返回对象的字符串表示形式 ```java public class ObjectsDemo { @@ -2464,7 +2461,7 @@ s = s + "cd"; //s = abccd 新对象 * `public char[] toCharArray()`:将字符串拆分为字符数组后返回 * `public boolean startsWith(String prefix)`:测试此字符串是否以指定的前缀开头 * `public int indexOf(String str)`:返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 -* `public int lastIndexOf(String str)`:返回字符串最后一次出现str的索引,没有返回 -1 +* `public int lastIndexOf(String str)`:返回字符串最后一次出现 str 的索引,没有返回 -1 * `public String substring(int beginIndex)`:返回子字符串,以原字符串指定索引处到结尾 * `public String substring(int i, int j)`:指定索引处扩展到 j - 1 的位置,字符串长度为 j - i * `public String toLowerCase()`:将此 String 所有字符转换为小写,使用默认语言环境的规则 @@ -2493,7 +2490,7 @@ s.replace("-","");//12378 直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc - 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** -- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 +- 直接赋值方式创建:以 `" "` 方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 `String str = new String("abc")` 创建字符串对象: @@ -2574,7 +2571,7 @@ public class Demo { System.out.println(s3 == s5); // true String x2 = new String("c") + new String("d"); // new String("cd") - // 虽然 new,但是在字符串常量池没有 cd 对象,toString() 方法 + // 虽然 new,但是在字符串常量池没有 cd 对象,因为 toString() 方法 x2.intern(); String x1 = "cd"; @@ -2612,7 +2609,7 @@ public static void main(String[] args) { String s2 = s.intern(); //jdk6:串池中创建一个字符串"ab" - //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向new String("ab"),将此引用返回 + //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向 new String("ab"),将此引用返回 System.out.println(s2 == "ab");//jdk6:true jdk8:true System.out.println(s == "ab");//jdk6:false jdk8:true @@ -2641,7 +2638,7 @@ public static void main(String[] args) { } ``` -* Version类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的"java"字符串字面量就被放入的字符串常量池: +* Version 类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的 `"java"` 字符串字面量就被放入的字符串常量池: ```java package sun.misc; @@ -2664,15 +2661,15 @@ public static void main(String[] args) { ##### 内存位置 -Java 7之前,String Pool 被放在运行时常量池中,它属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致OutOfMemoryError 错误 +Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误 演示 StringTable 位置: -* `-Xmx10m`设置堆内存10m +* `-Xmx10m` 设置堆内存 10m -* 在jdk8下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在Run Configurations VM options) +* 在 JDK8 下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在 Run Configurations VM options) -* 在jdk6下设置: `-XX:MaxPermSize=10m` +* 在 JDK6 下设置: `-XX:MaxPermSize=10m` ```java public static void main(String[] args) throws InterruptedException { @@ -2824,7 +2821,7 @@ public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { ### Arrays -Array 的工具类 +Array 的工具类 Arrays 常用API: @@ -3755,7 +3752,7 @@ public class RegexDemo { 压栈 == 入栈、弹栈 == 出栈 场景:手枪的弹夹 -* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)。元素存在索引 +* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)元素存在索引 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) **增删元素慢**(创建新数组,迁移元素) @@ -3765,12 +3762,11 @@ public class RegexDemo { * 树: * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) - 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差 - 为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - - * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 - - 特点:**红黑树的增删查改性能都好** + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 + +* 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 + + 特点:**红黑树的增删查改性能都好** 各数据结构时间复杂度对比: @@ -3790,7 +3786,7 @@ public class RegexDemo { #### 概述 -Java 中集合的代表是Collection,Collection 集合是 Java 中集合的祖宗类 +Java 中集合的代表是 Collection,Collection 集合是 Java 中集合的祖宗类 Collection 集合底层为数组:`[value1, value2, ....]` @@ -3979,8 +3975,6 @@ public static void main(String[] args){ } ``` -![ArrayList源码分析](https://gitee.com/seazean/images/raw/master/Java/ArrayList添加元素源码解析.png) - *** @@ -3997,8 +3991,8 @@ public class ArrayList extends AbstractList ``` - `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 -- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数`clone()`,能被克隆 -- `ArrayList` 实现了 `Serializable `接口,这意味着`ArrayList`支持序列化,能通过序列化去传输 +- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数 `clone()`,能被克隆 +- `ArrayList` 实现了 `Serializable ` 接口,这意味着 `ArrayList` 支持序列化,能通过序列化去传输 核心方法: @@ -4082,8 +4076,8 @@ public class ArrayList extends AbstractList MAX_ARRAY_SIZE:要分配的数组的最大大小,分配更大的**可能**会导致 - * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出VM限制) - * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置JVM参数 -Xmx 来调节) + * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出 VM 限制) + * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置 JVM 参数 -Xmx 来调节) * 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, @@ -4121,7 +4115,7 @@ public class ArrayList extends AbstractList } ``` -* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 +* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,改变了抛出 ConcurrentModificationException 异常 @@ -4186,7 +4180,7 @@ public class ArrayList extends AbstractList 2. Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍 -3. 底层都是 `Object[]`数组存储 +3. 底层都是 `Object[]` 数组存储 @@ -4214,7 +4208,7 @@ LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元 * `public void push(E e)`:将元素推入此列表所表示的堆栈 * `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 * `public int lastIndexOf(Object o)`:从尾遍历找 -* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回true +* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回 true * `public E remove(int index)`:删除指定位置的元素 ```java @@ -4247,9 +4241,9 @@ public class ListDemo { } ``` -![](https://gitee.com/seazean/images/raw/master/Java/LinkedList添加元素源码解析.png) +*** @@ -4336,7 +4330,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 * LinkedHashSet:添加的元素是有序,不重复,无索引的 * TreeSet:不重复,无索引,按照大小默认升序排序 -**面试问题**:没有索引,不能使用普通 for 循环遍历 +**注意**:没有索引,不能使用普通 for 循环遍历 @@ -4355,7 +4349,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 - 哈希值的特点 - 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的 - - 默认情况下,不同对象的哈希值是不同的。而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 + - 默认情况下,不同对象的哈希值是不同的,而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 **HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** @@ -4382,16 +4376,17 @@ Set 集合添加的元素是无序,不重复的。 不重复 重复了 ``` -* Set系列集合元素无序的根本原因 +* Set 系列集合元素无序的根本原因 - Set系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 - JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) - JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) - 当链表长度超过阈值8且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 - 当链表长度超过阈值8且当前数组的长度 < 64时,扩容 + Set 系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 + + * JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) + * JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) + * 当链表长度超过阈值 8 且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 + * 当链表长度超过阈值 8 且当前数组的长度 < 64时,扩容 ![](https://gitee.com/seazean/images/raw/master/Java/HashSet底层结构哈希表.png) - + 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 * 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写 hashCode 和 equals 方法** @@ -4426,9 +4421,9 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4436,7 +4431,7 @@ TreeSet 集合自排序的方式: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4475,7 +4470,7 @@ public class Student implements Comparable{ } ``` -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(红黑树) +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边(红黑树) @@ -4642,7 +4637,7 @@ Map集合的遍历方式有:3种。 1. “键找值”的方式遍历:先获取 Map 集合全部的键,再根据遍历键找值。 2. “键值对”的方式遍历:难度较大,采用增强 for 或者迭代器 -3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda表 达式 +3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda 表达式 集合可以直接输出内容,因为底层重写了 toString() 方法 @@ -4732,7 +4727,7 @@ JDK7 对比 JDK8: ##### 继承关系 -HashMap继承关系如下图所示: +HashMap 继承关系如下图所示: ![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) @@ -4740,9 +4735,9 @@ HashMap继承关系如下图所示: 说明: -* Cloneable 空接口,表示可以克隆, 创建并返回HashMap对象的一个副本。 -* Serializable 序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化。 -* AbstractMap 父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作 +* Cloneable 空接口,表示可以克隆, 创建并返回 HashMap 对象的一个副本。 +* Serializable 序列化接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。 +* AbstractMap 父类提供了 Map 实现接口,以最大限度地减少实现此接口所需的工作 @@ -4773,7 +4768,7 @@ HashMap继承关系如下图所示: * 为什么必须是 2 的 n 次幂? - HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 @@ -4833,7 +4828,7 @@ HashMap继承关系如下图所示: * 其他说法 红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8)=3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4,这才有转换成树的必要;链表长度如果是小于等于 6,6/2=3,而 log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 -6. 当链表的值小 于 6 则会从红黑树转回链表 +6. 当链表的值小于 6 则会从红黑树转回链表 ```java // 当桶(bucket)上的结点数小于这个值时树转链表 @@ -4847,7 +4842,7 @@ HashMap继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树,效率也变的更高效 + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于等于 64 时,链表才转换为红黑树,效率也变的更高效 8. table 用来初始化(必须是二的 n 次幂) @@ -4858,7 +4853,7 @@ HashMap继承关系如下图所示: jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap 中**存放元素的个数**(**重点**) + 9. HashMap 中**存放元素的个数** ```java // 存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 @@ -4899,7 +4894,7 @@ HashMap继承关系如下图所示: loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** - * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍**. + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** @@ -5013,7 +5008,7 @@ HashMap继承关系如下图所示: 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: @@ -5320,7 +5315,7 @@ HashMap继承关系如下图所示: 3. 桶上的 key 不是要找的 key,则查看后续的节点: - * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取v alue + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取 value * 如果后续节点是链表节点,则通过循环遍历链表根据 key 获取 value @@ -5443,7 +5438,7 @@ LinkedHashMap 是 HashMap 的子类 源码解析: -* 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序 +* **内部维护了一个双向链表**,用来维护插入顺序或者 LRU 顺序 ```java transient LinkedHashMap.Entry head; @@ -5672,7 +5667,7 @@ TreeMap 集合指定大小规则有 2 种方式: WeakHashMap 是基于弱引用的 -内部的 Entry 继承 WeakReference,被弱引用关联的对象在下一次垃圾回收时会被回收,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 +内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 ```java private static class Entry extends WeakReference implements Map.Entry { @@ -5734,41 +5729,6 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc -*** - - - -#### 面试题 - -输出一个字符串中每个字符出现的次数。 - -```java -/* - (1)键盘录入一个字符串。aabbccddaa123。 - (2)定义一个Map集合,键是每个字符,值是其出现的次数。 {a=4 , b=2 ,...} - (3)遍历字符串中的每一个字符。 - (4)拿着这个字符去Map集合中看是否有这个字符键,有说明之前统计过,其值+1 - 没有这个字符键,说明该字符是第一次统计,直接存入“该字符=1” -*/ -public class MapDemo{ - public static void main(String[] args){ - String s = "aabbccddaa123"; - Map infos = new HashMap<>(); - for (int i = 0; i < s.length(); i++){ - char ch = datas.charAt(i); - if(infos.containsKey(ch)){ - infos.put(ch,infos.get(ch) + 1); - } else { - infos.put(ch,1); - } - } - System.out.println("结果:"+infos); - } -} -``` - - - *** @@ -14234,7 +14194,7 @@ public static void main(String[] args) { } ``` -注意:如果调用了 foo() 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 +注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 diff --git a/Prog.md b/Prog.md index c09c693..8d5634a 100644 --- a/Prog.md +++ b/Prog.md @@ -14077,7 +14077,7 @@ ServerSocket 类: -相当于客户端和服务器建立一个数据管道,管道一般不用 close +**相当于**客户端和服务器建立一个数据管道(虚连接,不是真正的物理连接),管道一般不用 close diff --git a/Web.md b/Web.md index 3236ec1..ea0ed9d 100644 --- a/Web.md +++ b/Web.md @@ -2119,8 +2119,10 @@ URL 和 URI * 进行 URL 解析,进行编码 * DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 -* 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 +* 查找到 IP 之后,进行 TCP 协议的三次握手建立连接 +* 发出 HTTP 请求,取文件指令 * 服务器处理请求,返回响应 +* 释放 TCP 连接 * 浏览器解析渲染页面 From c6e1467b4466acb9b6686b8008dc844443b8de01 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 23:20:36 +0800 Subject: [PATCH 23/78] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ac1828..211ff15 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ * DB:MySQL、Redis * Frame:Maven、Netty、RocketMQ -* Java:JavaSE、JVM、Algorithm、Design Pattern +* Java:JavaSE、JVM、Algorithm * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker From 83f54324a1b289a542e899a5b0520710481e806e Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 21 Feb 2022 00:43:50 +0800 Subject: [PATCH 24/78] Update Java Note --- Java.md | 15131 ++++++++++++++++++++---------------------------------- 1 file changed, 5599 insertions(+), 9532 deletions(-) diff --git a/Java.md b/Java.md index be48f40..77f9179 100644 --- a/Java.md +++ b/Java.md @@ -2260,6 +2260,8 @@ public class CodeDemo { + + *** @@ -2872,6 +2874,48 @@ public class MyArraysDemo { +*** + + + +### System + +System 代表当前系统 + +静态方法: + +* `public static void exit(int status)`:终止 JVM 虚拟机,**非 0 是异常终止** + +* `public static long currentTimeMillis()`:获取当前系统此刻时间毫秒值 + +* `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)`:数组拷贝 + 参数一:原数组 + 参数二:从原数组的哪个位置开始赋值 + 参数三:目标数组 + 参数四:从目标数组的哪个位置开始赋值 + 参数五:赋值几个 + +```java +public class SystemDemo { + public static void main(String[] args) { + //System.exit(0); // 0代表正常终止!! + long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 + for(int i = 0; i < 10000; i++){输出i} + long endTime = new Date().getTime(); + System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 + + int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; + int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] + // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] + System.arraycopy(arr1, 2, arr2, 1, 3); + } +} +``` + + + + + *** @@ -2889,8 +2933,8 @@ public class MyArraysDemo { 时间记录的两种方式: -1. Date日期对象 -2. 时间毫秒值:从1970-01-01 00:00:00开始走到此刻的总的毫秒值。 1s = 1000ms +1. Date 日期对象 +2. 时间毫秒值:从 `1970-01-01 00:00:00` 开始走到此刻的总的毫秒值,1s = 1000ms ```java public class DateDemo { @@ -2934,10 +2978,10 @@ DateFormat 是一个抽象类,不能直接使用,使用它的子类:Simple SimpleDateFormat 简单日期格式化类: -* `public SimpleDateFormat(String pattern)` : 指定时间的格式创建简单日期对象 -* `public String format(Date date) ` : 把日期对象格式化成我们喜欢的时间形式,返回字符串 -* `public String format(Object time)` : 把时间毫秒值格式化成设定的时间形式,返回字符串! -* `public Date parse(String date)` : 把字符串的时间解析成日期对象 +* `public SimpleDateFormat(String pattern)`:指定时间的格式创建简单日期对象 +* `public String format(Date date) `:把日期对象格式化成我们喜欢的时间形式,返回字符串 +* `public String format(Object time)`:把时间毫秒值格式化成设定的时间形式,返回字符串! +* `public Date parse(String date)`:把字符串的时间解析成日期对象 >yyyy年MM月dd日 HH:mm:ss EEE a" 周几 上午下午 @@ -2972,12 +3016,12 @@ Calendar 日历类创建日历对象:`Calendar rightNow = Calendar.getInstance Calendar 的方法: -* `public static Calendar getInstance()`: 返回一个日历类的对象 +* `public static Calendar getInstance()`:返回一个日历类的对象 * `public int get(int field)`:取日期中的某个字段信息 * `public void set(int field,int value)`:修改日历的某个字段信息 * `public void add(int field,int amount)`:为某个字段增加/减少指定的值 -* `public final Date getTime()`: 拿到此刻日期对象 -* `public long getTimeInMillis()`: 拿到此刻时间毫秒值 +* `public final Date getTime()`:拿到此刻日期对象 +* `public long getTimeInMillis()`:拿到此刻时间毫秒值 ```java public static void main(String[] args){ @@ -3048,23 +3092,10 @@ public class JDK8DateDemo2 { } ``` -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------ | -| public LocalDateTime plusYears (long years) | 添加或者减去年 | -| public LocalDateTime plusMonths(long months) | 添加或者减去月 | -| public LocalDateTime plusDays(long days) | 添加或者减去日 | -| public LocalDateTime plusHours(long hours) | 添加或者减去时 | -| public LocalDateTime plusMinutes(long minutes) | 添加或者减去分 | -| public LocalDateTime plusSeconds(long seconds) | 添加或者减去秒 | -| public LocalDateTime plusWeeks(long weeks) | 添加或者减去周 | -| public LocalDateTime minusYears (long years) | 减去或者添加年 | -| public LocalDateTime withYear(int year) | 直接修改年 | -| public LocalDateTime withMonth(int month) | 直接修改月 | -| public LocalDateTime withDayOfMonth(int dayofmonth) | 直接修改日期(一个月中的第几天) | -| public LocalDateTime withDayOfYear(int dayOfYear) | 直接修改日期(一年中的第几天) | -| public LocalDateTime withHour(int hour) | 直接修改小时 | -| public LocalDateTime withMinute(int minute) | 直接修改分钟 | -| public LocalDateTime withSecond(int second) | 直接修改秒 | +| 方法名 | 说明 | +| ------------------------------------------- | -------------- | +| public LocalDateTime plusYears (long years) | 添加或者减去年 | +| public LocalDateTime withYear(int year) | 直接修改年 | @@ -3142,6 +3173,10 @@ public class MathDemo { +**** + + + ### DecimalFormat 使任何形式的数字解析和格式化 @@ -3175,42 +3210,6 @@ public static void main(String[]args){ -*** - - - -### System - -System代表当前系统。 - -静态方法: - -1. `public static void exit(int status)` : 终止JVM虚拟机,非0是异常终止 -2. `public static long currentTimeMillis()` : 获取当前系统此刻时间毫秒值 -3. `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)` : 数组拷贝 - 参数一:原数组 - 参数二:从原数组的哪个位置开始赋值。 - 参数三:目标数组 - 参数四:从目标数组的哪个位置开始赋值 - 参数五:赋值几个。 - -```java -public class SystemDemo { - public static void main(String[] args) { - //System.exit(0); // 0代表正常终止!! - long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 - for(int i = 0; i < 10000; i++){输出i} - long endTime = new Date().getTime(); - System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 - - int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; - int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] - // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] - System.arraycopy(arr1, 2, arr2, 1, 3); - } -} -``` - *** @@ -3233,8 +3232,8 @@ Java 在 java.math 包中提供的 API 类,用来对超过16位有效位的数 * `public BigDecimal subtract(BigDecimal value)`:减法运算 * `public BigDecimal multiply(BigDecimal value)`:乘法运算 * `public BigDecimal divide(BigDecimal value)`:除法运算 -* `public double doubleValue()`:把BigDecimal转换成double类型。 -* `public int intValue()`:转为int 其他类型相同 +* `public double doubleValue()`:把 BigDecimal 转换成 double 类型 +* `public int intValue()`:转为 int 其他类型相同 * `public BigDecimal divide (BigDecimal value,精确几位,舍入模式)`:除法 ```java @@ -3261,20 +3260,20 @@ public class BigDecimalDemo { } ``` -总结 +总结: 1. BigDecimal 是用来进行精确计算的 -2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的。 -3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法。 +2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的 +3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法 ```java BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式); -参数1:表示参与运算的BigDecimal 对象。 -参数2:表示小数点后面精确到多少位 -参数3:舍入模式 - BigDecimal.ROUND_UP 进一法 - BigDecimal.ROUND_FLOOR 去尾法 - BigDecimal.ROUND_HALF_UP 四舍五入 +//参数1:表示参与运算的BigDecimal 对象。 +//参数2:表示小数点后面精确到多少位 +//参数3:舍入模式 +// BigDecimal.ROUND_UP 进一法 +// BigDecimal.ROUND_FLOOR 去尾法 +// BigDecimal.ROUND_HALF_UP 四舍五入 ``` @@ -3718,6 +3717,8 @@ public class RegexDemo { + + ## 集合 ### 集合概述 @@ -3763,7 +3764,7 @@ public class RegexDemo { * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - + * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 特点:**红黑树的增删查改性能都好** @@ -5748,15 +5749,13 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc * **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** ```java -{ - ArrayList lists = new ArrayList<>(); - lists.add(99.9); - lists.add('a'); - lists.add("Java"); - ArrayList list = new ArrayList<>(); - lists1.add(10); - lists1.add(20); -} +ArrayList lists = new ArrayList<>(); +lists.add(99.9); +lists.add('a'); +lists.add("Java"); +ArrayList list = new ArrayList<>(); +lists1.add(10); +lists1.add(20); ``` 优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 @@ -5913,73 +5912,13 @@ class Dog{} -### 不可变 - -在 List、Set、Map 接口中都存在 of 方法,可以创建一个不可变的集合 -+ 这个集合不能添加,不能删除,不能修改 -+ 但是可以结合集合的带参构造,实现集合的批量添加 - -在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性 -+ 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中 - -````java -public class MyVariableParameter4 { - public static void main(String[] args) { - // static List of(E…elements) 创建一个具有指定元素的List集合对象 - //static Set of(E…elements) 创建一个具有指定元素的Set集合对象 - //static Map of(E…elements) 创建一个具有指定元素的Map集合对象 - - //method1(); - //method2(); - //method3(); - //method4(); - - } - - private static void method4() { - Map map = Map.ofEntries( - Map.entry("zhangsan", "江苏"), - Map.entry("lisi", "北京")); - System.out.println(map); - } - - private static void method3() { - Map map = Map.of("zhangsan", "江苏", "lisi", "北京"); - System.out.println(map); - } - - private static void method2() { - //传递的参数当中,不能存在重复的元素。 - Set set = Set.of("a", "b", "c", "d","a"); - System.out.println(set); - } - - private static void method1() { - List list = List.of("a", "b", "c", "d"); - System.out.println(list); - - //集合的批量添加。 - //首先是通过调用List.of方法来创建一个不可变的集合,of方法的形参就是一个可变参数。 - //再创建一个ArrayList集合,并把这个不可变的集合中所有的数据,都添加到ArrayList中。 - ArrayList list3 = new ArrayList<>(List.of("a", "b", "c", "d")); - System.out.println(list3); - } -} -```` - - - - - -*** - ## 异常 ### 基本介绍 -异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 @@ -5998,7 +5937,7 @@ Java 中异常继承的根类是:Throwable Exception 异常的分类: * 编译时异常:继承自 Exception 的异常或者其子类,编译阶段就会报错 -* 运行时异常: 继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理 +* 运行时异常:继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行阶段出错 @@ -6011,9 +5950,9 @@ Exception 异常的分类: 异常的产生默认的处理过程解析:(自动处理的过程) 1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) -2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机 +2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给 JVM 虚拟机 3. 虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据 -4. 直接从当前执行的异常点干掉当前程序 +4. 直接从当前执行的异常点终止当前程序 5. 后续代码没有机会执行了,因为程序已经死亡 ```java @@ -6040,12 +5979,9 @@ public class ExceptionDemo { #### 基本介绍 -编译时异常:继承自Exception的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错,必须程序员编译阶段就处理的。否则代码编译就报错 - -编译时异常的作用是什么: +编译时异常:继承自 Exception 的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错 -* 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒 -* 提醒程序员这里很可能出错,请检查并注意不要出bug +编译时异常的作用是什么:在编译阶段就爆出一个错误,目的在于提醒,请检查并注意不要出 BUG ```java public static void main(String[] args) throws ParseException { @@ -6066,12 +6002,9 @@ public static void main(String[] args) throws ParseException { ##### throws -在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机,JVM 虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 +在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给 JVM 虚拟机,JVM 虚拟机输出异常信息,直接终止掉程序,这种方式与默认方式是一样的 -* 优点:可以解决代码编译时的错误 -* 运行时出现异常,程序还是会立即死亡! - -**Exception是异常最高类型可以抛出一切异常!** +**Exception是异常最高类型可以抛出一切异常** ```java public static void main(String[] args) throws Exception { @@ -6091,7 +6024,7 @@ public static void main(String[] args) throws Exception { ##### try/catch -可以处理异常,并且出现异常后代码也不会死亡。 +可以处理异常,并且出现异常后代码也不会死亡 * 自己捕获异常和处理异常的格式:**捕获处理** @@ -6107,9 +6040,8 @@ public static void main(String[] args) throws Exception { } ``` -* 监视捕获处理异常企业级写法: - Exception可以捕获处理一切异常类型! - +* 监视捕获处理异常写法:Exception 可以捕获处理一切异常类型 + ```java try{ // 可能出现异常的代码! @@ -6119,9 +6051,10 @@ public static void main(String[] args) throws Exception { ``` **Throwable成员方法:** - `public String getMessage()` : 返回此 throwable 的详细消息字符串 - `public String toString()` : 返回此可抛出的简短描述 - `public void printStackTrace()` : 把异常的错误信息输出在控制台 + +* `public String getMessage()`:返回此 throwable 的详细消息字符串 +* `public String toString()`:返回此可抛出的简短描述 +* `public void printStackTrace()`:把异常的错误信息输出在控制台 ```java public static void main(String[] args) { @@ -6146,8 +6079,7 @@ public static void main(String[] args) { ##### 规范做法 -在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!(**规范做法**) -这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡(最好的方案) +在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理 ```java public class ExceptionDemo{ @@ -6174,16 +6106,16 @@ public class ExceptionDemo{ #### 基本介绍 -继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! +继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过 **常见的运行时异常**: -1. 数组索引越界异常: ArrayIndexOutOfBoundsException -2. 空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! +1. 数组索引越界异常:ArrayIndexOutOfBoundsException +2. 空指针异常:NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错 3. 类型转换异常:ClassCastException 4. 迭代器遍历没有此元素异常:NoSuchElementException 5. 算术异常(数学操作异常):ArithmeticException -6. 数字转换异常: NumberFormatException +6. 数字转换异常:NumberFormatException ```java public class ExceptionDemo { @@ -6267,11 +6199,9 @@ catch:0-N次 (如果有finally那么catch可以没有!!) finally: 0-1次 ``` +**finally 的作用**:可以在代码执行完毕以后进行资源的释放操作 - -**finally的作用**:可以在代码执行完毕以后进行资源的释放操作 - -资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法! +资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法 注意:如果在 finally 中出现了 return,会吞掉异常 @@ -6322,10 +6252,10 @@ public class FinallyDemo { 自定义异常: -* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用throw new 自定义对象抛出 -* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出! +* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 +* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 -**throws: 用在方法上,用于抛出方法中的异常** +**throws:用在方法上,用于抛出方法中的异常** **throw: 用在出现异常的地方,创建异常对象且立即从此处抛出** @@ -6412,17 +6342,19 @@ public class Demo{ + + ## λ ### lambda #### 基本介绍 -Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法 +Lambda 表达式是 JDK1.8 开始之后的新技术,是一种代码的新语法,一种特殊写法 作用:为了简化匿名内部类的代码写法 -Lambda表达式的格式: +Lambda 表达式的格式: ```java (匿名内部类被重写方法的形参列表) -> { @@ -6430,11 +6362,11 @@ Lambda表达式的格式: } ``` -Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** +Lambda 表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** 简化条件:首先必须是接口,接口中只能有一个抽象方法 -@FunctionalInterface函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 +@FunctionalInterface 函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 @@ -6444,9 +6376,9 @@ Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函 #### 简化方法 -Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) +Lambda 表达式的省略写法(进一步在 Lambda 表达式的基础上继续简化) -* 如果Lambda表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是return语句,必须省略return不写 +* 如果 Lambda 表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是 return 语句,必须省略 return 不写 * 参数类型可以省略不写 * 如果只有一个参数,参数类型可以省略,同时()也可以省略 @@ -6486,35 +6418,7 @@ names.forEach(s -> System.out.println(s) ); #### 常用简化 -##### Runnable - -```java -//1. -Thread t = new Thread(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName()+":执行~~~"); - } -}); -t.start(); - -//2. -Thread t1 = new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}); -t1.start(); -//3. -new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}).start(); - -//4.一行代码 -new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start(); -``` - - - -##### Comparator +Comparator ```java public class CollectionsDemo { @@ -6550,7 +6454,7 @@ public class CollectionsDemo { #### 基本介绍 -方法引用:方法引用是为了进一步简化Lambda表达式的写法 +方法引用:方法引用是为了进一步简化 Lambda 表达式的写法 方法引用的格式:类型或者对象::引用的方法 @@ -6708,6 +6612,8 @@ public class ConstructorDemo { + + ## I/O ### Stream @@ -6895,9 +6801,9 @@ public static void main(String[] args) { File 类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) File 类构造器: - `public File(String pathname)`:根据路径获取文件对象 - `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! - `public File(File parent , String child)` + +* `public File(String pathname)`:根据路径获取文件对象 +* `public File(String parent , String child)`:根据父路径和文件名称获取文件对象 File 类创建文件对象的格式: @@ -6945,17 +6851,19 @@ public class FileDemo{ ##### 常用方法 -`public String getAbsolutePath()` : 返回此File的绝对路径名字符串。 -`public String getPath()` : 获取创建文件对象的时候用的路径 -`public String getName()` : 返回由此File表示的文件或目录的名称。 -`public long length()` : 返回由此File表示的文件的长度(大小)。 -`public long length(FileFilter filter)` : 文件过滤器。 +| 方法 | 说明 | +| ------------------------------ | -------------------------------------- | +| String getAbsolutePath() | 返回此 File 的绝对路径名字符串 | +| String getPath() | 获取创建文件对象的时候用的路径 | +| String getName() | 返回由此 File 表示的文件或目录的名称 | +| long length() | 返回由此 File 表示的文件的长度(大小) | +| long length(FileFilter filter) | 文件过滤器 | ```java public class FileDemo { public static void main(String[] args) { // 1.绝对路径创建一个文件对象 - File f1 = new File("E:/图片/meinv.jpg"); + File f1 = new File("E:/图片/test.jpg"); // a.获取它的绝对路径。 System.out.println(f1.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6967,7 +6875,7 @@ public class FileDemo { System.out.println("------------------------"); // 2.相对路径 - File f2 = new File("Day09Demo/src/dlei01.txt"); + File f2 = new File("Demo/src/test.txt"); // a.获取它的绝对路径。 System.out.println(f2.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6983,14 +6891,20 @@ public class FileDemo { +*** + + + ##### 判断方法 -`public boolean exists()` : 此File表示的文件或目录是否实际存在。 -`public boolean isDirectory()` : 此File表示的是否为目录。 -`public boolean isFile()` : 此File表示的是否为文件 +方法列表: + +* `boolean exists()`:此 File 表示的文件或目录是否实际存在 +* `boolean isDirectory()`:此 File 表示的是否为目录 +* `boolean isFile()`:此 File 表示的是否为文件 ```java -File f = new File("Day09Demo/src/dlei01.txt"); +File f = new File("Demo/src/test.txt"); // a.判断文件路径是否存在 System.out.println(f.exists()); // true // b.判断文件对象是否是文件,是文件返回true ,反之 @@ -7001,17 +6915,23 @@ System.out.println(f.isDirectory()); // false +**** + + + ##### 创建删除 -`public boolean createNewFile()` : 当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件。 -`public boolean delete()` : 删除由此File表示的文件或目录。 (只能删除空目录) -`public boolean mkdir()` : 创建由此File表示的目录。(只能创建一级目录) -`public boolean mkdirs()` : 可以创建多级目录(建议使用的) +方法列表: + +* `boolean createNewFile()`:当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件 +* `boolean delete()`:删除由此 File 表示的文件或目录(只能删除空目录) +* `boolean mkdir()`:创建由此 File 表示的目录(只能创建一级目录) +* `boolean mkdirs()`:可以创建多级目录(建议使用) ```java public class FileDemo { public static void main(String[] args) throws IOException { - File f = new File("Day09Demo/src/dlei02.txt"); + File f = new File("Demo/src/test.txt"); // a.创建新文件,创建成功返回true ,反之 System.out.println(f.createNewFile()); @@ -7041,7 +6961,7 @@ public class FileDemo { #### 遍历目录 - `public String[] list()`:获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()(常用)`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) +- `public File[] listFiles()`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) - `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间。 ```java @@ -7060,7 +6980,7 @@ public class FileDemo { } // c - File f1 = new File("D:\\it\\图片资源\\beautiful.jpg"); + File f1 = new File("D:\\图片资源\\beautiful.jpg"); long time = f1.lastModified(); // 最后修改时间! SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(time)); @@ -7168,30 +7088,23 @@ IO 输入输出流:输入/输出流 * Input:输入 * Output:输出 -引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 +引入:File 类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用 IO 流 IO 流是一个水流模型:IO 理解成水管,把数据理解成水流 IO 流的分类: * 按照流的方向分为:输入流,输出流。 - * 输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流 - 输出流的作用:写数据到文件,或者写数据发送给别人 - * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中的流称为输入流 - 输入流的作用:读取数据到内存 + * 输出流:以内存为基准,把内存中的数据**写出到磁盘文件**或者网络介质中去的流称为输出流 + * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据**读入到内存**中的流称为输入流 * 按照流的内容分为:字节流,字符流 * 字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流 * 字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流(**针对于文本内容**) -流大体分为四大类: - -* 字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流 -* 字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流 -* 字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流 -* 字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流 +流大体分为四大类:字节输入流、字节输出流、字符输入流、字符输出流 ```java -IO流的体系: +IO 流的体系: 字节流 字符流 字节输入流 字节输出流 字符输入流 字符输出流 InputStream OutputStream Reader Writer (抽象类) @@ -7211,25 +7124,25 @@ ObjectInputStream ObjectOutputStream ##### 字节输入 -FileInputStream 文件字节输入流: +FileInputStream 文件字节输入流:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 + +构造方法: -* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 +* `public FileInputStream(File path)`:创建一个字节输入流管道与源文件对象接通 +* `public FileInputStream(String pathName)`:创建一个字节输入流管道与文件路径对接,底层实质上创建 File 对象 -* 构造器: - `public FileInputStream(File path)` : 创建一个字节输入流管道与源文件对象接通 - `public FileInputStream(String pathName)` : 创建一个字节输入流管道与文件路径对接,底层实质上创建了File对象 - -* 方法: - `public int read()` : 每次读取一个字节返回,读取完毕会返回-1 - `public int read(byte[] buffer)` : 从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回-1,**byte中新读取的数据默认是覆盖原数据**,构造String需要设定长度 - `public String(byte[] bytes,int offset,int length)` : 构造新的String - `public long transferTo(OutputStream out) ` : 从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流`is.transferTo(os)` +方法: + +* `public int read()`:每次读取一个字节返回,读取完毕会返回-1 +* `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回 -1,**byte 中新读取的数据默认是覆盖原数据**,构造 String 需要设定长度 +* `public String(byte[] bytes,int offset,int length)`:构造新的 String +* `public long transferTo(OutputStream out) `:从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流 ```java public class FileInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.创建文件对象定位dlei01.txt - File file = new File("Day09Demo/src/dlei01.txt"); + File file = new File("Demo/src/dlei01.txt"); // 2.创建一个字节输入流管道与源文件接通 InputStream is = new FileInputStream(file); // 3.读取一个字节的编号返回,读取完毕返回-1 @@ -7246,14 +7159,14 @@ public class FileInputStreamDemo01 { } ``` -一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** +一个一个字节读取英文和数字没有问题,但是读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** 采取下面的方案: ```java public static void main(String[] args) throws Exception { //简化写法,底层实质上创建了File对象 - InputStream is = new FileInputStream("Day09Demo/src/dlei01.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); byte[] buffer = new byte[3];//开发中使用byte[1024] int len; while((len = is.read(buffer)) !=-1){ @@ -7265,17 +7178,9 @@ public static void main(String[] args) throws Exception { ``` ```java -//定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出! -//可以避免中文读取输出乱码,但是如果读取的文件过大,会出现内存溢出!! -//字节流并不适合读取文本文件内容输出,读写文件内容建议使用字符流。 -/* - byte[] buffer = new byte[(int) f.length()]; - int len = is.read(buffer); - String rs = new String(buffer); -*/ - -File f = new File("Day09Demo/src/dlei03.txt"); +File f = new File("Demo/src/test.txt"); InputStream is = new FileInputStream(f); +// 读取全部的 byte[] buffer = is.readAllBytes(); String rs = new String(buffer); System.out.println(rs); @@ -7283,35 +7188,39 @@ System.out.println(rs); +**** + + + ##### 字节输出 -FileOutputStream 文件字节输出流: +FileOutputStream 文件字节输出流:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 + +构造方法: + +* `public FileOutputStream(File file)`:创建一个字节输出流管道通向目标文件对象 +* `public FileOutputStream(String file) `:创建一个字节输出流管道通向目标文件路径 +* `public FileOutputStream(File file, boolean append)` : 创建一个追加数据的字节输出流管道到目标文件对象 +* `public FileOutputStream(String file, boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 +API: -* 构造器: - `public FileOutputStream(File file)` : 创建一个字节输出流管道通向目标文件对象 - `public FileOutputStream(String file) ` : 创建一个字节输出流管道通向目标文件路径 - `public FileOutputStream(File file , boolean append)` : 追加数据的字节输出流管道到目标文件对象 - `public FileOutputStream(String file , boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* API: - `public void write(int a)` : 写一个字节出去 - `public void write(byte[] buffer)` :写一个字节数组出去 - `public void write(byte[] buffer , int pos , int len)` : 写一个字节数组的一部分出去 - 参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。 +* `public void write(int a)`:写一个字节出去 +* `public void write(byte[] buffer)`:写一个字节数组出去 +* `public void write(byte[] buffer , int pos , int len)`:写一个字节数组的一部分出去,从 pos 位置,写出 len 长度 -* FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: - `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 - `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 +* FileOutputStream 字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: + * `OutputStream os = new FileOutputStream("Demo/out05")`:覆盖数据管道 + * `OutputStream os = new FileOutputStream("Demo/out05" , true)`:追加数据的管道 说明: -* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道。 -* 换行用: **os.write("\r\n".getBytes());** -* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! +* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道 +* 换行用:**os.write("\r\n".getBytes())** +* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了 ```java -OutputStream os = new FileOutputStream("Day09Demo/out05"); +OutputStream os = new FileOutputStream("Demo/out05"); os.write(97);//a os.write('b'); os.write("\r\n".getBytes()); @@ -7323,14 +7232,7 @@ os.close(); ##### 文件复制 -思想:字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 - -分析步骤: - (1)创建一个字节输入流管道与源文件接通。 - (2)创建一个字节输出流与目标文件接通。 - (3)创建一个字节数组作为桶 - (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。 - (5)关闭资源! +字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 ```java public class CopyDemo01 { @@ -7339,9 +7241,9 @@ public class CopyDemo01 { OutputStream os = null ; try{ //(1)创建一个字节输入流管道与源文件接通。 - is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); + is = new FileInputStream("D:\\seazean\\图片资源\\test.jpg"); //(2)创建一个字节输出流与目标文件接通。 - os = new FileOutputStream("D:\\seazean\\meimei.jpg"); + os = new FileOutputStream("D:\\seazean\\test.jpg"); //(3)创建一个字节数组作为桶 byte buffer = new byte[1024]; //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 @@ -7375,32 +7277,30 @@ public class CopyDemo01 { ##### 字符输入 -FileReader:文件字符输入流 +FileReader:文件字符输入流,以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去 + +构造器: + +* `public FileReader(File file)`:创建一个字符输入流与源文件对象接通。 +* `public FileReader(String filePath)`:创建一个字符输入流与源文件路径接通。 + +方法: + +* `public int read()`:读取一个字符的编号返回,读取完毕返回 -1 +* `public int read(char[] buffer)`:读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 + +结论: + +* 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件,但是一个一个字符的读取文本内容性能较差 +* 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好 - * 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去。 - * 构造器: - `public FileReader(File file)` : 创建一个字符输入流与源文件对象接通。 - `public FileReader(String filePath)` : 创建一个字符输入流与源文件路径接通。 - * 方法: - `public int read()` : 读取一个字符的编号返回! 读取完毕返回 -1 - `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 - * 结论: - 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件。 - 但是:一个一个字符的读取文本内容性能较差!! - 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好!! - * **字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去map这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 +**字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去 map 这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 ```java public class FileReaderDemo01{//字符 public static void main(String[] args) throws Exception { - // 1.创建一个文件对象定位源文件 - // File f = new File("Day10Demo/src/dlei01.txt"); - // 2.创建一个字符输入流管道与源文件接通 - // Reader fr = new FileReader(f); - // 3.简化写法:创建一个字符输入流管道与源文件路径接通 - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); - //int code1 = fr.read(); - //System.out.print((char)code1); + // 创建一个字符输入流管道与源文件路径接通 + Reader fr = new FileReader("Demo/src/test.txt"); int ch; while((ch = fr.read()) != -1){ System.out.print((char)ch); @@ -7409,13 +7309,8 @@ public class FileReaderDemo01{//字符 } public class FileReaderDemo02 {//字符数组 public static void main(String[] args) throws Exception { - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); - //char[] buffer = new char[3]; - //int len = fr.read(buffer); - //System.out.println("字符数:"+len); - //String rs = new String(buffer,0,len); - //System.out.println(rs); char[] buffer = new char[1024]; int len; while((len = fr.read(buffer)) != -1) { @@ -7427,30 +7322,33 @@ public class FileReaderDemo02 {//字符数组 +*** + + + ##### 字符输出 -FileWriter:文件字符输出流 - -* 作用:以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 -* 构造器: - `public FileWriter(File file)` : 创建一个字符输出流管道通向目标文件对象 - `public FileWriter(String filePath)` : 创建一个字符输出流管道通向目标文件路径 - `public FileWriter(File file,boolean append)` : 创建一个追加数据的字符输出流管道通向文件对象 - `public FileWriter(String filePath,boolean append)` : 创建一个追加数据的字符输出流管道通向目标文件路径 -* 方法: - `public void write(int c)` : 写一个字符出去 - `public void write(String c)` : 写一个字符串出去 - `public void write(char[] buffer)` : 写一个字符数组出去 - `public void write(String c ,int pos ,int len)` : 写字符串的一部分出去 - `public void write(char[] buffer ,int pos ,int len)` : 写字符数组的一部分出去 -* 说明: - 覆盖数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt")` - 追加数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt",true)` - 换行:fw.write("\r\n"); // 换行 - 读写字符文件数据建议使用字符流 - -```java -Writer fw = new FileWriter("Day10Demo/src/dlei03.txt"); +FileWriter:文件字符输出流,以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 + +构造器: + +* `public FileWriter(File file)`:创建一个字符输出流管道通向目标文件对象(覆盖数据管道) +* `public FileWriter(String filePath)`:创建一个字符输出流管道通向目标文件路径 +* `public FileWriter(File file, boolean append)`:创建一个追加数据的字符输出流管道通向文件对象(追加数据管道) +* `public FileWriter(String filePath, boolean append)`:创建一个追加数据的字符输出流管道通向目标文件路径 + +方法: + +* `public void write(int c)`:写一个字符出去 +* `public void write(char[] buffer)`:写一个字符数组出去 +* `public void write(String c, int pos, int len)`:写字符串的一部分出去 +* `public void write(char[] buffer, int pos, int len)`:写字符数组的一部分出去 +* `fw.write("\r\n")`:换行 + +读写字符文件数据建议使用字符流 + +```java +Writer fw = new FileWriter("Demo/src/test.txt"); fw.write(97); // 字符a fw.write('b'); // 字符b fw.write("Java是最优美的语言!"); @@ -7468,14 +7366,14 @@ fw.close; ##### 基本介绍 -作用:缓冲流可以提高字节流和字符流的读写数据的性能。 +缓冲流可以提高字节流和字符流的读写数据的性能 缓冲流分为四类: -* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 -* BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 -* BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 -* BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 +* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能 +* BufferedOutStream:字节缓冲输出流,可以提高字节输出流写数据的性能 +* BufferedReader:字符缓冲输入流,可以提高字符输入流读数据的性能 +* BufferedWriter:字符缓冲输出流,可以提高字符输出流写数据的性能 @@ -7487,7 +7385,7 @@ fw.close; 字节缓冲输入流:BufferedInputStream -作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 +作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,提高字节输入流读数据的性能 构造器:`public BufferedInputStream(InputStream in)` @@ -7497,7 +7395,7 @@ fw.close; public class BufferedInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.定义一个低级的字节输入流与源文件接通 - InputStream is = new FileInputStream("Day10Demo/src/dlei04.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。 BufferInputStream bis = new BufferInputStream(is); // 3.定义一个字节数组按照循环读取。 @@ -7525,13 +7423,13 @@ public class BufferedInputStreamDemo01 { 构造器:`public BufferedOutputStream(OutputStream os)` -原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了 +原理:缓冲字节输出流自带了 8KB 缓冲池,数据就直接写入到缓冲池中去,性能提高了 ```java public class BufferedOutputStreamDemo02 { public static void main(String[] args) throws Exception { // 1.写一个原始的字节输出流 - OutputStream os = new FileOutputStream("Day10Demo/src/dlei05.txt"); + OutputStream os = new FileOutputStream("Demo/src/test.txt"); // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流 BufferedOutputStream bos = new BufferedOutputStream(os); // 3.写数据出去 @@ -7548,14 +7446,14 @@ public class BufferedOutputStreamDemo02 { ##### 字节流性能 -利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 +利用字节流的复制统计各种写法形式下缓冲流的性能执行情况 复制流: -* 使用低级的字节流按照一个一个字节的形式复制文件。 -* 使用低级的字节流按照一个一个字节数组的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 +* 使用低级的字节流按照一个一个字节的形式复制文件 +* 使用低级的字节流按照一个一个字节数组的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件 高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 @@ -7573,14 +7471,14 @@ public class BufferedOutputStreamDemo02 { 构造器:`public BufferedReader(Reader reader)` -原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能 +原理:缓冲字符输入流默认会有一个 8K 的字符缓冲池,可以提高读字符的性能 -按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回null +按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回 null ```java public static void main(String[] args) throws Exception { // 1.定义一个原始的字符输入流读取源文件 - Reader fr = new FileReader("Day10Demo/src/dlei06.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道 BufferedReader br = new BufferedReader(fr); // 定义一个字符串变量存储每行数据 @@ -7611,13 +7509,13 @@ public static void main(String[] args) throws Exception { 构造器:`public BufferedWriter(Writer writer)` - 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了 + 原理:高级的字符缓冲输出流多了一个 8K 的字符缓冲池,写数据性能极大提高了 字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** ```java public static void main(String[] args) throws Exception { - Writer fw = new FileWriter("Day10Demo/src/dlei07.txt",true);//追加 + Writer fw = new FileWriter("Demo/src/test.txt",true);//追加 BufferedWriter bw = new BufferedWriter(fw); bw.write("我爱学习Java"); @@ -7667,8 +7565,8 @@ GBK GBK 不乱码! UTF-8 GBK 乱码! ``` -如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码。 -如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码。 +* 如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码 +* 如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码 @@ -7684,8 +7582,8 @@ UTF-8 GBK 乱码! 构造器: -* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码 UTF-8 转换成字符流 -* `public InputStreamReader(InputStream is, String charset)` : 指定编码把字节流转换成字符流 +* `public InputStreamReader(InputStream is)`:使用当前代码默认编码 UTF-8 转换成字符流 +* `public InputStreamReader(InputStream is, String charset)`:指定编码把字节流转换成字符流 ```java public class InputStreamReaderDemo{ @@ -7719,11 +7617,11 @@ public class InputStreamReaderDemo{ 构造器: -* `public OutputStreamWriter(OutputStream os)` : 用默认编码 UTF-8 把字节输出流转换成字符输出流 -* `public OutputStreamWriter(OutputStream os, String charset)` : 指定编码把字节输出流转换成 +* `public OutputStreamWriter(OutputStream os)`:用默认编码 UTF-8 把字节输出流转换成字符输出流 +* `public OutputStreamWriter(OutputStream os, String charset)`:指定编码把字节输出流转换成 ```Java -OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt"); +OutputStream os = new FileOutputStream("Demo/src/test.txt"); OutputStreamWriter osw = new OutputStreamWriter(os,"GBK"); osw.write("我在学习Java"); osw.close(); @@ -7739,11 +7637,11 @@ osw.close(); ##### 基本介绍 -对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 +对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中。对象 => 文件中 -对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 +对象反序列化:把字节序列恢复为 Java 对象的过程,从 IO 流中恢复对象,文件中 => 对象 -transient 关键字修饰的成员变量,将不参与序列化! +transient 关键字修饰的成员变量,将不参与序列化 @@ -7755,7 +7653,7 @@ transient 关键字修饰的成员变量,将不参与序列化! 对象序列化流(对象字节输出流):ObjectOutputStream -作用:把内存中的Java对象数据保存到文件中去 +作用:把内存中的 Java 对象数据保存到文件中去 构造器:`public ObjectOutputStream(OutputStream out)` @@ -7769,7 +7667,7 @@ public class SerializeDemo01 { // 1.创建User用户对象 User user = new User("seazean","980823","七十一"); // 2.创建低级的字节输出流通向目标文件 - OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat"); + OutputStream os = new FileOutputStream("Demo/src/obj.dat"); // 3.把低级的字节输出流包装成高级的对象字节输出流 ObjectOutputStream ObjectOutputStream oos = new ObjectOutputStream(os); // 4.通过对象字节输出流序列化对象: @@ -7787,7 +7685,7 @@ class User implements Serializable { private String loginName; private transient String passWord; private String userName; - ///get+set + // get+set } ``` @@ -7812,19 +7710,20 @@ byte[] bytes = bos.toByteArray(); 对象反序列化(对象字节输入流):ObjectInputStream -作用:读取序列化的对象文件恢复到Java对象中 +作用:读取序列化的对象文件恢复到 Java 对象中 构造器:`public ObjectInputStream(InputStream is)` 方法:`public final Object readObject()` 序列化版本号:`private static final long serialVersionUID = 2L` -说明:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 + +注意:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 ```java public class SerializeDemo02 { public static void main(String[] args) throws Exception { - InputStream is = new FileInputStream("Day10Demo/src/obj.dat"); + InputStream is = new FileInputStream("Demo/src/obj.dat"); ObjectInputStream ois = new ObjectInputStream(is); User user = (User)ois.readObject();//反序列化 System.out.println(user); @@ -7859,15 +7758,14 @@ class User implements Serializable { * `public PrintStream(OutputStream os)` * `public PrintStream(String filepath)` -System类: +System 类: * `public static void setOut(PrintStream out)`:让系统的输出流向打印流 ```java public class PrintStreamDemo01 { public static void main(String[] args) throws Exception { - PrintStream ps = new PrintStream("Day10Demo/src/dlei.txt"); - //PrintWriter pw = new PrintWriter("Day10Demo/src/dlei08.txt"); + PrintStream ps = new PrintStream("Demo/src/test.txt"); ps.println(任何类型的数据); ps.print(不换行); ps.write("我爱你".getBytes()); @@ -7877,7 +7775,7 @@ public class PrintStreamDemo01 { public class PrintStreamDemo02 { public static void main(String[] args) throws Exception { System.out.println("==seazean0=="); - PrintStream ps = new PrintStream("Day10Demo/src/log.txt"); + PrintStream ps = new PrintStream("Demo/src/log.txt"); System.setOut(ps); // 让系统的输出流向打印流 //不输出在控制台,输出到文件里 System.out.println("==seazean1=="); @@ -7937,23 +7835,23 @@ try( ### Properties -Properties:属性集对象。就是一个Map集合,一个键值对集合 +Properties:属性集对象。就是一个 Map 集合,一个键值对集合 -核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 +核心作用:Properties 代表的是一个属性文件,可以把键值对数据存入到一个属性文件 -属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value +属性文件:后缀是 `.properties` 结尾的文件,里面的内容都是 key=value -Properties方法: +Properties 方法: -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------------------- | -| public Object setProperty(String key, String value) | 设置集合的键和值,底层调用Hashtable方法 put | -| public String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | -| public Set stringPropertyNames() | 所有键的名称的集合 | -| public synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | -| public synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | -| public void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties表 | -| public void store(OutputStream os, String comments) | 保存数据到属性文件中去 | +| 方法名 | 说明 | +| -------------------------------------------- | --------------------------------------------- | +| Object setProperty(String key, String value) | 设置集合的键和值,底层调用 Hashtable 方法 put | +| String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | +| Set stringPropertyNames() | 所有键的名称的集合 | +| synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | +| synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | +| void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties 表 | +| void store(OutputStream os, String comments) | 保存数据到属性文件中去 | ````java public class PropertiesDemo01 { @@ -7962,7 +7860,7 @@ public class PropertiesDemo01 { Properties properties = new Properties();//{} properties.setProperty("admin" , "123456"); // b.把属性集对象的数据存入到属性文件中去(重点) - OutputStream os = new FileOutputStream("Day10Demo/src/users.properties"); + OutputStream os = new FileOutputStream("Demo/src/users.properties"); properties.store(os,"i am very happy!!我保存了用户数据!"); //参数一:被保存数据的输出管道 //参数二:保存心得。就是对象保存的数据进行解释说明! @@ -7974,7 +7872,7 @@ public class PropertiesDemo01 { public class PropertiesDemo02 { public static void main(String[] args) throws Exception { Properties properties = new Properties();//底层基于map集合 - properties.load(new FileInputStream("Day10Demo/src/users.properties")); + properties.load(new FileInputStream("Demo/src/users.properties")); System.out.println(properties); System.out.println(properties.getProperty("admin")); @@ -7998,13 +7896,15 @@ public class PropertiesDemo02 { RandomAccessFile 类:该类的实例支持读取和写入随机访问文件 构造器: -RandomAccessFile(File file, String mode):创建随机访问文件流,从File参数指定的文件读取,可选择写入 -RandomAccessFile(String name, String mode):创建随机访问文件流,从指定名称文件读取,可选择写入文件 + +* `RandomAccessFile(File file, String mode)`:创建随机访问文件流,从 File 参数指定的文件读取,可选择写入 +* `RandomAccessFile(String name, String mode)`:创建随机访问文件流,从指定名称文件读取,可选择写入文件 常用方法: -`public void seek(long pos)` : 设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) -`public void write(byte[] b)` : 从指定的字节数组写入 b.length个字节到该文件 -`public int read(byte[] b)` : 从该文件读取最多b.length个字节的数据到字节数组 + +* `public void seek(long pos)`:设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) +* `public void write(byte[] b)`:从指定的字节数组写入 b.length 个字节到该文件 +* `public int read(byte[] b)`:从该文件读取最多 b.length 个字节的数据到字节数组 ```java public static void main(String[] args) throws Exception { @@ -8024,16 +7924,16 @@ public static void main(String[] args) throws Exception { ### Commons -commons-io 是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。 +commons-io 是 apache 提供的一组有关 IO 操作的类库,可以挺提高 IO 功能开发的效率。 commons-io 工具包提供了很多有关 IO 操作的类: -| 包 | 功能描述 | -| ----------------------------------- | :------------------------------------------- | -| org.apache.commons.io | 有关Streams、Readers、Writers、Files的工具类 | -| org.apache.commons.io.input | 输入流相关的实现类,包含Reader和InputStream | -| org.apache.commons.io.output | 输出流相关的实现类,包含Writer和OutputStream | -| org.apache.commons.io.serialization | 序列化相关的类 | +| 包 | 功能描述 | +| ----------------------------------- | :---------------------------------------------- | +| org.apache.commons.io | 有关 Streams、Readers、Writers、Files 的工具类 | +| org.apache.commons.io.input | 输入流相关的实现类,包含 Reader 和 InputStream | +| org.apache.commons.io.output | 输出流相关的实现类,包含 Writer 和 OutputStream | +| org.apache.commons.io.serialization | 序列化相关的类 | IOUtils 和 FileUtils 可以方便的复制文件和文件夹 @@ -8041,18 +7941,18 @@ IOUtils 和 FileUtils 可以方便的复制文件和文件夹 public class CommonsIODemo01 { public static void main(String[] args) throws Exception { // 1.完成文件复制! - IOUtils.copy(new FileInputStream("Day13Demo/src/books.xml"), - new FileOutputStream("Day13Demo/new.xml")); + IOUtils.copy(new FileInputStream("Demo/src/books.xml"), + new FileOutputStream("Demo/new.xml")); // 2.完成文件复制到某个文件夹下! - FileUtils.copyFileToDirectory(new File("Day13Demo/src/books.xml"), + FileUtils.copyFileToDirectory(new File("Demo/src/books.xml"), new File("D:/it")); // 3.完成文件夹复制到某个文件夹下! FileUtils.copyDirectoryToDirectory(new File("D:\\it\\图片服务器") , new File("D:\\")); // Java从1.7开始提供了一些nio, 自己也有一行代码完成复制的技术。 - Files.copy(Paths.get("Day13Demo/src/books.xml") - , new FileOutputStream("Day13Demo/new11.txt")); + Files.copy(Paths.get("Demo/src/books.xml") + , new FileOutputStream("Demo/new11.txt")); } } ``` @@ -8065,19 +7965,17 @@ public class CommonsIODemo01 { + + ## 反射 ### 测试框架 -> 单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。 -> 单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。 +单元测试的经典框架:Junit,是 Java 语言编写的第三方单元测试框架 -单元测试的经典框架:Junit - -* Junit : 是 Java 语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 -* 单元测试: - * 单元:在 Java 中,一个类就是一个单元 - * 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 +单元测试: +* 单元:在 Java 中,一个类就是一个单元 +* 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 Junit 单元测试框架的作用: @@ -8086,14 +7984,14 @@ Junit 单元测试框架的作用: 测试方法注意事项:**必须是 public 修饰的,没有返回值,没有参数,使用注解@Test修饰** -Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: +Junit常用注解(Junit 4.xxxx 版本),@Test 测试方法: * @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 * @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 * @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 -Junit常用注解(Junit5.xxxx版本),@Test 测试方法: +Junit 常用注解(Junit5.xxxx 版本),@Test 测试方法: * @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 @@ -8180,7 +8078,7 @@ public class UserServiceTest { 核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分 -反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class +反射提供了一个 Class 类型:HelloWorld.java → javac → HelloWorld.class * `Class c = HelloWorld.class` @@ -8188,16 +8086,16 @@ public class UserServiceTest { 作用:可以在运行时得到一个类的全部成分然后操作,破坏封装性,也可以破坏泛型的约束性。 -**反射的优点:** +反射的优点: - 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类 - 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员,可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码 -- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 +- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员,测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 -**反射的缺点:** +反射的缺点: - 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 -- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了 +- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行 - 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 @@ -8260,9 +8158,9 @@ class Student{} 获取构造器的 API: * Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿 public 修饰的构造器 -* **Constructor getDeclaredConstructor(Class... parameterTypes)**:根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor getDeclaredConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 * Constructor[] getConstructors():获取所有的构造器,只能拿 public 修饰的构造器 -* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor[] getDeclaredConstructors():获取所有构造器,只要申明就可以定位,不关心权限修饰符 Constructor 的常用 API: @@ -8356,12 +8254,12 @@ public class TestStudent02 { #### 获取变量 -获取Field成员变量API: +获取 Field 成员变量 API: -* Field getField(String name) : 根据成员变量名获得对应 Field 对象,只能获得 public 修饰 -* Field getDeclaredField(String name) : 根据成员变量名获得对应 Field 对象,所有申明的变量 -* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得 public 的 -* Field[] getDeclaredFields() : 获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 +* Field getField(String name):根据成员变量名获得对应 Field 对象,只能获得 public 修饰 +* Field getDeclaredField(String name):根据成员变量名获得对应 Field 对象,所有申明的变量 +* Field[] getFields():获得所有的成员变量对应的 Field 对象,只能获得 public 的 +* Field[] getDeclaredFields():获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 Field 的方法:给成员变量赋值和取值 @@ -8440,6 +8338,10 @@ public class FieldDemo02 { +*** + + + #### 获取方法 获取 Method 方法 API: @@ -8546,10 +8448,9 @@ public class ReflectDemo { 注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 -* 注解是 JDK1.5 的新特性 * 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 * 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 -* 父类中的注解是不能被子类继承的 +* **父类中的注解是不能被子类继承的** 注解作用: @@ -8665,29 +8566,25 @@ public class AnnotationDemo01{ 元注解有四个: -* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方 - - 可使用的值定义在ElementType枚举类中: +* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方,可用值定义在 ElementType 类中: - `ElementType.CONSTRUCTOR`:用于描述构造器 - - `ElementType.FIELD`:成员变量、对象、属性(包括enum实例) + - `ElementType.FIELD`:成员变量、对象、属性(包括 enum 实例) - `ElementType.LOCAL_VARIABLE`:用于描述局部变量 - `ElementType.METHOD`:用于描述方法 - `ElementType.PACKAGE`:用于描述包 - `ElementType.PARAMETER`:用于描述参数 - - `ElementType.TYPE`:用于描述类、接口(包括注解类型) 或enum声明 - -* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时 - - 可使用的值定义在RetentionPolicy枚举类中: + - `ElementType.TYPE`:用于描述类、接口(包括注解类型)或 enum 声明 + +* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时,可使用的值定义在 RetentionPolicy 枚举类中: - - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在。`@Override`, `@SuppressWarnings`都属于这类注解 + - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在,`@Override`、`@SuppressWarnings` 都属于这类注解 - `RetentionPolicy.CLASS`:在类加载时丢弃,在字节码文件的处理中有用,运行阶段不存在,默认值 - `RetentionPolicy.RUNTIME` : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式 - + * @Inherited:表示修饰的自定义注解可以被子类继承 -* @Documented:表示是否将自定义的注解信息添加在 java 文档中 +* @Documented:表示是否将自定义的注解信息添加在 Java 文档中 ```java public class AnnotationDemo01{ @@ -8816,10 +8713,14 @@ public class TestDemo{ + + **** + + ## XML ### 概述 @@ -8966,11 +8867,11 @@ XML 文件中常见的组成元素有:文档声明、元素、属性、注释、 #### DTD -##### DTD定义 +##### DTD 定义 -DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 +DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。 -##### DTD规则 +DTD 规则: * 约束元素的嵌套层级 @@ -9066,7 +8967,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD引入 +##### DTD 引入 * 引入本地 dtd @@ -9142,7 +9043,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD实现 +##### DTD 实现 persondtd.dtd 文件 @@ -9179,13 +9080,13 @@ persondtd.dtd 文件 #### Schema -##### XSD定义 +##### XSD 定义 1. Schema 语言也可作为 XSD(XML Schema Definition) -2. Schema 约束文件本身也是一个 xml 文件,符合 xml 的语法,这个文件的后缀名 .xsd -3. 一个 xml 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) +2. Schema 约束文件本身也是一个 XML 文件,符合 XML 的语法,这个文件的后缀名 .xsd +3. 一个 XML 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) 4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 -5. **Schema 文件约束 xml 文件的同时也被别的文件约束着** +5. **Schema 文件约束 XML 文件的同时也被别的文件约束着** @@ -9193,7 +9094,7 @@ persondtd.dtd 文件 -##### XSD规则 +##### XSD 规则 1. 创建一个文件,这个文件的后缀名为 .xsd 2. 定义文档声明 @@ -9244,7 +9145,7 @@ person.xsd -##### XSD引入 +##### XSD 引入 1. 在根标签上定义属性 xmlns="http://www.w3.org/2001/XMLSchema-instance" 2. **通过 xmlns 引入约束文件的名称空间** @@ -9273,7 +9174,7 @@ person.xsd -##### XSD属性 +##### XSD 属性 ```scheme @@ -9339,12 +9240,12 @@ DOM(Document Object Model):文档对象模型,把文档的各个组成 Dom4J 实现: * Dom4J 解析器构造方法:`SAXReader saxReader = new SAXReader()` -* SAXReader 常用API: +* SAXReader 常用 API: * `public Document read(File file)`:Reads a Document from the given File * `public Document read(InputStream in)`:Reads a Document from the given stream using SAX -* Java Class 类API: +* Java Class 类 API: * `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 @@ -9401,6 +9302,10 @@ public class Dom4JDemo { +**** + + + #### 子元素 Element 元素的 API: @@ -9620,92 +9525,126 @@ public class XPathDemo { +**** -*** +## SDP +### 单例模式 -# JVM +#### 基本介绍 -## JVM概述 +创建型模式的主要关注点是怎样创建对象,将对象的创建与使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节 -### 基本介绍 +创建型模式分为:单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式 -JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 -特点: +单例设计模式分类两种: -* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 -* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +* 饿汉式:类加载就会导致该单实例对象被创建 -Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +* 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 -JVM 结构: - -JVM、JRE、JDK 对比: +*** - +#### 饿汉式 -参考书籍:https://book.douban.com/subject/34907497/ +饿汉式在类加载的过程导致该单实例对象被创建,**虚拟机会保证类加载的线程安全**,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 -参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +* 静态变量的方式: -参考视频:https://www.bilibili.com/video/BV1yE411Z7AP + ```java + public final class Singleton { + // 私有构造方法 + private Singleton() {} + // 在成员位置创建该类的对象 + private static final Singleton instance = new Singleton(); + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + + // 解决序列化问题 + protected Object readResolve() { + return INSTANCE; + } + } + ``` + * 加 final 修饰,所以不会被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 + * 防止反序列化破坏单例的方式: -*** + * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 + 条件:访问权限为 private/protected、返回值必须是 Object、异常可以不抛 + * 实现 readResolve() 方法,当 JVM 从内存中反序列化地组装一个新对象,就会自动调用 readResolve 方法返回原来单例 -### 架构模型 + * 构造方法设置为私有,防止其他类无限创建对象,但是不能防止反射破坏 -Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 + * 静态变量初始化在类加载时完成,由 JVM 保证线程安全,能保证单例对象创建时的安全 -* 基于栈式架构的特点: - * 设计和实现简单,适用于资源受限的系统 - * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 - * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 - * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 - * 不需要硬件的支持,可移植性更好,更好实现跨平台 -* 基于寄存器架构的特点: - * 需要硬件的支持,可移植性差 - * 性能更好,执行更高效,寄存器比内存快 - * 以一地址指令、二地址指令、三地址指令为主 + * 提供静态方法而不是直接将 INSTANCE 设置为 public,体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计 +* 静态代码块的方式: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + static { + instance = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + } + ``` -*** +* 枚举方式:枚举类型是所用单例实现中**唯一一种不会被破坏**的单例实现模式 + ```java + public enum Singleton { + INSTANCE; + public void doSomething() { + System.out.println("doSomething"); + } + } + public static void main(String[] args) { + Singleton.INSTANCE.doSomething(); + } + ``` + * 问题1:枚举单例是如何限制实例个数的?每个枚举项都是一个实例,是一个静态成员变量 + * 问题2:枚举单例在创建时是否有并发问题?否 + * 问题3:枚举单例能否被反射破坏单例?否,反射创建对象时判断是枚举类型就直接抛出异常 + * 问题4:枚举单例能否被反序列化破坏单例?否 + * 问题5:枚举单例属于懒汉式还是饿汉式?**饿汉式** + * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 -### 生命周期 + 反编译结果: -JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 + ```java + public final class Singleton extends java.lang.Enum { // Enum实现序列化接口 + public static final Singleton INSTANCE = new Singleton(); + } + ``` -- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 -- **运行**: - - - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - - Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 -- **死亡**: - - 当程序中的用户线程都中止,JVM 才会退出 - - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 - - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 - - @@ -9713,36 +9652,78 @@ JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +#### 懒汉式 +* 线程不安全 -## 内存结构 - -### 内存概述 - -内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance == null) { + // 多线程环境,会出现线程安全问题,可能多个线程同时进入这里 + instance = new Singleton(); + } + return instance; + } + } + ``` -JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 +* 双端检锁机制 -* Java1.8 以前的内存结构图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) + 在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 -* Java1.8 之后的内存结果图: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + private static volatile Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 + if(instance == null) { + synchronized (Singleton.class) { + // 抢到锁之后再次判断是否为null + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } + } + ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) +* 静态内部类方式 -线程运行诊断: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 定位:jps 定位进程 id -* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 + * 内部类属于懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 -常见OOM错误: + 类加载的时候方法不会被调用,所以不会触发 getInstance 方法调用 invokestatic 指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池,解析工作是将常量池中的符号引用解析成直接引用,但是解析过程不一定非得在类加载时完成,可以延迟到运行时进行,所以静态内部类实现单例会**延迟加载** -* java.lang.StackOverflowError -* java.lang.OutOfMemoryError:java heap space -* java.lang.OutOfMemoryError:GC overhead limit exceeded -* java.lang.OutOfMemoryError:Direct buffer memory -* java.lang.OutOfMemoryError:unable to create new native thread -* java.lang.OutOfMemoryError:Metaspace + * 没有线程安全问题,静态变量初始化在类加载时完成,由 JVM 保证线程安全 @@ -9750,47 +9731,100 @@ JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理 -### JVM内存 - -#### 虚拟机栈 - -##### Java栈 - -Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 - -* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) +#### 破坏单例 -* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** +##### 反序列化 -* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 +将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,**反序列化得到的对象不执行构造器** -* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: +* Singleton - * 局部变量表:存储方法里的java基本数据类型以及对象的引用 - * 动态链接:也叫指向运行时常量池的方法引用 - * 方法返回地址:方法正常退出或者异常退出的定义 - * 操作数栈或表达式栈和其他一些附加信息 + ```java + public class Singleton implements Serializable { //实现序列化接口 + // 私有构造方法 + private Singleton() {} + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } - - -设置栈内存大小:`-Xss size` `-Xss 1024k` + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M +* 序列化 -虚拟机栈特点: + ```java + public class Test { + public static void main(String[] args) throws Exception { + //往文件中写对象 + //writeObject2File(); + //从文件中读取对象 + Singleton s1 = readObjectFromFile(); + Singleton s2 = readObjectFromFile(); + //判断两个反序列化后的对象是否是同一个对象 + System.out.println(s1 == s2); + } + + private static Singleton readObjectFromFile() throws Exception { + //创建对象输入流对象 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C://a.txt")); + //第一个读取Singleton对象 + Singleton instance = (Singleton) ois.readObject(); + return instance; + } + + public static void writeObject2File() throws Exception { + //获取Singleton类的对象 + Singleton instance = Singleton.getInstance(); + //创建对象输出流 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C://a.txt")); + //将instance对象写出到文件中 + oos.writeObject(instance); + } + } + ``` -* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 解决方法: -* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) + 在 Singleton 类中添加 `readResolve()` 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 -* 方法内的局部变量是否**线程安全**: - * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) - * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 + ```java + private Object readResolve() { + return SingletonHolder.INSTANCE; + } + ``` -异常: + ObjectInputStream 类源码分析: -* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 -* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 + ```java + public final Object readObject() throws IOException, ClassNotFoundException{ + //... + Object obj = readObject0(false);//重点查看readObject0方法 + } + + private Object readObject0(boolean unshared) throws IOException { + try { + switch (tc) { + case TC_OBJECT: + return checkResolve(readOrdinaryObject(unshared)); + } + } + } + private Object readOrdinaryObject(boolean unshared) throws IOException { + // isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类 + obj = desc.isInstantiable() ? desc.newInstance() : null; + // 添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true + if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { + // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 + // 多次调用ObjectInputStream类中的readObject方法,本质调用定义的readResolve方法,返回的是同一个对象。 + Object rep = desc.invokeReadResolve(obj); + } + return obj; + } + ``` @@ -9798,21 +9832,62 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 局部变量 +##### 反射破解 -局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 +* 反射 -* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 -* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 -* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 -* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 + ```java + public class Test { + public static void main(String[] args) throws Exception { + //获取Singleton类的字节码对象 + Class clazz = Singleton.class; + //获取Singleton类的私有无参构造方法对象 + Constructor constructor = clazz.getDeclaredConstructor(); + //取消访问检查 + constructor.setAccessible(true); + + //创建Singleton类的对象s1 + Singleton s1 = (Singleton) constructor.newInstance(); + //创建Singleton类的对象s2 + Singleton s2 = (Singleton) constructor.newInstance(); + + //判断通过反射创建的两个Singleton对象是否是同一个对象 + System.out.println(s1 == s2); //false + } + } + ``` + +* 反射方式破解单例的解决方法: + + ```java + public class Singleton { + private static volatile Singleton instance; + + // 私有构造方法 + private Singleton() { + // 反射破解单例模式需要添加的代码 + if(instance != null) { + throw new RuntimeException(); + } + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance != null) { + return instance; + } + synchronized (Singleton.class) { + if(instance != null) { + return instance; + } + instance = new Singleton(); + return instance; + } + } + } + ``` -局部变量表最基本的存储单元是 **slot(变量槽)**: -* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 -* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 -* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot -* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -9820,97 +9895,172 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 操作数栈 +#### Runtime -栈:可以使用数组或者链表来实现 +Runtime 类就是使用的单例设计模式中的饿汉式 -操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) +```java +public class Runtime { + private static Runtime currentRuntime = new Runtime(); + public static Runtime getRuntime() { + return currentRuntime; + } + private Runtime() {} + ... +} +``` -* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 +使用 Runtime -* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 -* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** +```java +public class RuntimeDemo { + public static void main(String[] args) throws IOException { + //获取Runtime类对象 + Runtime runtime = Runtime.getRuntime(); -栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 + //返回 Java 虚拟机中的内存总量。 + System.out.println(runtime.totalMemory()); + //返回 Java 虚拟机试图使用的最大内存量。 + System.out.println(runtime.maxMemory()); -基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 + //创建一个新的进程执行指定的字符串命令,返回进程对象 + Process process = runtime.exec("ipconfig"); + //获取命令执行后的结果,通过输入流获取 + InputStream inputStream = process.getInputStream(); + byte[] arr = new byte[1024 * 1024* 100]; + int b = inputStream.read(arr); + System.out.println(new String(arr,0,b,"gbk")); + } +} +``` -*** +**** -##### 动态链接 -动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** -* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 +### 代理模式 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) +#### 静态代理 -* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - - 常量池的作用:提供一些符号和常量,便于指令的识别 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) +代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 +Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在 Java 运行时动态生成,动态代理又有 JDK 代理和 CGLib 代理两种 +代理(Proxy)模式分为三种角色: -*** +* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 +* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 +* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 +买票案例,火车站是目标对象,代售点是代理对象 +* 卖票接口: -##### 返回地址 + ```java + public interface SellTickets { + void sell(); + } + ``` -Return Address:存放调用该方法的 PC 寄存器的值 +* 火车站,具有卖票功能,需要实现SellTickets接口 -方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 + ```java + public class TrainStation implements SellTickets { + public void sell() { + System.out.println("火车站卖票"); + } + } + ``` -* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** -* 异常:返回地址是要通过异常表来确定 +* 代售点: -正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 + ```java + public class ProxyPoint implements SellTickets { + private TrainStation station = new TrainStation(); + + public void sell() { + System.out.println("代理点收取一些服务费用"); + station.sell(); + } + } + ``` -异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 +* 测试类: -两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 + ```java + public class Client { + public static void main(String[] args) { + ProxyPoint pp = new ProxyPoint(); + pp.sell(); + } + } + ``` + 测试类直接访问的是 ProxyPoint 类对象,也就是 ProxyPoint 作为访问对象和目标对象的中介 -##### 附加信息 -栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 +**** -*** +#### JDK +##### 使用方式 +Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类,而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象 -#### 本地方法栈 +`static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` -本地方法栈是为虚拟机执行本地方法时提供服务的 +* 参数一:类加载器,负责加载代理类。传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 -JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 -* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 参数三:代理真正的执行方法,也就是代理的处理逻辑 -* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 +代码实现: -* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 +* 代理工厂:创建代理对象 -* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 + ```java + public class ProxyFactory { + private TrainStation station = new TrainStation(); + //也可以在参数中提供 getProxyObject(TrainStation station) + public SellTickets getProxyObject() { + //使用 Proxy 获取代理对象 + SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance( + station.getClass().getClassLoader(), + station.getClass().getInterfaces(), + new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + System.out.println("代理点(JDK动态代理方式)"); + //执行真实对象 + Object result = method.invoke(station, args); + return result; + } + }); + return sellTickets; + } + } + ``` - * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** - * 直接从本地内存的堆中分配任意数量的内存 - * 可以直接使用本地处理器中的寄存器 - - - - +* 测试类: -图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md + ```java + public class Client { + public static void main(String[] args) { + //获取代理对象 + ProxyFactory factory = new ProxyFactory(); + //必须时代理ji + SellTickets proxyObject = factory.getProxyObject(); + proxyObject.sell(); + } + } + ``` @@ -9918,185 +10068,292 @@ JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可 -#### 程序计数器 - -Program Counter Register 程序计数器(寄存器) +##### 实现原理 -作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) +JDK 动态代理方式的优缺点: -原理: +- 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 +- 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 +- 原因:**生成的代理类继承了 Proxy**,Java 是单继承的,所以 JDK 动态代理只能代理接口 -* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 -* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: -特点: +* 代理类($Proxy0)实现了 SellTickets 接口,真实类和代理类实现同样的接口 +* 代理类($Proxy0)将提供了的匿名内部类对象传递给了父类 +* 代理类($Proxy0)的修饰符是 public final -* 是线程私有的 -* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC +```java +// 程序运行过程中动态生成的代理类 +public final class $Proxy0 extends Proxy implements SellTickets { + private static Method m3; -Java 反编译指令:`javap -v Test.class` + public $Proxy0(InvocationHandler invocationHandler) { + super(invocationHandler);//InvocationHandler对象传递给父类 + } -#20:代表去 Constant pool 查看该地址的指令 + static { + m3 = Class.forName("proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); + } -```java -0: getstatic #20 // PrintStream out = System.out; -3: astore_1 // -- -4: aload_1 // out.println(1); -5: iconst_1 // -- -6: invokevirtual #26 // -- -9: aload_1 // out.println(2); -10: iconst_2 // -- -11: invokevirtual #26 // -- + public final void sell() { + // 调用InvocationHandler的invoke方法 + this.h.invoke(this, m3, null); + } +} + +// Java提供的动态代理相关类 +public class Proxy implements java.io.Serializable { + protected InvocationHandler h; + + protected Proxy(InvocationHandler h) { + this.h = h; + } +} ``` +执行流程如下: + +1. 在测试类中通过代理对象调用 sell() 方法 +2. 根据多态的特性,执行的是代理类($Proxy0)中的 sell() 方法 +3. 代理类($Proxy0)中的 sell() 方法中又调用了 InvocationHandler 接口的子实现类对象的 invoke 方法 +4. invoke 方法通过反射执行了真实对象所属类(TrainStation)中的 sell() 方法 + **** -#### 堆 +##### 源码解析 -Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 +```java +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h){ + // InvocationHandler 为空则抛出异常 + Objects.requireNonNull(h); -存放哪些资源: + // 复制一份 interfaces + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } -* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 -* 字符串常量池: - * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 - * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table -* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 -* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 + // 从缓存中查找 class 类型的代理对象,会调用 ProxyClassFactory#apply 方法 + Class cl = getProxyClass0(loader, intfs); + //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) + + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } -设置堆内存指令:`-Xmx Size` + // 获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + // 构造方法不是 pubic 的需要启用权限,暴力p + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + // 设置可访问的权限 + cons.setAccessible(true); + return null; + } + }); + } + // cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) {} +} +``` -内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 +Proxy 的静态内部类: -堆内存诊断工具:(控制台命令) +```java +private static final class ProxyClassFactory { + // 代理类型的名称前缀 + private static final String proxyClassNamePrefix = "$Proxy"; -1. jps:查看当前系统中有哪些 java 进程 -2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` -3. jconsole:图形界面的,多功能的监测工具,可以连续监测 + // 生成唯一数字使用,结合上面的代理类型名称前缀一起生成 + private static final AtomicLong nextUniqueNumber = new AtomicLong(); -在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: + //参数一:Proxy.newInstance 时传递的 + //参数二:Proxy.newInstance 时传递的接口集合 + @Override + public Class apply(ClassLoader loader, Class[] interfaces) { + + Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); + // 遍历接口集合 + for (Class intf : interfaces) { + Class interfaceClass = null; + try { + // 加载接口类到 JVM + interfaceClass = Class.forName(intf.getName(), false, loader); + } catch (ClassNotFoundException e) { + } + if (interfaceClass != intf) { + throw new IllegalArgumentException( + intf + " is not visible from class loader"); + } + // 如果 interfaceClass 不是接口 直接报错,保证集合内都是接口 + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException( + interfaceClass.getName() + " is not an interface"); + } + // 保证接口 interfaces 集合中没有重复的接口 + if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "repeated interface: " + interfaceClass.getName()); + } + } -* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 -* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 -* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 + // 生成的代理类的包名 + String proxyPkg = null; + // 【生成的代理类访问修饰符 public final】 + int accessFlags = Modifier.PUBLIC | Modifier.FINAL; -分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 + // 检查接口集合内的接口,看看有没有某个接口的访问修饰符不是 public 的 如果不是 public 的接口, + // 生成的代理类 class 就必须和它在一个包下,否则访问出现问题 + for (Class intf : interfaces) { + // 获取访问修饰符 + int flags = intf.getModifiers(); + if (!Modifier.isPublic(flags)) { + accessFlags = Modifier.FINAL; + // 获取当前接口的全限定名 包名.类名 + String name = intf.getName(); + int n = name.lastIndexOf('.'); + // 获取包名 + String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); + if (proxyPkg == null) { + proxyPkg = pkg; + } else if (!pkg.equals(proxyPkg)) { + throw new IllegalArgumentException( + "non-public interfaces from different packages"); + } + } + } -```java -public static void main(String[] args) { - //返回Java虚拟机中的堆内存总量 - long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; - //返回Java虚拟机使用的最大堆内存量 - long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; - - System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M - System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M + if (proxyPkg == null) { + // if no non-public proxy interfaces, use com.sun.proxy package + proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; + } + + // 获取唯一的编号 + long num = nextUniqueNumber.getAndIncrement(); + // 包名+ $proxy + 数字,比如 $proxy1 + String proxyName = proxyPkg + proxyClassNamePrefix + num; + + // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 + byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); + try { + // 【使用加载器加载二进制到 jvm】,并且返回 class + return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); + } catch (ClassFormatError e) { } + } } ``` -*** +*** -#### 方法区 -方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** +#### CGLIB -方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) +CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充($$Proxy) -方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 +* CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: -为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** + ```xml + + cglib + cglib + 2.2.2 + + ``` -类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +* 代理工厂类: -常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 + ```java + public class ProxyFactory implements MethodInterceptor { + private TrainStation target = new TrainStation(); + + public TrainStation getProxyObject() { + //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数 + Enhancer enhancer = new Enhancer(); + //设置父类的字节码对象 + enhancer.setSuperclass(target.getClass()); + //设置回调函数 + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)"); + Object o = methodProxy.invokeSuper(obj, args); + return null;//因为返回值为void + } + }); + //创建代理对象 + TrainStation obj = (TrainStation) enhancer.create(); + return obj; + } + } + ``` -- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 -- 符号引用:类、字段、方法、接口等的符号引用 +CGLIB 的优缺点 -运行时常量池是方法区的一部分 +* 优点: + * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 + * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 + * **JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强**,包括 Object 类中的方法,toString、hashCode 等 +* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是**动态生成被代理类的子类,继承被代理类** -* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 -* 类在解析阶段将这些符号引用替换成直接引用 -* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -*** +**** -### 本地内存 -#### 基本介绍 +#### 方式对比 -虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +三种方式对比: -本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +* 动态代理和静态代理: -本地内存概述图: + * 动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,不需要像静态代理那样每一个方法进行中转 - + * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 + * 动态代理是程序**在运行后通过反射创建字节码文件**交由 JVM 加载 +* JDK 代理和 CGLIB 代理: + JDK 动态代理采用 ProxyGenerator.generateProxyClass() 方法在运行时生成字节码;CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口或者当前类就是接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 -*** +代理模式的优缺点: +* 优点: + * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 + * **代理对象可以增强目标对象的功能,内部持有原始的目标对象** + * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 +* 缺点:增加了系统的复杂度 -#### 元空间 +代理模式的使用场景: -PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 - -元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 +* 远程(Remote)代理:本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 -方法区内存溢出: +* 防火墙(Firewall)代理:当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 -* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space +* 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 - ```sh - -XX:MaxPermSize=8m #参数设置 - ``` - -* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace - ```sh - -XX:MaxMetaspaceSize=8m #参数设置 - ``` -元空间内存溢出演示: -```java -public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 - public static void main(String[] args) { - int j = 0; - try { - Demo1_8 test = new Demo1_8(); - for (int i = 0; i < 10000; i++, j++) { - // ClassWriter 作用是生成类的二进制字节码 - ClassWriter cw = new ClassWriter(0); - // 版本号, public, 类名, 包名, 父类, 接口 - cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); - // 返回 byte[] - byte[] code = cw.toByteArray(); - // 执行了类的加载 - test.defineClass("Class" + i, code, 0, code.length); // Class 对象 - } - } finally { - System.out.println(j); - } - } -} -``` @@ -10104,172 +10361,193 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 -#### 直接内存 -直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 +# JVM +## JVM概述 -直接内存详解参考:NET → NIO → 直接内存 +### 基本介绍 +JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +特点: -*** +* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 +* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +JVM 结构: -### 变量位置 + -变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** +JVM、JRE、JDK 对比: -静态内部类和其他内部类: + -* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -类变量: +参考书籍:https://book.douban.com/subject/34907497/ -* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 -* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +参考视频:https://www.bilibili.com/video/BV1yE411Z7AP -实例变量: -* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 -* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** -局部变量: +*** -* 局部变量是定义在类的方法中的变量 -* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 -* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 -* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** -* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 +### 架构模型 -什么是字面量?什么是符号引用? +Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 -* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 +* 基于栈式架构的特点: + * 设计和实现简单,适用于资源受限的系统 + * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 + * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 + * 不需要硬件的支持,可移植性更好,更好实现跨平台 +* 基于寄存器架构的特点: + * 需要硬件的支持,可移植性差 + * 性能更好,执行更高效,寄存器比内存快 + * 以一地址指令、二地址指令、三地址指令为主 - ```java - int a = 1; //这个1便是字面量 - String b = "iloveu"; //iloveu便是字面量 - ``` -* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 +*** -*** +### 生命周期 +JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 +- **运行**: + + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 + - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 + - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 + + Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 +- **死亡**: + + - 当程序中的用户线程都中止,JVM 才会退出 + - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 + - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 -## 内存管理 -### 内存分配 -#### 两种方式 +*** -不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 -* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 -* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 -*** +## 内存结构 +### 内存概述 +内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 -#### 分代思想 +JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 -##### 分代介绍 +* Java1.8 以前的内存结构图: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) -Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +* Java1.8 之后的内存结果图: -- 新生代使用:复制算法 -- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) -**Minor GC 和 Full GC**: +线程运行诊断: -- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 -- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 +* 定位:jps 定位进程 id +* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 - Eden 和 Survivor 大小比例默认为 8:1:1 +常见OOM错误: - +* java.lang.StackOverflowError +* java.lang.OutOfMemoryError:java heap space +* java.lang.OutOfMemoryError:GC overhead limit exceeded +* java.lang.OutOfMemoryError:Direct buffer memory +* java.lang.OutOfMemoryError:unable to create new native thread +* java.lang.OutOfMemoryError:Metaspace +*** -*** +### JVM内存 +#### 虚拟机栈 -##### 分代分配 +##### Java栈 -工作机制: +Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 -* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC -* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 -* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 -* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 -* From 区和 To 区 也可以叫做 S0 区和 S1 区 +* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) -晋升到老年代: +* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** -* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 - - `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 -* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 - - `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 -* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 +* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 -空间分配担保: +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: -* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 -* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 + * 局部变量表:存储方法里的java基本数据类型以及对象的引用 + * 动态链接:也叫指向运行时常量池的方法引用 + * 方法返回地址:方法正常退出或者异常退出的定义 + * 操作数栈或表达式栈和其他一些附加信息 + + +设置栈内存大小:`-Xss size` `-Xss 1024k` +* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M -*** +虚拟机栈特点: +* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) -#### TLAB +* 方法内的局部变量是否**线程安全**: + * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) + * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 -TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** +异常: -- 栈上分配使用的是栈来进行对象内存的分配 -- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 +* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 -堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 -问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +*** -JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 -栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -参数设置: +##### 局部变量 -* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 -* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% -* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 +* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 +* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 +* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 +* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) +局部变量表最基本的存储单元是 **slot(变量槽)**: + +* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 +* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 +* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot +* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -10277,223 +10555,183 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 -#### 逃逸分析 +##### 操作数栈 -即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 +栈:可以使用数组或者链表来实现 -* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 -* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 +操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) -逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 -* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 - * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 - * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 -* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 +* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** -如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 +栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 -* 同步消除 +基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 - 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -* 标量替换 - * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 - * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 - - 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 - * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 - * 参数设置: - - * `-XX:+EliminateAllocations`:开启标量替换 - * `-XX:+PrintEliminateAllocations`:查看标量替换情况 +*** -* 栈上分配 - JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC - User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 +##### 动态链接 - ```java - public class JVM { - public static void main(String[] args) throws Exception { - int sum = 0; - int count = 1000000; - //warm up - for (int i = 0; i < count ; i++) { - sum += fn(i); - } - System.out.println(sum); - System.in.read(); - } - private static int fn(int age) { - User user = new User(age); - int i = user.getAge(); - return i; - } - } - - class User { - private final int age; +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** + +* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) + +* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - public User(int age) { - this.age = age; - } + 常量池的作用:提供一些符号和常量,便于指令的识别 - public int getAge() { - return age; - } - } - ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) - -*** +*** -### 回收策略 -#### 触发条件 +##### 返回地址 -内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Return Address:存放调用该方法的 PC 寄存器的值 -Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC +方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 -FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: +* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** +* 异常:返回地址是要通过异常表来确定 -* 调用 System.gc(): +正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 - * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 - * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() +异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 -* 老年代空间不足: +两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 - * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 -* 空间分配担保失败 -* JDK 1.7 及以前的永久代(方法区)空间不足 +##### 附加信息 -* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 -手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -```java -public void localvarGC1() { - byte[] buffer = new byte[10 * 1024 * 1024];//10MB - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 -} +*** -public void localvarGC2() { - byte[] buffer = new byte[10 * 1024 * 1024]; - buffer = null; - System.gc(); //输出: 正常被回收 -} - public void localvarGC3() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 - } -public void localvarGC4() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - int value = 10; - System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 -} -``` +#### 本地方法栈 +本地方法栈是为虚拟机执行本地方法时提供服务的 -*** +JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 -#### 安全区域 +* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 -安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 -- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 -- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 + * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** + * 直接从本地内存的堆中分配任意数量的内存 + * 可以直接使用本地处理器中的寄存器 + + + + -在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md -- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 -- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 +*** -运行流程: -- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 +#### 程序计数器 +Program Counter Register 程序计数器(寄存器) +作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) -*** +原理: +* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 +* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +特点: -### 垃圾判断 +* 是线程私有的 +* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC -#### 垃圾介绍 +Java 反编译指令:`javap -v Test.class` -垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** +#20:代表去 Constant pool 查看该地址的指令 -作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 +```java +0: getstatic #20 // PrintStream out = System.out; +3: astore_1 // -- +4: aload_1 // out.println(1); +5: iconst_1 // -- +6: invokevirtual #26 // -- +9: aload_1 // out.println(2); +10: iconst_2 // -- +11: invokevirtual #26 // -- +``` -垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +**** -*** +#### 堆 +Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 -#### 引用计数法 +存放哪些资源: -引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) +* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 +* 字符串常量池: + * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 + * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table +* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 +* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 -优点: +设置堆内存指令:`-Xmx Size` -- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 -- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 -- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 +内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 -缺点: +堆内存诊断工具:(控制台命令) -- 每次对象被引用时,都需要去更新计数器,有一点时间开销 +1. jps:查看当前系统中有哪些 java 进程 +2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` +3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 +在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 +* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 - ```java - public class Test { - public Object instance = null; - public static void main(String[] args) { - Test a = new Test();// a = 1 - Test b = new Test();// b = 1 - a.instance = b; // b = 2 - b.instance = a; // a = 2 - a = null; // a = 1 - b = null; // b = 1 - } - } - ``` +分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) +```java +public static void main(String[] args) { + //返回Java虚拟机中的堆内存总量 + long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; + //返回Java虚拟机使用的最大堆内存量 + long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; + + System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M + System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M +} +``` @@ -10501,159 +10739,164 @@ public void localvarGC4() { -#### 可达性分析 +#### 方法区 -##### GC Roots +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 +方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** -GC Roots 对象: +方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) -- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 -- 本地方法栈中引用的对象 -- 堆中类静态属性引用的对象 -- 方法区中的常量引用的对象 -- 字符串常量池(string Table)里的引用 -- 同步锁 synchronized 持有的对象 +方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 -**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 +为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** +类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 -*** +- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 +- 符号引用:类、字段、方法、接口等的符号引用 +运行时常量池是方法区的一部分 +* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 +* 类在解析阶段将这些符号引用替换成直接引用 +* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -##### 工作原理 -可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +*** -基本原理: -- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 +### 本地内存 -- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 +#### 基本介绍 - +虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +本地内存概述图: -*** + -##### 三色标记 +*** -###### 标记算法 -三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -- 白色:尚未访问过 -- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 -- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 - -当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: - -1. 初始时,所有对象都在白色集合 -2. 将 GC Roots 直接引用到的对象挪到灰色集合 -3. 从灰色集合中获取对象: - * 将本对象引用到的其他对象全部挪到灰色集合中 - * 将本对象挪到黑色集合里面 -4. 重复步骤 3,直至灰色集合为空时结束 -5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 - - +#### 元空间 +PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 +元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 -参考文章:https://www.jianshu.com/p/12544c0ad5c1 +方法区内存溢出: +* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space + ```sh + -XX:MaxPermSize=8m #参数设置 + ``` + +* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace -**** + ```sh + -XX:MaxMetaspaceSize=8m #参数设置 + ``` +元空间内存溢出演示: +```java +public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 + public static void main(String[] args) { + int j = 0; + try { + Demo1_8 test = new Demo1_8(); + for (int i = 0; i < 10000; i++, j++) { + // ClassWriter 作用是生成类的二进制字节码 + ClassWriter cw = new ClassWriter(0); + // 版本号, public, 类名, 包名, 父类, 接口 + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); + // 返回 byte[] + byte[] code = cw.toByteArray(); + // 执行了类的加载 + test.defineClass("Class" + i, code, 0, code.length); // Class 对象 + } + } finally { + System.out.println(j); + } + } +} +``` -###### 并发标记 -并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** +*** -* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 -* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 - -**漏标情况:** +#### 直接内存 -* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 -* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 -* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 +直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 - -代码角度解释漏标: -```java -Object G = objE.fieldG; // 读 -objE.fieldG = null; // 写 -objD.fieldG = G; // 写 -``` +直接内存详解参考:NET → NIO → 直接内存 -为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 -解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +*** -* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 - 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 - 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +### 变量位置 -* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 +变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 +静态内部类和其他内部类: - SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 +* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 +* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: +类变量: -- CMS:写屏障 + 增量更新 -- G1:写屏障 + SATB -- ZGC:读屏障 +* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 +* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +实例变量: -*** +* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 +* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** +局部变量: +* 局部变量是定义在类的方法中的变量 +* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -#### finalization +类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 +* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 +* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 +* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** +* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 -垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 +什么是字面量?什么是符号引用? -生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: +* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 -- 可触及的:从根节点开始,可以到达这个对象。 -- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 -- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 + ```java + int a = 1; //这个1便是字面量 + String b = "iloveu"; //iloveu便是字面量 + ``` -永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 -* finalize() 时可能会导致对象复活 -* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 -* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -10661,87 +10904,75 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -#### 引用分析 -无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 -1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 +## 内存管理 - * 强引用可以直接访问目标对象 - * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 - * 强引用可能导致**内存泄漏** +### 内存分配 - ```java - Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 - ``` +#### 两种方式 -2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 - * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 - * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 - * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 +* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 - ```java - Object obj = new Object(); - SoftReference sf = new SoftReference(obj); - obj = null; // 使对象只被软引用关联 - ``` -3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 - * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 - * 配合引用队列来释放弱引用自身 - * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM +*** - ```java - Object obj = new Object(); - WeakReference wf = new WeakReference(obj); - obj = null; - ``` -4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 - * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 - * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 - * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 +#### 分代思想 - ```java - Object obj = new Object(); - PhantomReference pf = new PhantomReference(obj, null); - obj = null; - ``` +##### 分代介绍 -5. 终结器引用(finalization) +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +- 新生代使用:复制算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 +**Minor GC 和 Full GC**: -*** +- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 +- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 + Eden 和 Survivor 大小比例默认为 8:1:1 + -#### 无用属性 -##### 无用类 -方法区主要回收的是无用的类 -判定一个类是否是无用的类,需要同时满足下面 3 个条件: -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 -- 加载该类的 `ClassLoader` 已经被回收 -- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 +*** -虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 +##### 分代分配 -*** +工作机制: +* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 +* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 +* From 区和 To 区 也可以叫做 S0 区和 S1 区 +晋升到老年代: -##### 废弃常量 +* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 + + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 +* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 + + `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 +* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 -在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 +空间分配担保: + +* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 +* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 @@ -10749,163 +10980,215 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -##### 静态变量 +#### TLAB -类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** -如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null +- 栈上分配使用的是栈来进行对象内存的分配 +- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 +问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 +栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -*** +参数设置: +* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% +* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 -### 回收算法 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) -#### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 -- 标记清除算法(Mark-Sweep) -- 复制算法(copying) -- 标记压缩算法(Mark-Compact) +*** -标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** -- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 +#### 逃逸分析 -算法缺点: +即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 -- 标记和清除过程效率都不高 -- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 +* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 +* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 - +逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 + * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 + * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 +* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 -*** - +* 同步消除 + 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -#### 复制算法 +* 标量替换 -复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 + * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 + * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 + + 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 + * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 + * 参数设置: + + * `-XX:+EliminateAllocations`:开启标量替换 + * `-XX:+PrintEliminateAllocations`:查看标量替换情况 -应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 +* 栈上分配 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) + JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC -算法优点: + User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 -- 没有标记和清除过程,实现简单,运行高效 -- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 + ```java + public class JVM { + public static void main(String[] args) throws Exception { + int sum = 0; + int count = 1000000; + //warm up + for (int i = 0; i < count ; i++) { + sum += fn(i); + } + System.out.println(sum); + System.in.read(); + } + private static int fn(int age) { + User user = new User(age); + int i = user.getAge(); + return i; + } + } + + class User { + private final int age; + + public User(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + } + ``` -算法缺点: + -- 主要不足是**只使用了内存的一半** -- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +*** -现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +### 回收策略 -*** +#### 触发条件 +内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC -#### 标记整理 +FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: -标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +* 调用 System.gc(): -标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() -优点:不会产生内存碎片 +* 老年代空间不足: -缺点:需要移动大量对象,处理效率比较低 + * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 - +* 空间分配担保失败 -| | Mark-Sweep | Mark-Compact | Copying | -| -------- | ---------------- | -------------- | ----------------------------------- | -| 速度 | 中等 | 最慢 | 最快 | -| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | -| 移动对象 | 否 | 是 | 是 | +* JDK 1.7 及以前的永久代(方法区)空间不足 -- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 -- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 +* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -*** +```java +public void localvarGC1() { + byte[] buffer = new byte[10 * 1024 * 1024];//10MB + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 +} +public void localvarGC2() { + byte[] buffer = new byte[10 * 1024 * 1024]; + buffer = null; + System.gc(); //输出: 正常被回收 +} + public void localvarGC3() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 + } +public void localvarGC4() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + int value = 10; + System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 +} +``` -#### 增量收集 -增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 -工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +*** -缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 +#### 安全区域 +安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 +- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 -*** +在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 +- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -### 垃圾回收器 +安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 -#### 概述 +运行流程: -垃圾收集器分类: +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 - * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 -* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 - * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 - * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 -* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 - * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 - * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 -* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 -GC 性能指标: -- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) -- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 -- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 -- 收集频率:相对于应用程序的执行,收集操作发生的频率 -- 内存占用:Java 堆区所占的内存大小 -- 快速:一个对象从诞生到被回收所经历的时间 -**垃圾收集器的组合关系**: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) -新生代收集器:Serial、ParNew、Paralle1 Scavenge; -老年代收集器:Serial old、Parallel old、CMS; +### 垃圾判断 -整堆收集器:G1 +#### 垃圾介绍 -* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** -查看默认的垃圾收回收器: +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 -* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID +在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -10913,209 +11196,199 @@ GC 性能指标: -#### Serial +#### 引用计数法 -Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) -**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 +优点: -**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 +- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 +- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 +- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 -- Serial old 是 Client 模式下默认的老年代的垃圾回收器 -- Serial old 在 Server 模式下主要有两个用途: - - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 - - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 +缺点: -开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC +- 每次对象被引用时,都需要去更新计数器,有一点时间开销 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) +- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 -优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 +- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) -缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 + ```java + public class Test { + public Object instance = null; + public static void main(String[] args) { + Test a = new Test();// a = 1 + Test b = new Test();// b = 1 + a.instance = b; // b = 2 + b.instance = a; // a = 2 + a = null; // a = 1 + b = null; // b = 1 + } + } + ``` +![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) -**** +*** -#### Parallel -Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +#### 可达性分析 -Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** +##### GC Roots -对比其他回收器: +可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 -* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 -* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 -* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +GC Roots 对象: -应用场景: +- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 +- 本地方法栈中引用的对象 +- 堆中类静态属性引用的对象 +- 方法区中的常量引用的对象 +- 字符串常量池(string Table)里的引用 +- 同步锁 synchronized 持有的对象 -* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 -* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 +**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 -停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +*** -参数配置: -* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 -* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 - * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 -* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 - * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 - * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] -* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 - * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 - * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 -* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 - * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 - * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 +##### 工作原理 +可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -*** +分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +基本原理: +- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -#### ParNew +- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 -Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 -并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 + -对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** - -相关参数: -* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 -* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) -ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 -- 对于新生代,回收次数频繁,使用并行方式高效 -- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) +##### 三色标记 +###### 标记算法 +三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -**** +- 白色:尚未访问过 +- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 +- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 +当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: +1. 初始时,所有对象都在白色集合 +2. 将 GC Roots 直接引用到的对象挪到灰色集合 +3. 从灰色集合中获取对象: + * 将本对象引用到的其他对象全部挪到灰色集合中 + * 将本对象挪到黑色集合里面 +4. 重复步骤 3,直至灰色集合为空时结束 +5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 -#### CMS + -CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 -分为以下四个流程: +参考文章:https://www.jianshu.com/p/12544c0ad5c1 -- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 -- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) -- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 -Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: -* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 +**** -* Mark Compact 更适合 Stop The World 场景 -在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) +###### 并发标记 -优点:并发收集、低延迟 +并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -缺点: +**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** -- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 -- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 - - 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 -- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 +* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 +* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 -参数设置: + -* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 +**漏标情况:** - 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 +* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 +* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 -* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 + - * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 - * JDK6 及以上版本默认值为 92% +代码角度解释漏标: -* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 +```java +Object G = objE.fieldG; // 读 +objE.fieldG = null; // 写 +objD.fieldG = G; // 写 +``` -* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** +为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** +> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 - * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 - * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 -*** + 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 -#### G1 + SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 -##### G1特点 +* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 -G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: -G1 对比其他处理器的优点: +- CMS:写屏障 + 增量更新 +- G1:写屏障 + SATB +- ZGC:读屏障 -* 并发与并行: - * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW - * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 - * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -* **分区算法**: - * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC - * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 - * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC - * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 - - * Region 结构图: - -![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) +*** -- 空间整合: - - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 +#### finalization - - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 +Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 - * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 +垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 -G1垃圾收集器的缺点: +生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: -* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 -* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 +- 可触及的:从根节点开始,可以到达这个对象。 +- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 +- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 -应用场景: +永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: -* 面向服务端应用,针对具有大内存、多处理器的机器 -* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 +* finalize() 时可能会导致对象复活 +* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -11123,282 +11396,278 @@ G1垃圾收集器的缺点: -##### 记忆集 +#### 引用分析 -记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 - +1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 -* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 -* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 + * 强引用可以直接访问目标对象 + * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 + * 强引用可能导致**内存泄漏** -垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: + ```java + Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 + ``` -* 字长精度 -* 对象精度 -* 卡精度(卡表) +2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 -卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 + * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 + * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 + * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 -收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 + ```java + Object obj = new Object(); + SoftReference sf = new SoftReference(obj); + obj = null; // 使对象只被软引用关联 + ``` -* CSet of Young Collection -* CSet of Mix Collection +3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 + * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 + * 配合引用队列来释放弱引用自身 + * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM + ```java + Object obj = new Object(); + WeakReference wf = new WeakReference(obj); + obj = null; + ``` -*** +4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 + * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 + * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 + * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 + ```java + Object obj = new Object(); + PhantomReference pf = new PhantomReference(obj, null); + obj = null; + ``` -##### 工作原理 +5. 终结器引用(finalization) -G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 -* 标记完成马上开始混合回收过程 - +*** -顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 - **回收过程**: +#### 无用属性 - 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 - 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 - * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet - * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 - 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 - 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 +##### 无用类 -* **Concurrent Mark **: +方法区主要回收的是无用的类 - * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC - * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 - * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) - * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 +判定一个类是否是无用的类,需要同时满足下面 3 个条件: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 +- 加载该类的 `ClassLoader` 已经被回收 +- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 -* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 -* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC +*** - 产生 Full GC 的原因: - * 晋升时没有足够的空间存放晋升的对象 - * 并发处理过程完成之前空间耗尽,浮动垃圾 +##### 废弃常量 +在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 -*** +*** -##### 相关参数 -- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 -- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 -- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms -- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 -- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 -- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 -- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) -- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) -- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 +##### 静态变量 +类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 -*** +如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null -##### 调优 +参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 -G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: -1. 开启 G1 垃圾收集器 -2. 设置堆的最大内存 -3. 设置最大的停顿时间(STW) -不断调优暂停时间指标: +*** -* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 -* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 -* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC -* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -不要设置新生代和老年代的大小: - -- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 -- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 +### 回收算法 +#### 标记清除 -*** +当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 +- 标记清除算法(Mark-Sweep) +- 复制算法(copying) +- 标记压缩算法(Mark-Compact) +标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -#### ZGC +- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** +- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** +- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 -* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 -* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 - * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 - * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) - * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 - * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 -* 内存多重映射:多个虚拟地址指向同一个物理地址 +算法缺点: -可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +- 标记和清除过程效率都不高 +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 -ZGC 目标: + -- 停顿时间不会超过 10ms -- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) -- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -ZGC 的工作过程可以分为 4 个阶段: -* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 -* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 -* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 +*** -ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -优点:高吞吐量、低延迟 -缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 +#### 复制算法 +复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 +应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 -参考文章:https://www.cnblogs.com/jimoer/p/13170249.html +![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) +算法优点: +- 没有标记和清除过程,实现简单,运行高效 +- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 -*** +算法缺点: +- 主要不足是**只使用了内存的一半** +- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 -#### 总结 -Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: -- 最小化地使用内存和并行开销,选 Serial GC -- 最大化应用程序的吞吐量,选 Parallel GC -- 最小化 GC 的中断或停顿时间,选 CMS GC +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) +#### 标记整理 +标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 -*** +优点:不会产生内存碎片 +缺点:需要移动大量对象,处理效率比较低 + -### 内存泄漏 +| | Mark-Sweep | Mark-Compact | Copying | +| -------- | ---------------- | -------------- | ----------------------------------- | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | +| 移动对象 | 否 | 是 | 是 | -#### 泄露溢出 +- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 +- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 -内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 -可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 -内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 +*** -内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 +#### 增量收集 -*** +增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 +工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 -#### 几种情况 -##### 静态集合 -静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -```java -public class MemoryLeak { - static List list = new ArrayList(); - public void oomTest(){ - Object obj = new Object();//局部变量 - list.add(obj); - } -} -``` +*** -*** +### 垃圾回收器 +#### 概述 -##### 单例模式 +垃圾收集器分类: -单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 +* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 + * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 +* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 + * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 + * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 +* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 + * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 + * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 +* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +GC 性能指标: +- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) +- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 +- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 +- 收集频率:相对于应用程序的执行,收集操作发生的频率 +- 内存占用:Java 堆区所占的内存大小 +- 快速:一个对象从诞生到被回收所经历的时间 -**** +**垃圾收集器的组合关系**: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) +新生代收集器:Serial、ParNew、Paralle1 Scavenge; -##### 内部类 +老年代收集器:Serial old、Parallel old、CMS; -内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 +整堆收集器:G1 +* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +查看默认的垃圾收回收器: -*** +* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID -##### 连接相关 -数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 +*** -**** +#### Serial +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 -##### 不合理域 +**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 -变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 +- Serial old 是 Client 模式下默认的老年代的垃圾回收器 +- Serial old 在 Server 模式下主要有两个用途: + - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 + - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 -```java -public class UsingRandom { - private String msg; - public void receiveMsg(){ - msg = readFromNet();// 从网络中接受数据保存到 msg 中 - saveDB(msg); // 把 msg 保存到数据库中 - } -} -``` +开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC -通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -解决: +优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 -* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 -* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 +缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 @@ -11406,255 +11675,262 @@ public class UsingRandom { -##### 改变哈希 - -当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 +#### Parallel +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** -*** +对比其他回收器: +* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 +* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 +* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +应用场景: -##### 缓存泄露 +* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 +* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 -内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 +停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 +在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +参数配置: -*** +* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 +* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 + * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 + * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 + * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] +* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 + * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 + * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 +* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 + * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 + * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 -##### 监听器 +*** -监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 +#### ParNew -*** +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** -#### 案例分析 +相关参数: -```java -public class Stack { - private Object[] elements; - private int size = 0; - private static final int DEFAULT_INITIAL_CAPACITY = 16; +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 - public Stack() { - elements = new Object[DEFAULT_INITIAL_CAPACITY]; - } +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 - public void push(Object e) { //入栈 - ensureCapacity(); - elements[size++] = e; - } +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) - public Object pop() { //出栈 - if (size == 0) - throw new EmptyStackException(); - return elements[--size]; - } +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - private void ensureCapacity() { - if (elements.length == size) - elements = Arrays.copyOf(elements, 2 * size + 1); - } -} -``` +- 对于新生代,回收次数频繁,使用并行方式高效 +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) -程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 -解决方法: -```java -public Object pop() { - if (size == 0) - throw new EmptyStackException(); - Object result = elements[--size]; - elements[size] = null; - return result; -} -``` +**** +#### CMS +CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -*** +CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 +分为以下四个流程: +- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 +- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 +Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: +* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 -## 类加载 +* Mark Compact 更适合 Stop The World 场景 -### 对象访存 +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -#### 存储结构 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) -一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) +优点:并发收集、低延迟 -对象头: +缺点: -* 普通对象:分为两部分 +- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 +- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 + + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 - * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 +参数设置: - ```ruby - hash(25) + age(4) + lock(3) = 32bit #32位系统 - unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 - ``` +* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 - * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) + 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 - ```ruby - |-----------------------------------------------------| - | Object Header (64 bits) | - |---------------------------|-------------------------| - | Mark Word (32 bits) | Klass Word (32 bits) | - |---------------------------|-------------------------| - ``` +* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 -* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) + * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 + * JDK6 及以上版本默认值为 92% - ```ruby - |-------------------------------------------------------------------------------| - | Object Header (96 bits) | - |-----------------------|-----------------------------|-------------------------| - | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | - |-----------------------|-----------------------------|-------------------------| - ``` +* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 -实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** -对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** -32 位系统: + * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 -* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: - ```java - private final int value; - ``` - ```ruby - # 需要补位4byte - 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte - ``` +*** -* `int[] arr = new int[10]` - ```ruby - # 由于需要8位对齐,所以最终大小为`56byte`。 - 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte - ``` +#### G1 +##### G1特点 -*** +G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +G1 对比其他处理器的优点: +* 并发与并行: + * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW + * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 + * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -#### 实际大小 +* **分区算法**: + * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC + * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 + * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC + * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 + + * Region 结构图: + -浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) -JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +- 空间整合: -```java -private final char value[]; -private int hash; -private int hash32; -``` + - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 + - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 +- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 -深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 + - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 -对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 + * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 -下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 +G1垃圾收集器的缺点: - +* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 +* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 -内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 +应用场景: -基本性质: +* 面向服务端应用,针对具有大内存、多处理器的机器 +* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 -- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 -- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -- 支配树的边与对象引用图的边不直接对应 +*** -左图表示对象引用图,右图表示左图所对应的支配树: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 +##### 记忆集 +记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) + -参考文章:https://www.yuque.com/u21195183/jvm/nkq31c +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 +* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 +垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: +* 字长精度 +* 对象精度 +* 卡精度(卡表) -*** +卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 +收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 +* CSet of Young Collection +* CSet of Mix Collection -#### 节约内存 -* 尽量使用基本数据类型 -* 满足容量前提下,尽量用小字段 +*** -* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil - 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: - ```java - private transient Object[] elementData; - private int size; - ``` +##### 工作原理 - Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) +G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 时间用 long/int 表示,不用 Date 或者 String +* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 +* 标记完成马上开始混合回收过程 + +顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -*** +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 + **回收过程**: + 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 + 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet + * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 + 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 -#### 对象访问 +* **Concurrent Mark **: -JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: + * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC + * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) + * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 -* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - - 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) +* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC -* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 - - 优点:速度更快,**节省了一次指针定位的时间开销** + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) + 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 +* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC + 产生 Full GC 的原因: -参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html + * 晋升时没有足够的空间存放晋升的对象 + * 并发处理过程完成之前空间耗尽,浮动垃圾 @@ -11662,178 +11938,123 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -### 对象创建 - -#### 生命周期 +##### 相关参数 -在 Java 中,对象的生命周期包括以下几个阶段: +- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 +- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 +- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms +- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 +- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 +- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 +- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 +- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) +- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) +- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 -1. 创建阶段 (Created): -2. 应用阶段 (In Use):对象至少被一个强引用持有着 -3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 -4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 -5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 -6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 -7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 +*** -参考文章:https://blog.csdn.net/sodino/article/details/38387049 +##### 调优 -*** +G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: +1. 开启 G1 垃圾收集器 +2. 设置堆的最大内存 +3. 设置最大的停顿时间(STW) +不断调优暂停时间指标: -#### 创建时机 +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 +* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 +* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC +* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +不要设置新生代和老年代的大小: -Java 对象创建时机: +- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 +- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 -1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -2. 使用 Class 类的 newInstance 方法(反射机制) -3. 使用 Constructor 类的 newInstance 方法(反射机制) +*** - ```java - public class Student { - private int id; - public Student(Integer id) { - this.id = id; - } - public static void main(String[] args) throws Exception { - Constructor c = Student.class.getConstructor(Integer.class); - Student stu = c.newInstance(123); - } - } - ``` - 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +#### ZGC -5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** -从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 - - - -*** +* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 +* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 + * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 + * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 +* 内存多重映射:多个虚拟地址指向同一个物理地址 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +ZGC 目标: -#### 创建过程 +- 停顿时间不会超过 10ms +- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) +- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -创建对象的过程: +ZGC 的工作过程可以分为 4 个阶段: -1. 判断对象对应的类是否加载、链接、初始化 +* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 +* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 -2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) +ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -3. 处理并发安全问题: +优点:高吞吐量、低延迟 - * 采用 CAS 配上自旋保证更新的原子性 - * 每个线程预先分配一块 TLAB +缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 -4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 +参考文章:https://www.cnblogs.com/jimoer/p/13170249.html - * 实例变量初始化与实例代码块初始化: - 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 - * 构造函数初始化: +*** - **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 +#### 总结 -*** +Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: +- 最小化地使用内存和并行开销,选 Serial GC +- 最大化应用程序的吞吐量,选 Parallel GC +- 最小化 GC 的中断或停顿时间,选 CMS GC +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) -#### 承上启下 -1. 一个实例变量在对象初始化的过程中会被赋值几次? - JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 - 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 - 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 - 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -2. 类的初始化过程与类的实例化过程的异同? - 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 - 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +*** -3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) - ```java - public class StaticTest { - public static void main(String[] args) { - staticFunction();//调用静态方法,触发初始化 - } - - static StaticTest st = new StaticTest(); - - static { //静态代码块 - System.out.println("1"); - } - - { // 实例代码块 - System.out.println("2"); - } - - StaticTest() { // 实例构造器 - System.out.println("3"); - System.out.println("a=" + a + ",b=" + b); - } - - public static void staticFunction() { // 静态方法 - System.out.println("4"); - } - - int a = 110; // 实例变量 - static int b = 112; // 静态变量 - }/* Output: - 2 - 3 - a=110,b=0 - 1 - 4 - *///:~ - ``` - `static StaticTest st = new StaticTest();`: +### 内存泄漏 - * 实例实例化不一定要在类初始化结束之后才开始 +#### 泄露溢出 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 +内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 - 代码等价于: +可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 - ```java - public class StaticTest { - (){ - a = 110; // 实例变量 - System.out.println("2"); // 实例代码块 - System.out.println("3"); // 实例构造器中代码的执行 - System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 - 类变量st被初始化 - System.out.println("1"); //静态代码块 - 类变量b被初始化为112 - } - } - ``` +内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 - +内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 @@ -11841,21 +12062,21 @@ Java 对象创建时机: -### 加载过程 - -#### 生命周期 - -类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 +#### 几种情况 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +##### 静态集合 -包括 7 个阶段: +静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -* 加载(Loading) -* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) -* 初始化(Initialization) -* 使用(Using) -* 卸载(Unloading) +```java +public class MemoryLeak { + static List list = new ArrayList(); + public void oomTest(){ + Object obj = new Object();//局部变量 + list.add(obj); + } +} +``` @@ -11863,115 +12084,88 @@ Java 对象创建时机: -#### 加载阶段 +##### 单例模式 -加载是类加载的其中一个阶段,注意不要混淆 +单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 -加载过程完成以下三件事: -- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) -- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 -其中二进制字节流可以从以下方式中获取: +**** -- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 -- 从网络中获取,最典型的应用是 Applet -- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: +##### 内部类 -* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 -* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 +内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 -加载过程: -* 如果这个类还有父类没有加载,先加载父类 -* 加载和链接可能是交替运行的 -* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 - +*** -创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: -- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 -- JVM 使用指定的元素类型和数组维度来创建新的数组类 -- 基本数据类型由启动类加载器加载 +##### 连接相关 +数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 -*** +**** -#### 链接阶段 -##### 验证 -确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 +##### 不合理域 -主要包括**四种验证**: +变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 -* 文件格式验证 +```java +public class UsingRandom { + private String msg; + public void receiveMsg(){ + msg = readFromNet();// 从网络中接受数据保存到 msg 中 + saveDB(msg); // 把 msg 保存到数据库中 + } +} +``` -* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 +通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 - * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) +解决: - * 是否一些被定义为 final 的方法或者类被重写或继承了 +* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 +* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 - * 非抽象类是否实现了所有抽象方法或者接口方法 - * 是否存在不兼容的方法 -* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 +**** - * 在字节码的执行过程中,是否会跳转到一条不存在的指令 - * 函数的调用是否传递了正确类型的参数 - * 变量的赋值是不是给了正确的数据类型 - * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 -* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 +##### 改变哈希 +当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 -*** +*** -##### 准备 -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加 static 的变量 +##### 缓存泄露 -说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 -类变量初始化: +使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 -* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** -* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 -* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -实例: -* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: +*** - ```java - public static int value = 123; - ``` -* 常量 value 被初始化为 123 而不是 0: - ```java - public static final int value = 123; - ``` +##### 监听器 -* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false +监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 @@ -11979,194 +12173,195 @@ Java 对象创建时机: -##### 解析 +#### 案例分析 -将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +```java +public class Stack { + private Object[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** -* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 + public Stack() { + elements = new Object[DEFAULT_INITIAL_CAPACITY]; + } -例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** + public void push(Object e) { //入栈 + ensureCapacity(); + elements[size++] = e; + } -解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 - -* 在类加载阶段解析的是非虚方法,静态绑定 -* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** -* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 + public Object pop() { //出栈 + if (size == 0) + throw new EmptyStackException(); + return elements[--size]; + } -```java -public class Load2 { - public static void main(String[] args) throws Exception{ - ClassLoader classloader = Load2.class.getClassLoader(); - // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D - Class c = classloader.loadClass("cn.jvm.t3.load.C"); - - // new C();会导致类的解析和初始化,从而解析初始化D - System.in.read(); + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); } } -class C { - D d = new D(); -} -class D { -} ``` +程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 +解决方法: -**** - +```java +public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; + return result; +} +``` -#### 初始化 -##### 介绍 -初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init +*** -类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 -类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 -*** +## 类加载 +### 对象访存 +#### 存储结构 -##### clinit +一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) -():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 +对象头: -作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 +* 普通对象:分为两部分 -* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 -* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 -* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 -* static 不加 final 的变量都在初始化环节赋值 + * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 -**线程安全**问题: + ```ruby + hash(25) + age(4) + lock(3) = 32bit #32位系统 + unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 + ``` -* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 -* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 + * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) -特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 + ```ruby + |-----------------------------------------------------| + | Object Header (64 bits) | + |---------------------------|-------------------------| + | Mark Word (32 bits) | Klass Word (32 bits) | + |---------------------------|-------------------------| + ``` -```java -public class Test { - static { - //i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” - } - static int i = 1; -} -``` +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: + ```ruby + |-------------------------------------------------------------------------------| + | Object Header (96 bits) | + |-----------------------|-----------------------------|-------------------------| + | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | + |-----------------------|-----------------------------|-------------------------| + ``` -* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 -* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 -* 只有当父接口中定义的变量使用时,父接口才会初始化 +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +32 位系统: -**** +* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: + ```java + private final int value; + ``` + ```ruby + # 需要补位4byte + 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte + ``` -##### 时机 +* `int[] arr = new int[10]` -类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 + ```ruby + # 由于需要8位对齐,所以最终大小为`56byte`。 + 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte + ``` -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) -* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 - * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) - * putstatic:程序给类的静态变量赋值 - * invokestatic :调用一个类的静态方法 -* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** -* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 -* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 -* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 +*** -* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 -* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 -* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 -* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 +#### 实际大小 -*** +浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +```java +private final char value[]; +private int hash; +private int hash32; +``` -##### init +保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 -init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 -实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 +对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 -类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 + +内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 -*** +基本性质: +- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 +- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -#### 卸载阶段 +- 支配树的边与对象引用图的边不直接对应 -时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 +左图表示对象引用图,右图表示左图所对应的支配树: -卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 -2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 +比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 -在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c -**** +*** -### 类加载器 -#### 类加载 -类加载方式: +#### 节约内存 -* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 - * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 - * 在 JVM 启动时,通过三大类加载器加载 class -* 显式加载: - * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +* 尽量使用基本数据类型 -类的唯一性: +* 满足容量前提下,尽量用小字段 -* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: - - 类的完整类名必须一致,包括包名 - - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 -* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true +* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil -命名空间: + 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: -- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 -- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 + ```java + private transient Object[] elementData; + private int size; + ``` -基本特征: + Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) -* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 -* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 +* 时间用 long/int 表示,不用 Date 或者 String @@ -12174,110 +12369,90 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, -#### 加载器 +#### 对象访问 -类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 +JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: +* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 -- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 -- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) -从 Java 开发人员的角度看: +* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 + + 优点:速度更快,**节省了一次指针定位的时间开销** -* 启动类加载器(Bootstrap ClassLoader): - * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 - * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 - * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 - * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 -* 扩展类加载器(Extension ClassLoader): - * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null - * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 - * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 -* 应用程序类加载器(Application ClassLoader): - * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension - * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 - * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 - * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 -* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application + 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) -```java -public static void main(String[] args) { - //获取系统类加载器 - ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); - System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //获取其上层 扩展类加载器 - ClassLoader extClassLoader = systemClassLoader.getParent(); - System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 - //获取其上层 获取不到引导类加载器 - ClassLoader bootStrapClassLoader = extClassLoader.getParent(); - System.out.println(bootStrapClassLoader);//null +参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html - //对于用户自定义类来说:使用系统类加载器进行加载 - ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); - System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 - ClassLoader classLoader1 = String.class.getClassLoader(); - System.out.println(classLoader1);//null -} -``` - -补充两个类加载器: +*** -* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 -* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 +### 对象创建 -*** +#### 生命周期 +在 Java 中,对象的生命周期包括以下几个阶段: +1. 创建阶段 (Created): +2. 应用阶段 (In Use):对象至少被一个强引用持有着 +3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 +5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 +7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 -#### 常用API -ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -获取 ClassLoader 的途径: +参考文章:https://blog.csdn.net/sodino/article/details/38387049 -* 获取当前类的 ClassLoader:`clazz.getClassLoader()` -* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` -* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` -* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` -ClassLoader 类常用方法: -* `getParent()`:返回该类加载器的超类加载器 -* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** -* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 -* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 -* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 -* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 -* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 +*** -*** +#### 创建时机 +类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +Java 对象创建时机: -#### 加载模型 +1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -##### 加载机制 +2. 使用 Class 类的 newInstance 方法(反射机制) -在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 +3. 使用 Constructor 类的 newInstance 方法(反射机制) -- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 + ```java + public class Student { + private int id; + public Student(Integer id) { + this.id = id; + } + public static void main(String[] args) throws Exception { + Constructor c = Student.class.getConstructor(Integer.class); + Student stu = c.newInstance(123); + } + } + ``` -- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 + 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 - - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 +4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 @@ -12285,343 +12460,310 @@ ClassLoader 类常用方法: -##### 双亲委派 - -双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) +#### 创建过程 -工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 +创建对象的过程: -双亲委派机制的优点: +1. 判断对象对应的类是否加载、链接、初始化 -* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 +2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) -* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 +3. 处理并发安全问题: -* 保护程序安全,防止类库的核心 API 被随意篡改 + * 采用 CAS 配上自旋保证更新的原子性 + * 每个线程预先分配一块 TLAB - 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 +4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 - ```java - public class String { - public static void main(String[] args) { - System.out.println("demo info"); - } - } - ``` - - 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) +6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 - + * 实例变量初始化与实例代码块初始化: + 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 + * 构造函数初始化: -*** + **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 -##### 源码分析 +*** -```java -protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 - Class c = findLoadedClass(name); - - // 当前类加载器如果没有加载过 - if (c == null) { - long t0 = System.nanoTime(); - try { - // 判断当前类加载器是否有父类加载器 - if (parent != null) { - // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) -          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 - c = parent.loadClass(name, false); - } else { - // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader -           // 则调用 BootStrap ClassLoader 的方法加载类 - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { } - if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 - // 可以自定义 findClass() 方法 - long t1 = System.nanoTime(); - c = findClass(name); - // this is the defining class loader; record the stats - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } - if (resolve) { - // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 - resolveClass(c); - } - return c; - } -} -``` +#### 承上启下 +1. 一个实例变量在对象初始化的过程中会被赋值几次? + JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 + 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 + 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 + 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 + 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -**** +2. 类的初始化过程与类的实例化过程的异同? + 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 + 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) -##### 破坏委派 + ```java + public class StaticTest { + public static void main(String[] args) { + staticFunction();//调用静态方法,触发初始化 + } + + static StaticTest st = new StaticTest(); + + static { //静态代码块 + System.out.println("1"); + } + + { // 实例代码块 + System.out.println("2"); + } + + StaticTest() { // 实例构造器 + System.out.println("3"); + System.out.println("a=" + a + ",b=" + b); + } + + public static void staticFunction() { // 静态方法 + System.out.println("4"); + } + + int a = 110; // 实例变量 + static int b = 112; // 静态变量 + }/* Output: + 2 + 3 + a=110,b=0 + 1 + 4 + *///:~ + ``` -双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 + `static StaticTest st = new StaticTest();`: -破坏双亲委派模型的方式: + * 实例实例化不一定要在类初始化结束之后才开始 -* 自定义 ClassLoader + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 - * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 - * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 + 代码等价于: -* 引入线程**上下文类加载器** + ```java + public class StaticTest { + (){ + a = 110; // 实例变量 + System.out.println("2"); // 实例代码块 + System.out.println("3"); // 实例构造器中代码的执行 + System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 + 类变量st被初始化 + System.out.println("1"); //静态代码块 + 类变量b被初始化为112 + } + } + ``` - Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: + - * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 - * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 - JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 - -* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) - IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 +*** - 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: - 1. 将以 java.* 开头的类,委派给父类加载器加载 - 2. 否则,将委派列表名单内的类,委派给父类加载器加载 - 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 - 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 - 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 - 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 - 7. 否则,类查找失败 - - 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 - +### 加载过程 +#### 生命周期 +类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -*** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +包括 7 个阶段: +* 加载(Loading) +* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) +* 初始化(Initialization) +* 使用(Using) +* 卸载(Unloading) -#### 沙箱机制 -沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 +*** -* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 -* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 -* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 -* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 - +#### 加载阶段 +加载是类加载的其中一个阶段,注意不要混淆 -*** +加载过程完成以下三件事: +- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) +- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) +- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 +其中二进制字节流可以从以下方式中获取: -#### 自定义 +- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 +- 从网络中获取,最典型的应用是 Applet +- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 +将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 +方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: -```java -//自定义类加载器,读取指定的类路径classPath下的class文件 -public class MyClassLoader extends ClassLoader{ - private String classPath; +* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 +* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 - public MyClassLoader(String classPath) { - this.classPath = classPath; - } - - public MyClassLoader(ClassLoader parent, String byteCodePath) { - super(parent); - this.classPath = classPath; - } +加载过程: - @Override - protected Class findClass(String name) throws ClassNotFoundException { - BufferedInputStream bis = null; - ByteArrayOutputStream baos = null; - try { - // 获取字节码文件的完整路径 - String fileName = classPath + className + ".class"; - // 获取一个输入流 - bis = new BufferedInputStream(new FileInputStream(fileName)); - // 获取一个输出流 - baos = new ByteArrayOutputStream(); - // 具体读入数据并写出的过程 - int len; - byte[] data = new byte[1024]; - while ((len = bis.read(data)) != -1) { - baos.write(data, 0, len); - } - // 获取内存中的完整的字节数组的数据 - byte[] byteCodes = baos.toByteArray(); - // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 - Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); - return clazz; - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (baos != null) - baos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - try { - if (bis != null) - bis.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return null; - } -} -``` +* 如果这个类还有父类没有加载,先加载父类 +* 加载和链接可能是交替运行的 +* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 -```java -public static void main(String[] args) { - MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + - try { - Class clazz = loader.loadClass("Demo1"); - System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader +创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: - System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } -} -``` +- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 +- JVM 使用指定的元素类型和数组维度来创建新的数组类 +- 基本数据类型由启动类加载器加载 -**** +*** -#### JDK9 +#### 链接阶段 -为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: +##### 验证 -* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 +确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 -* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 +主要包括**四种验证**: -* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` +* 文件格式验证 - +* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 + * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) + * 是否一些被定义为 final 的方法或者类被重写或继承了 -*** + * 非抽象类是否实现了所有抽象方法或者接口方法 + * 是否存在不兼容的方法 +* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 + * 在字节码的执行过程中,是否会跳转到一条不存在的指令 + * 函数的调用是否传递了正确类型的参数 + * 变量的赋值是不是给了正确的数据类型 + * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 +* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 -## 运行机制 -### 执行过程 - Java 文件编译执行的过程: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) -- 类加载器:用于装载字节码文件(.class文件) -- 运行时数据区:用于分配存储空间 -- 执行引擎:执行字节码文件或本地方法 -- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 +##### 准备 +准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -**** +* 类变量也叫静态变量,就是是被 static 修饰的变量 +* 实例变量也叫对象变量,即没加 static 的变量 +说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +类变量初始化: -### 字节码 +* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** +* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 +* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -#### 跨平台性 +实例: -Java 语言:跨平台的语言(write once ,run anywhere) +* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: -* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** -* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 + ```java + public static int value = 123; + ``` -编译过程中的编译器: +* 常量 value 被初始化为 123 而不是 0: -* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + ```java + public static final int value = 123; + ``` - * IntelliJ IDEA 使用 javac 编译器 - * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 - * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false -* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 - * JIT编译器:执行引擎部分详解 - * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 -* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, +*** - * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 - * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 - * 缺点: - * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 - * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 +##### 解析 +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 +例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** -*** +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 +* 在类加载阶段解析的是非虚方法,静态绑定 +* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** +* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 +```java +public class Load2 { + public static void main(String[] args) throws Exception{ + ClassLoader classloader = Load2.class.getClassLoader(); + // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D + Class c = classloader.loadClass("cn.jvm.t3.load.C"); + + // new C();会导致类的解析和初始化,从而解析初始化D + System.in.read(); + } +} +class C { + D d = new D(); +} +class D { +} +``` -#### 语言发展 -机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 +**** -指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -- x86 指令集,对应的是 x86 架构的平台 -- ARM 指令集,对应的是 ARM 架构的平台 -汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +#### 初始化 -* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 -* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 +##### 介绍 -高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init -* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 -* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 +类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 - +类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 @@ -12629,108 +12771,83 @@ Java 语言:跨平台的语言(write once ,run anywhere) +##### clinit +():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 -#### 类结构 - -##### 文件结构 +作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 -字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 +* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 +* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 +* static 不加 final 的变量都在初始化环节赋值 -字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 +**线程安全**问题: -JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html +* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 +* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 -根据 JVM 规范,类文件结构如下: +特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 ```java -ClassFile { - u4 magic; - u2 minor_version; - u2 major_version; - u2 constant_pool_count; - cp_info constant_pool[constant_pool_count-1]; - u2 access_flags; - u2 this_class; - u2 super_class; - u2 interfaces_count; - u2 interfaces[interfaces_count]; - u2 fields_count; - field_info fields[fields_count]; - u2 methods_count; - method_info methods[methods_count]; - u2 attributes_count; - attribute_info attributes[attributes_count]; +public class Test { + static { + //i = 0; // 给变量赋值可以正常编译通过 + System.out.print(i); // 这句编译器会提示“非法向前引用” + } + static int i = 1; } ``` -| 类型 | 名称 | 说明 | 长度 | 数量 | -| -------------- | ------------------- | -------------------- | ------- | --------------------- | -| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | -| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | -| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | -| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | -| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | -| u2 | access_flags | 访问标识 | 2个字节 | 1 | -| u2 | this_class | 类索引 | 2个字节 | 1 | -| u2 | super_class | 父类索引 | 2个字节 | 1 | -| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | -| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | -| u2 | fields_count | 字段计数器 | 2个字节 | 1 | -| field_info | fields | 字段表 | n个字节 | fields_count | -| u2 | methods_count | 方法计数器 | 2个字节 | 1 | -| method_info | methods | 方法表 | n个字节 | methods_count | -| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | -| attribute_info | attributes | 属性表 | n个字节 | attributes_count | +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: -Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 +* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 +* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 +* 只有当父接口中定义的变量使用时,父接口才会初始化 -* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 -* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 -获取方式: -* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 -* 写入文件指令 `javap -v xxx.class >xxx.txt` -* IDEA 插件 jclasslib +**** -*** +##### 时机 +类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -##### 魔数版本 +* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) +* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) + * putstatic:程序给类的静态变量赋值 + * invokestatic :调用一个类的静态方法 +* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** +* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 +* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, +**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 -* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 +* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 -* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 -版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +*** -| 主版本(十进制) | 副版本(十进制) | 编译器版本 | -| ---------------- | ---------------- | ---------- | -| 45 | 3 | 1.1 | -| 46 | 0 | 1.2 | -| 47 | 0 | 1.3 | -| 48 | 0 | 1.4 | -| 49 | 0 | 1.5 | -| 50 | 0 | 1.6 | -| 51 | 0 | 1.7 | -| 52 | 0 | 1.8 | -| 53 | 0 | 1.9 | -| 54 | 0 | 1.10 | -| 55 | 0 | 1.11 | -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) +##### init + +init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 -图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ +类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** @@ -12738,81 +12855,53 @@ Class 文件格式采用一种类似于 C 语言结构体的方式进行数据 -##### 常量池 - -常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +#### 卸载阶段 -constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 -* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 +卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: -* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 - * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 +在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 - * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x - * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - | 标志符 | 含义 | - | ------ | --------------------------------------------------------- | - | B | 基本数据类型 byte | - | C | 基本数据类型 char | - | D | 基本数据类型 double | - | F | 基本数据类型 float | - | I | 基本数据类型 int | - | J | 基本数据类型 long | - | S | 基本数据类型 short | - | Z | 基本数据类型 boolean | - | V | 代表 void 类型 | - | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | - | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +**** -常量类型和结构: -| 类型 | 标志(或标识) | 描述 | -| -------------------------------- | ------------ | ---------------------- | -| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | -| CONSTANT_Integer_info | 3 | 整型字面量 | -| CONSTANT_Float_info | 4 | 浮点型字面量 | -| CONSTANT_Long_info | 5 | 长整型字面量 | -| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | -| CONSTANT_Class_info | 7 | 类或接口的符号引用 | -| CONSTANT_String_info | 8 | 字符串类型字面量 | -| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | -| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | -| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | -| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | -| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | -| CONSTANT_MethodType_info | 16 | 标志方法类型 | -| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | -18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer +### 类加载器 +#### 类加载 +类加载方式: -**** +* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在 JVM 启动时,通过三大类加载器加载 class +* 显式加载: + * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +类的唯一性: +* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: + - 类的完整类名必须一致,包括包名 + - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 +* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true -##### 访问标识 +命名空间: -访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 +- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 +- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 -* 类的访问权限通常为 ACC_ 开头的常量 -* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` -* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 +基本特征: -| 标志名称 | 标志值 | 含义 | -| -------------- | ------ | ------------------------------------------------------------ | -| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | -| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | -| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | -| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | -| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | -| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | -| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | -| ACC_ENUM | 0x4000 | 标志这是一个枚举 | +* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 +* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 @@ -12820,84 +12909,89 @@ constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表 -##### 索引集合 +#### 加载器 -类索引、父类索引、接口索引集合 +类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 -* 类索引用于确定这个类的全限定名 +从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: -* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 +- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 +- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 -* 接口索引集合就用来描述这个类实现了哪些接口 - * interfaces_count 项的值表示当前类或接口的直接超接口数量 - * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 +从 Java 开发人员的角度看: -| 长度 | 含义 | -| ---- | ---------------------------- | -| u2 | this_class | -| u2 | super_class | -| u2 | interfaces_count | -| u2 | interfaces[interfaces_count] | +* 启动类加载器(Bootstrap ClassLoader): + * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 + * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 + * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 + * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 +* 扩展类加载器(Extension ClassLoader): + * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null + * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 + * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 +* 应用程序类加载器(Application ClassLoader): + * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension + * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 + * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 + * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 +* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application +```java +public static void main(String[] args) { + //获取系统类加载器 + ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //获取其上层 扩展类加载器 + ClassLoader extClassLoader = systemClassLoader.getParent(); + System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 -*** + //获取其上层 获取不到引导类加载器 + ClassLoader bootStrapClassLoader = extClassLoader.getParent(); + System.out.println(bootStrapClassLoader);//null + //对于用户自定义类来说:使用系统类加载器进行加载 + ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); + System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 + ClassLoader classLoader1 = String.class.getClassLoader(); + System.out.println(classLoader1);//null -##### 字段表 +} +``` -字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 +补充两个类加载器: -fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 +* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 +* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 -fields[](字段表): -* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 -* 字段访问标识: +*** - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为public | - | ACC_PRIVATE | 0x0002 | 字段是否为private | - | ACC_PROTECTED | 0x0004 | 字段是否为protected | - | ACC_STATIC | 0x0008 | 字段是否为static | - | ACC_FINAL | 0x0010 | 字段是否为final | - | ACC_VOLATILE | 0x0040 | 字段是否为volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为enum | -* 字段名索引:根据该值查询常量池中的指定索引项即可 -* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 +#### 常用API - | 字符 | 类型 | 含义 | - | ----------- | --------- | ----------------------- | - | B | byte | 有符号字节型树 | - | C | char | Unicode字符,UTF-16编码 | - | D | double | 双精度浮点数 | - | F | float | 单精度浮点数 | - | I | int | 整型数 | - | J | long | 长整数 | - | S | short | 有符号短整数 | - | Z | boolean | 布尔值true/false | - | V | void | 代表void类型 | - | L Classname | reference | 一个名为Classname的实例 | - | [ | reference | 一个一维数组 | +ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 +获取 ClassLoader 的途径: - ```java - ConstantValue_attribute{ - u2 attribute_name_index; - u4 attribute_length; - u2 constantvalue_index; - } - ``` +* 获取当前类的 ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` - 对于常量属性而言,attribute_length 值恒为2 +ClassLoader 类常用方法: + +* `getParent()`:返回该类加载器的超类加载器 +* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** +* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 +* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 +* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 +* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 +* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 @@ -12905,108 +12999,108 @@ fields[](字段表): -##### 方法表 +#### 加载模型 -方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +##### 加载机制 -* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 -* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 -* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 +- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 -methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 + - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 -* 方法表结构如下: - | 类型 | 名称 | 含义 | 数量 | - | -------------- | ---------------- | ---------- | ---------------- | - | u2 | access_flags | 访问标志 | 1 | - | u2 | name_index | 字段名索引 | 1 | - | u2 | descriptor_index | 描述符索引 | 1 | - | u2 | attrubutes_count | 属性计数器 | 1 | - | attribute_info | attributes | 属性集合 | attributes_count | -* 方法表访问标志: - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为 public | - | ACC_PRIVATE | 0x0002 | 字段是否为 private | - | ACC_PROTECTED | 0x0004 | 字段是否为 protected | - | ACC_STATIC | 0x0008 | 字段是否为 static | - | ACC_FINAL | 0x0010 | 字段是否为 final | - | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为 enum | +*** -*** +##### 双亲委派 +双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) -##### 属性表 +工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 -属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +双亲委派机制的优点: -attributes_ count(属性计数器):表示当前文件属性表的成员个数 +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 -attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 +* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 -* 属性的通用格式: +* 保护程序安全,防止类库的核心 API 被随意篡改 + + 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 ```java - ConstantValue_attribute{ - u2 attribute_name_index; //属性名索引 - u4 attribute_length; //属性长度 - u2 attribute_info; //属性表 + public class String { + public static void main(String[] args) { + System.out.println("demo info"); + } } ``` + + 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 -* 属性类型: - - | 属性名称 | 使用位置 | 含义 | - | ------------------------------------- | ------------------ | ------------------------------------------------------------ | - | Code | 方法表 | Java 代码编译成的字节码指令 | - | ConstantValue | 字段表 | final 关键字定义的常量池 | - | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | - | Exceptions | 方法表 | 方法抛出的异常 | - | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | - | InnerClass | 类文件 | 内部类列表 | - | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | - | LocalVariableTable | Code 属性 | 方法的局部变量描述 | - | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | - | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | - | SourceFile | 类文件 | 记录源文件名称 | - | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | - | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | - | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | - | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | - | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | - | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | - | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | - | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | - | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | - +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) + -**** +*** -#### 编译指令 +##### 源码分析 -##### javac +```java +protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 + Class c = findLoadedClass(name); + + // 当前类加载器如果没有加载过 + if (c == null) { + long t0 = System.nanoTime(); + try { + // 判断当前类加载器是否有父类加载器 + if (parent != null) { + // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) +          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 + c = parent.loadClass(name, false); + } else { + // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader +           // 则调用 BootStrap ClassLoader 的方法加载类 + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { } -javac:编译命令,将 java 源文件编译成 class 字节码文件 + if (c == null) { + // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 + // 可以自定义 findClass() 方法 + long t1 = System.nanoTime(); + c = findClass(name); -`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } + if (resolve) { + // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 + resolveClass(c); + } + return c; + } +} +``` @@ -13014,62 +13108,43 @@ javac:编译命令,将 java 源文件编译成 class 字节码文件 -##### javap - -javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - -用法:javap - -```sh --help --help -? 输出此用法消息 --version 版本信息 --public 仅显示公共类和成员 --protected 显示受保护的/公共类和成员 --package 显示程序包/受保护的/公共类和成员 (默认) --p -private 显示所有类和成员 - #常用的以下三个 --v -verbose 输出附加信息 --l 输出行号和本地变量表 --c 对代码进行反汇编 #反编译 - --s 输出内部类型签名 --sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) --constants 显示最终常量 --classpath 指定查找用户类文件的位置 --cp 指定查找用户类文件的位置 --bootclasspath 覆盖引导类文件的位置 -``` - - +##### 破坏委派 -*** +双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 +破坏双亲委派模型的方式: +* 自定义 ClassLoader -#### 指令集 + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 -##### 执行指令 +* 引入线程**上下文类加载器** -Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) + Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: -由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 + * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 + * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 -在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 + JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 + +* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) -* i 代表对 int 类型的数据操作 -* l 代表 long -* s 代表 short -* b 代表 byte -* c 代表 char -* f 代表 float -* d 代表 double + IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 -大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 + 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: -在做值相关操作时: + 1. 将以 java.* 开头的类,委派给父类加载器加载 + 2. 否则,将委派列表名单内的类,委派给父类加载器加载 + 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 + 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 + 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 + 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 + 7. 否则,类查找失败 + + 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 -- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 -- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 + @@ -13077,195 +13152,180 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 -##### 加载存储 - -加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 - -局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 +#### 沙箱机制 -* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 -* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 -* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 +沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 +沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 -* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 -* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 -* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 +* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 +* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 +* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 +* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 -出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 + -* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 -* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s -扩充局部变量表的访问索引的指令:wide +*** -**** +#### 自定义 +对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 -##### 算术指令 +作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 -算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 +```java +//自定义类加载器,读取指定的类路径classPath下的class文件 +public class MyClassLoader extends ClassLoader{ + private String classPath; -没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 - -* 加法指令:iadd、ladd、fadd、dadd -* 减法指令:isub、lsub、fsub、dsub -* 乘法指令:imu、lmu、fmul、dmul -* 除法指令:idiv、ldiv、fdiv、ddiv -* 求余指令:irem、lrem、frem、drem(remainder 余数) -* 取反指令:ineg、lneg、fneg、dneg (negation 取反) -* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) -* 位运算指令,又可分为: - - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr - - 按位或指令:ior、lor - - 按位与指令:iand、land - - 按位异或指令:ixor、lxor - -* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp - -运算模式: - -* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 -* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 - -NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 - -```java -double j = i / 0.0; -System.out.println(j);//无穷大,NaN: not a number -``` - -**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc - -```java - 4 iload_1 //存入操作数栈 - 5 iinc 1 by 1 //自增i++ - 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 - 9 iinc 2 by 1 //++i -12 iload_2 //加载到操作数栈 -13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 -``` + public MyClassLoader(String classPath) { + this.classPath = classPath; + } + + public MyClassLoader(ClassLoader parent, String byteCodePath) { + super(parent); + this.classPath = classPath; + } -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = a++ + ++a + a--; - System.out.println(a); //11 - System.out.println(b); //34 + @Override + protected Class findClass(String name) throws ClassNotFoundException { + BufferedInputStream bis = null; + ByteArrayOutputStream baos = null; + try { + // 获取字节码文件的完整路径 + String fileName = classPath + className + ".class"; + // 获取一个输入流 + bis = new BufferedInputStream(new FileInputStream(fileName)); + // 获取一个输出流 + baos = new ByteArrayOutputStream(); + // 具体读入数据并写出的过程 + int len; + byte[] data = new byte[1024]; + while ((len = bis.read(data)) != -1) { + baos.write(data, 0, len); + } + // 获取内存中的完整的字节数组的数据 + byte[] byteCodes = baos.toByteArray(); + // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 + Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); + return clazz; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (baos != null) + baos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + if (bis != null) + bis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; } } ``` -判断结果: - ```java -public class Demo { - public static void main(String[] args) { - int i = 0; - int x = 0; - while (i < 10) { - x = x++; - i++; - } - System.out.println(x); // 结果是 0 +public static void main(String[] args) { + MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + + try { + Class clazz = loader.loadClass("Demo1"); + System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader + + System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader + } catch (ClassNotFoundException e) { + e.printStackTrace(); } } ``` -*** +**** -##### 类型转换 +#### JDK9 -类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 +为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: -宽化类型转换: +* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 -* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 - * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d - * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d - * 从 float 类型到 double 类型,对应的指令为 f2d +* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 -* 精度损失问题 - * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 - * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 +* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` -* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 + -窄化类型转换: -* Java 虚拟机直接支持以下窄化类型转换: - * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s - * 从 long 类型到 int 类型,对应的指令有 l2i - * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l - * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -* 精度损失问题: - * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 - * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: - - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 - - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 +*** -*** +## 运行机制 -##### 创建访问 +### 执行过程 -创建指令: + Java 文件编译执行的过程: -* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) - ```java - 0: new #2 // class com/jvm/bytecode/Demo - 3: dup - 4: invokespecial #3 // Method "":()V - ``` +- 类加载器:用于装载字节码文件(.class文件) +- 运行时数据区:用于分配存储空间 +- 执行引擎:执行字节码文件或本地方法 +- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 - **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: - - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) - - 一个要配合 astore_1 赋值给局部变量 -* 创建数组的指令:newarray、anewarray、multianewarray +**** - * newarray:创建基本类型数组 - * anewarray:创建引用类型数组 - * multianewarray:创建多维数组 -字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 -* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic -* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +### 字节码 -类型检查指令:检查类实例或数组类型的指令 +#### 跨平台性 -* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +Java 语言:跨平台的语言(write once ,run anywhere) -* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 +* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** +* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 +编译过程中的编译器: +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + * IntelliJ IDEA 使用 javac 编译器 + * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 + * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 -**** +* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 + * JIT编译器:执行引擎部分详解 + * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 +* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, -##### 方法指令 + * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 + + * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 + * 缺点: + * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 + * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 -方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -**方法调用章节详解** @@ -13273,140 +13333,101 @@ public class Demo { -##### 操作数栈 +#### 语言发展 -JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 +机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 -* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 +指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 -* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 +指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 +- x86 指令集,对应的是 x86 架构的平台 +- ARM 指令集,对应的是 ARM 架构的平台 +汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 +* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 -*** +高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 +* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 -##### 控制转移 + -比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -* lcmp:比较两个 long 类型值 -* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) -* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) -* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) -* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) +*** -条件跳转指令: -| 指令 | 说明 | -| --------- | -------------------------------------------------- | -| ifeq | equals,当栈顶int类型数值等于0时跳转 | -| ifne | not equals,当栈顶in类型数值不等于0时跳转 | -| iflt | lower than,当栈顶in类型数值小于0时跳转 | -| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | -| ifgt | greater than,当栈顶int类型数组大于0时跳转 | -| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | -| ifnull | 为 null 时跳转 | -| ifnonnull | 不为 null 时跳转 | -比较条件跳转指令: -| 指令 | 说明 | -| --------- | --------------------------------------------------------- | -| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | -| if_icmpne | 当前者不等于后者时跳转 | -| if_icmplt | 当前者小于后者时跳转 | -| if_icmple | 当前者小于等于后者时跳转 | -| if_icmpgt | 当前者大于后者时跳转 | -| if_icmpge | 当前者大于等于后者时跳转 | -| if_acmpeq | 当结果相等时跳转 | -| if_acmpne | 当结果不相等时跳转 | -多条件分支跳转指令: +#### 类结构 -* tableswitch:用于 switch 条件跳转,case 值连续 -* lookupswitch:用于 switch 条件跳转,case 值不连续 - -无条件跳转指令: - -* goto:用来进行跳转到指定行号的字节码 - -* goto_w:无条件跳转(宽索引) - - - - - -*** +##### 文件结构 +字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 -##### 异常处理 +JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html -###### 处理机制 +根据 JVM 规范,类文件结构如下: -抛出异常指令:athrow 指令 +```java +ClassFile { + u4 magic; + u2 minor_version; + u2 major_version; + u2 constant_pool_count; + cp_info constant_pool[constant_pool_count-1]; + u2 access_flags; + u2 this_class; + u2 super_class; + u2 interfaces_count; + u2 interfaces[interfaces_count]; + u2 fields_count; + field_info fields[fields_count]; + u2 methods_count; + method_info methods[methods_count]; + u2 attributes_count; + attribute_info attributes[attributes_count]; +} +``` -JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +| 类型 | 名称 | 说明 | 长度 | 数量 | +| -------------- | ------------------- | -------------------- | ------- | --------------------- | +| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | +| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | +| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | +| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | +| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | +| u2 | access_flags | 访问标识 | 2个字节 | 1 | +| u2 | this_class | 类索引 | 2个字节 | 1 | +| u2 | super_class | 父类索引 | 2个字节 | 1 | +| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | +| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | +| u2 | fields_count | 字段计数器 | 2个字节 | 1 | +| field_info | fields | 字段表 | n个字节 | fields_count | +| u2 | methods_count | 方法计数器 | 2个字节 | 1 | +| method_info | methods | 方法表 | n个字节 | methods_count | +| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | +| attribute_info | attributes | 属性表 | n个字节 | attributes_count | -* 代码: +Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 - ```java - public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (Exception e) { - i = 20; - } finally { - i = 30; - } - } - ``` - -* 字节码: +* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 +* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 - * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 - * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 +获取方式: - ```java - 0: iconst_0 - 1: istore_1 // 0 -> i ->赋值 - 2: bipush 10 // try 10 放入操作数栈顶 - 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 - 5: bipush 30 // 【finally】 - 7: istore_1 // 30 -> i - 8: goto 27 // return ----------------------------------- - 11: astore_2 // catch Exceptin -> e ---------------------- - 12: bipush 20 // - 14: istore_1 // 20 -> i - 15: bipush 30 // 【finally】 - 17: istore_1 // 30 -> i - 18: goto 27 // return ----------------------------------- - 21: astore_3 // catch any -> slot 3 ---------------------- - 22: bipush 30 // 【finally】 - 24: istore_1 // 30 -> i - 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 - 26: athrow // throw 抛出异常 - 27: return - Exception table: - // 任何阶段出现任务异常都会执行 finally - from to target type - 2 5 11 Class java/lang/Exception - 2 5 21 any // 剩余的异常类型,比如 Error - 11 15 21 any // 剩余的异常类型,比如 Error - LineNumberTable: ... - LocalVariableTable: - Start Length Slot Name Signature - 12 3 2 e Ljava/lang/Exception; - 0 28 0 args [Ljava/lang/String; - 2 26 1 i I - ``` +* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 +* 写入文件指令 `javap -v xxx.class >xxx.txt` +* IDEA 插件 jclasslib @@ -13414,418 +13435,482 @@ JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是* -###### finally - -finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) - -* 代码: - - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +##### 魔数版本 -* 字节码: +魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` +* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 +版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -*** +* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +| 主版本(十进制) | 副版本(十进制) | 编译器版本 | +| ---------------- | ---------------- | ---------- | +| 45 | 3 | 1.1 | +| 46 | 0 | 1.2 | +| 47 | 0 | 1.3 | +| 48 | 0 | 1.4 | +| 49 | 0 | 1.5 | +| 50 | 0 | 1.6 | +| 51 | 0 | 1.7 | +| 52 | 0 | 1.8 | +| 53 | 0 | 1.9 | +| 54 | 0 | 1.10 | +| 55 | 0 | 1.11 | +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) -###### return -* 吞异常 - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` - * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 - * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -* 不吞异常 +*** - ```java - public class Demo { - public static void main(String[] args) { - int result = test(); - System.out.println(result);//10 - } - public static int test() { - int i = 10; - try { - return i;//返回10 - } finally { - i = 20; - } - } - } - ``` - - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 赋值给i,放入slot 0 - 3: iload_0 // i(10)加载至操作数栈 - 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 - 5: bipush 20 // 20 放入栈顶 - 7: istore_0 // 20 slot 0 - 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 - 9: ireturn // 返回栈顶的 int(10) - 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 - 11: bipush 20 - 13: istore_0 - 14: aload_2 - 15: athrow // 不会吞掉异常 - Exception table: - from to target type - 3 5 10 any - ``` +##### 常量池 -*** +常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 -##### 同步控制 +* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 -方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 + * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 -方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 + * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x -* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 -* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 + * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - + | 标志符 | 含义 | + | ------ | --------------------------------------------------------- | + | B | 基本数据类型 byte | + | C | 基本数据类型 char | + | D | 基本数据类型 double | + | F | 基本数据类型 float | + | I | 基本数据类型 int | + | J | 基本数据类型 long | + | S | 基本数据类型 short | + | Z | 基本数据类型 boolean | + | V | 代表 void 类型 | + | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | + | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +常量类型和结构: +| 类型 | 标志(或标识) | 描述 | +| -------------------------------- | ------------ | ---------------------- | +| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Integer_info | 3 | 整型字面量 | +| CONSTANT_Float_info | 4 | 浮点型字面量 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | +| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | +| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | +| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | +| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | +| CONSTANT_MethodType_info | 16 | 标志方法类型 | +| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | +18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer -*** +**** -#### 执行流程 -原始 Java 代码: +##### 访问标识 -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = Short.MAX_VALUE + 1; - int c = a + b; - System.out.println(c); - } -} -``` +访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 -javap -v Demo.class:省略 +* 类的访问权限通常为 ACC_ 开头的常量 +* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` +* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 -* 常量池载入运行时常量池 +| 标志名称 | 标志值 | 含义 | +| -------------- | ------ | ------------------------------------------------------------ | +| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | +| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | +| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | +| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | +| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | +| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | +| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | +| ACC_ENUM | 0x4000 | 标志这是一个枚举 | -* 方法区字节码载入方法区 -* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) -* **执行引擎**开始执行字节码 +*** - `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 - * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) - * ldc 将一个 int 压入操作数栈 - * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) - * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 - `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 +##### 索引集合 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) +类索引、父类索引、接口索引集合 - `ldc #3`:从常量池加载 #3 数据到操作数栈 - Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 +* 类索引用于确定这个类的全限定名 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) +* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 - `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 +* 接口索引集合就用来描述这个类实现了哪些接口 + * interfaces_count 项的值表示当前类或接口的直接超接口数量 + * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 - `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 +| 长度 | 含义 | +| ---- | ---------------------------- | +| u2 | this_class | +| u2 | super_class | +| u2 | interfaces_count | +| u2 | interfaces[interfaces_count] | - `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 - `iadd`:执行相加操作 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) +*** - `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 - `getstatic #4`:获取静态字段 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) +##### 字段表 - `iload_3`: +字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) +fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 - `invokevirtual #5`: +fields[](字段表): - * 找到常量池 #5 项 - * 定位到方法区 java/io/PrintStream.println:(I)V 方法 - * **生成新的栈帧**(分配 locals、stack等) - * 传递参数,执行新栈帧中的字节码 - * 执行完毕,弹出栈帧 - * 清除 main 操作数栈内容 +* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) +* 字段访问标识: - return:完成 main 方法调用,弹出 main 栈帧,程序结束 + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为public | + | ACC_PRIVATE | 0x0002 | 字段是否为private | + | ACC_PROTECTED | 0x0004 | 字段是否为protected | + | ACC_STATIC | 0x0008 | 字段是否为static | + | ACC_FINAL | 0x0010 | 字段是否为final | + | ACC_VOLATILE | 0x0040 | 字段是否为volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为enum | - +* 字段名索引:根据该值查询常量池中的指定索引项即可 +* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 + | 字符 | 类型 | 含义 | + | ----------- | --------- | ----------------------- | + | B | byte | 有符号字节型树 | + | C | char | Unicode字符,UTF-16编码 | + | D | double | 双精度浮点数 | + | F | float | 单精度浮点数 | + | I | int | 整型数 | + | J | long | 长整数 | + | S | short | 有符号短整数 | + | Z | boolean | 布尔值true/false | + | V | void | 代表void类型 | + | L Classname | reference | 一个名为Classname的实例 | + | [ | reference | 一个一维数组 | -*** +* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; + u4 attribute_length; + u2 constantvalue_index; + } + ``` + 对于常量属性而言,attribute_length 值恒为2 -### 执行引擎 -#### 基本介绍 -执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 +*** -虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 -* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: +##### 方法表 -* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 -* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 +方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 +* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 +* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 -*** +methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +* 方法表结构如下: -#### 执行方式 + | 类型 | 名称 | 含义 | 数量 | + | -------------- | ---------------- | ---------- | ---------------- | + | u2 | access_flags | 访问标志 | 1 | + | u2 | name_index | 字段名索引 | 1 | + | u2 | descriptor_index | 描述符索引 | 1 | + | u2 | attrubutes_count | 属性计数器 | 1 | + | attribute_info | attributes | 属性集合 | attributes_count | -HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 +* 方法表访问标志: -HostSpot JVM 的默认执行方式: + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为 public | + | ACC_PRIVATE | 0x0002 | 字段是否为 private | + | ACC_PROTECTED | 0x0004 | 字段是否为 protected | + | ACC_STATIC | 0x0008 | 字段是否为 static | + | ACC_FINAL | 0x0010 | 字段是否为 final | + | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为 enum | -* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) -* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -HotSpot VM 可以通过 VM 参数设置程序执行方式: -- -Xint:完全采用解释器模式执行程序 -- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 -- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) +##### 属性表 -*** +属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +attributes_ count(属性计数器):表示当前文件属性表的成员个数 +attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 -#### 热点探测 +* 属性的通用格式: -热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; //属性名索引 + u4 attribute_length; //属性长度 + u2 attribute_info; //属性表 + } + ``` -热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +* 属性类型: -JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 + | 属性名称 | 使用位置 | 含义 | + | ------------------------------------- | ------------------ | ------------------------------------------------------------ | + | Code | 方法表 | Java 代码编译成的字节码指令 | + | ConstantValue | 字段表 | final 关键字定义的常量池 | + | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | + | Exceptions | 方法表 | 方法抛出的异常 | + | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | + | InnerClass | 类文件 | 内部类列表 | + | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | + | LocalVariableTable | Code 属性 | 方法的局部变量描述 | + | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | + | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | + | SourceFile | 类文件 | 记录源文件名称 | + | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | + | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | + | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | + | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | + | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | + | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | + | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | + | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | + | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | -* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI -* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 -HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) -* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 - 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** -* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 +**** - 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 +#### 编译指令 -*** +##### javac +javac:编译命令,将 java 源文件编译成 class 字节码文件 +`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 -#### 分层编译 -HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +**** -* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 - 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 - ```java - private static int square(final int i) { - return i * i; - } - System.out.println(square(9)); - ``` +##### javap - square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: +javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - ```java - System.out.println(9 * 9); - ``` +用法:javap - 还能够进行常量折叠(constant folding)的优化: +```sh +-help --help -? 输出此用法消息 +-version 版本信息 +-public 仅显示公共类和成员 +-protected 显示受保护的/公共类和成员 +-package 显示程序包/受保护的/公共类和成员 (默认) +-p -private 显示所有类和成员 + #常用的以下三个 +-v -verbose 输出附加信息 +-l 输出行号和本地变量表 +-c 对代码进行反汇编 #反编译 - ```java - System.out.println(81); - ``` +-s 输出内部类型签名 +-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) +-constants 显示最终常量 +-classpath 指定查找用户类文件的位置 +-cp 指定查找用户类文件的位置 +-bootclasspath 覆盖引导类文件的位置 +``` -* 冗余消除:根据运行时状况进行代码折叠或削除 -* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 +*** -C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -VM 参数设置: -- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 -- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 -- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +#### 指令集 -分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: +##### 执行指令 -* 0 层,解释执行(Interpreter) +Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) -* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 -* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) +在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 -* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) +* i 代表对 int 类型的数据操作 +* l 代表 long +* s 代表 short +* b 代表 byte +* c 代表 char +* f 代表 float +* d 代表 double -* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 - 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 +在做值相关操作时: +- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 +- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 -参考文章:https://www.jianshu.com/p/20bd2e9b1f03 +*** -*** +##### 加载存储 +加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 -### 方法调用 +局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 -#### 方法识别 +* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 +* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 +* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 -Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 -* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 -* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 +* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 +* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 +* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 -JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 +出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 -```java -// 返回值类型不同,编译阶段直接报错 -public static Integer invoke(Object... args) { - return 1; -} -public static int invoke(Object... args) { - return 2; -} -``` +* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 +* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s +扩充局部变量表的访问索引的指令:wide -*** +**** -#### 调用机制 -方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 +##### 算术指令 -在 JVM 中,将符号引用转换为直接引用有两种机制: +算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 -- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) +没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 -对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: +* 加法指令:iadd、ladd、fadd、dadd +* 减法指令:isub、lsub、fsub、dsub +* 乘法指令:imu、lmu、fmul、dmul +* 除法指令:idiv、ldiv、fdiv、ddiv +* 求余指令:irem、lrem、frem、drem(remainder 余数) +* 取反指令:ineg、lneg、fneg、dneg (negation 取反) +* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) +* 位运算指令,又可分为: + - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr + - 按位或指令:ior、lor + - 按位与指令:iand、land + - 按位异或指令:ixor、lxor -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 +* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +运算模式: -非虚方法: +* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 +* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 -- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 -- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 -- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 -动态类型语言和静态类型语言: +```java +double j = i / 0.0; +System.out.println(j);//无穷大,NaN: not a number +``` -- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc -- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +```java + 4 iload_1 //存入操作数栈 + 5 iinc 1 by 1 //自增i++ + 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 + 9 iinc 2 by 1 //++i +12 iload_2 //加载到操作数栈 +13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 +``` -- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 +```java +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = a++ + ++a + a--; + System.out.println(a); //11 + System.out.println(b); //34 + } +} +``` - ```java - String s = "abc"; //Java - info = "abc"; //Python - ``` +判断结果: + +```java +public class Demo { + public static void main(String[] args) { + int i = 0; + int x = 0; + while (i < 10) { + x = x++; + i++; + } + System.out.println(x); // 结果是 0 + } +} +``` @@ -13833,34 +13918,36 @@ public static int invoke(Object... args) { -#### 调用指令 - -##### 五种指令 +##### 类型转换 -普通调用指令: +类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 -- invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 -- invokevirtual:调用所有虚方法(虚方法分派) -- invokeinterface:调用接口方法 +宽化类型转换: -动态调用指令: +* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 + * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d + * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d + * 从 float 类型到 double 类型,对应的指令为 f2d -- invokedynamic:动态解析出需要调用的方法 - - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 - - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +* 精度损失问题 + * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 + * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 -指令对比: +* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 -- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 -- 动态调用指令支持用户确定方法 -- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 -- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 +窄化类型转换: -指令说明: +* Java 虚拟机直接支持以下窄化类型转换: + * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s + * 从 long 类型到 int 类型,对应的指令有 l2i + * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l + * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 -- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 +* 精度损失问题: + * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 + * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: + - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 + - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 @@ -13868,89 +13955,52 @@ public static int invoke(Object... args) { -##### 符号引用 +##### 创建访问 -在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 +创建指令: -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 +* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 -符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: + ```java + 0: new #2 // class com/jvm/bytecode/Demo + 3: dup + 4: invokespecial #3 // Method "":()V + ``` -```java -Constant pool: -... - #16 = InterfaceMethodref #27.#29 // 接口 -... - #22 = Methodref #1.#33 // 非接口 -... -``` + **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: -对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: + - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) + - 一个要配合 astore_1 赋值给局部变量 -1. 在 C 中查找符合名字及描述符的方法 -2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 -3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 +* 创建数组的指令:newarray、anewarray、multianewarray -对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + * newarray:创建基本类型数组 + * anewarray:创建引用类型数组 + * multianewarray:创建多维数组 -1. 在 I 中查找符合名字及描述符的方法 -2. 如果没有找到,在 Object 类中的公有实例方法中搜索 -3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 +字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 +* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic +* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +类型检查指令:检查类实例或数组类型的指令 -*** +* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 -##### 执行流程 -```java -public class Demo { - public Demo() { } - private void test1() { } - private final void test2() { } - public void test3() { } - public static void test4() { } +**** - public static void main(String[] args) { - Demo3_9 d = new Demo3_9(); - d.test1(); - d.test2(); - d.test3(); - d.test4(); - Demo.test4(); - } -} -``` -几种不同的方法调用对应的字节码指令: -```java -0: new #2 // class cn/jvm/t3/bytecode/Demo -3: dup -4: invokespecial #3 // Method "":()V -7: astore_1 -8: aload_1 -9: invokespecial #4 // Method test1:()V -12: aload_1 -13: invokespecial #5 // Method test2:()V -16: aload_1 -17: invokevirtual #6 // Method test3:()V -20: aload_1 -21: pop -22: invokestatic #7 // Method test4:()V -25: invokestatic #7 // Method test4:()V -28: return -``` +##### 方法指令 -- invokespecial 调用该对象的构造方法 :()V +方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 - - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 - - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 +**方法调用章节详解** @@ -13958,129 +14008,177 @@ public class Demo { -#### 多态原理 - -##### 执行原理 - -Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 - -理解多态: +##### 操作数栈 -- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 -- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) -- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 +JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 -方法重写的本质: +* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 +* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 -1. 找到操作数栈的第一个元素**所执行的对象的实际类型**,记作 C +* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 -2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 +* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 -3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 -4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 +*** -*** +##### 控制转移 +比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -##### 虚方法表 +* lcmp:比较两个 long 类型值 +* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) +* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) +* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) +* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) -在虚拟机工作过程中会频繁使用到动态绑定,每次动态绑定的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在每个类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 +条件跳转指令: -* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 - 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class - 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 -* invokeinterface 所使用的接口方法表(interface method table,itable) +| 指令 | 说明 | +| --------- | -------------------------------------------------- | +| ifeq | equals,当栈顶int类型数值等于0时跳转 | +| ifne | not equals,当栈顶in类型数值不等于0时跳转 | +| iflt | lower than,当栈顶in类型数值小于0时跳转 | +| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | +| ifgt | greater than,当栈顶int类型数组大于0时跳转 | +| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | +| ifnull | 为 null 时跳转 | +| ifnonnull | 不为 null 时跳转 | -虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕 +比较条件跳转指令: -虚方法表的执行过程: +| 指令 | 说明 | +| --------- | --------------------------------------------------------- | +| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | +| if_icmpne | 当前者不等于后者时跳转 | +| if_icmplt | 当前者小于后者时跳转 | +| if_icmple | 当前者小于等于后者时跳转 | +| if_icmpgt | 当前者大于后者时跳转 | +| if_icmpge | 当前者大于等于后者时跳转 | +| if_acmpeq | 当结果相等时跳转 | +| if_acmpne | 当结果不相等时跳转 | -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) +多条件分支跳转指令: -为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 +* tableswitch:用于 switch 条件跳转,case 值连续 +* lookupswitch:用于 switch 条件跳转,case 值不连续 -方法表满足以下的特质: +无条件跳转指令: -* 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 +* goto:用来进行跳转到指定行号的字节码 - +* goto_w:无条件跳转(宽索引) -Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 -虚方法表对性能的影响: -* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 -* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) -```java -class Person { - public String toString() { - return "I'm a person."; - } - public void eat() {} - public void speak() {} -} -class Boy extends Person { - public String toString() { - return "I'm a boy"; - } - public void speak() {} - public void fight() {} -} +*** -class Girl extends Person { - public String toString() { - return "I'm a girl"; - } - public void speak() {} - public void sing() {} -} -``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-虚方法表指向.png) +##### 异常处理 +###### 处理机制 -参考文档:https://www.cnblogs.com/kaleidoscope/p/9790766.html +抛出异常指令:athrow 指令 +JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +* 代码: -*** + ```java + public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (Exception e) { + i = 20; + } finally { + i = 30; + } + } + ``` + +* 字节码: + * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 + * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 + ```java + 0: iconst_0 + 1: istore_1 // 0 -> i ->赋值 + 2: bipush 10 // try 10 放入操作数栈顶 + 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 + 5: bipush 30 // 【finally】 + 7: istore_1 // 30 -> i + 8: goto 27 // return ----------------------------------- + 11: astore_2 // catch Exceptin -> e ---------------------- + 12: bipush 20 // + 14: istore_1 // 20 -> i + 15: bipush 30 // 【finally】 + 17: istore_1 // 30 -> i + 18: goto 27 // return ----------------------------------- + 21: astore_3 // catch any -> slot 3 ---------------------- + 22: bipush 30 // 【finally】 + 24: istore_1 // 30 -> i + 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 + 26: athrow // throw 抛出异常 + 27: return + Exception table: + // 任何阶段出现任务异常都会执行 finally + from to target type + 2 5 11 Class java/lang/Exception + 2 5 21 any // 剩余的异常类型,比如 Error + 11 15 21 any // 剩余的异常类型,比如 Error + LineNumberTable: ... + LocalVariableTable: + Start Length Slot Name Signature + 12 3 2 e Ljava/lang/Exception; + 0 28 0 args [Ljava/lang/String; + 2 26 1 i I + ``` -##### 内联缓存 -内联缓存:是一种加快动态绑定的优化技术,能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,便会直接调用该类型所对应的目标方法;反之内联缓存则会退化至使用基于方法表的动态绑定 -多态的三个术语: +*** -* 单态 (monomorphic):指的是仅有一种状态的情况 -* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 -* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 -对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: -* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 -* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 +###### finally -为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: +finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) -* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 -* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 +* 代码: -虽然内联缓存附带内联二字,但是并没有内联目标方法 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` +* 字节码: + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` @@ -14088,30 +14186,73 @@ class Girl extends Person { -### 代码优化 +###### return -#### 语法糖 - -语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 +* 吞异常 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` -#### 构造器 + * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 + * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -```java -public class Candy1 { -} -``` +* 不吞异常 -```java -public class Candy1 { - // 这个无参构造是编译器帮助我们加上的 - public Candy1() { - super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." - ":()V - } -} -``` + ```java + public class Demo { + public static void main(String[] args) { + int result = test(); + System.out.println(result);//10 + } + public static int test() { + int i = 10; + try { + return i;//返回10 + } finally { + i = 20; + } + } + } + ``` + + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 赋值给i,放入slot 0 + 3: iload_0 // i(10)加载至操作数栈 + 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 + 5: bipush 20 // 20 放入栈顶 + 7: istore_0 // 20 slot 0 + 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 + 9: ireturn // 返回栈顶的 int(10) + 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 + 11: bipush 20 + 13: istore_0 + 14: aload_2 + 15: athrow // 不会吞掉异常 + Exception table: + from to target type + 3 5 10 any + ``` @@ -14119,130 +14260,100 @@ public class Candy1 { -#### 拆装箱 +##### 同步控制 -```java -Integer x = 1; -int y = x; -``` +方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 -这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: +方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 -```java -Integer x = Integer.valueOf(1); -int y = x.intValue(); -``` +* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 +* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 -JDK5 以后编译阶段自动转换成上述片段 + -*** +*** -#### 泛型擦除 -泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 -在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: -```java -List list = new ArrayList<>(); -list.add(10); // 实际调用的是 List.add(Object e) -Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); -``` +#### 执行流程 -编译器真正生成的字节码中,还要额外做一个类型转换的操作: +原始 Java 代码: ```java -// 需要将 Object 转为 Integer -Integer x = (Integer)list.get(0); +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = Short.MAX_VALUE + 1; + int c = a + b; + System.out.println(c); + } +} ``` -如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: - -```java -// 需要将 Object 转为 Integer, 并执行拆箱操作 -int x = ((Integer)list.get(0)).intValue(); -``` +javap -v Demo.class:省略 +* 常量池载入运行时常量池 +* 方法区字节码载入方法区 -*** +* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) +* **执行引擎**开始执行字节码 + `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 -#### 可变参数 + * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) + * ldc 将一个 int 压入操作数栈 + * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) + * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 -```java -public class Candy4 { - public static void foo(String... args) { - String[] array = args; // 直接赋值 - System.out.println(array); - } - public static void main(String[] args) { - foo("hello", "world"); - } -} -``` + `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 -可变参数 `String... args` 其实是 `String[] args` , java 编译器会在编译期间将上述代码变换为: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) -```java -public static void main(String[] args) { - foo(new String[]{"hello", "world"}); -} -``` + `ldc #3`:从常量池加载 #3 数据到操作数栈 + Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 -注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) + `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 + `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 -**** + `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 + `iadd`:执行相加操作 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) -#### foreach + `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 -**数组的循环:** + `getstatic #4`:获取静态字段 -```java -int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 -for (int e : array) { - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) -编译后为循环取数: + `iload_3`: -```java -for(int i = 0; i < array.length; ++i) { - int e = array[i]; - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) -**集合的循环:** + `invokevirtual #5`: -```java -List list = Arrays.asList(1,2,3,4,5); -for (Integer i : list) { - System.out.println(i); -} -``` + * 找到常量池 #5 项 + * 定位到方法区 java/io/PrintStream.println:(I)V 方法 + * **生成新的栈帧**(分配 locals、stack等) + * 传递参数,执行新栈帧中的字节码 + * 执行完毕,弹出栈帧 + * 清除 main 操作数栈内容 -编译后转换为对迭代器的调用: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) -```java -List list = Arrays.asList(1, 2, 3, 4, 5); -Iterator iter = list.iterator(); -while(iter.hasNext()) { - Integer e = (Integer)iter.next(); - System.out.println(e); -} -``` + return:完成 main 方法调用,弹出 main 栈帧,程序结束 -注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器 + @@ -14250,56 +14361,21 @@ while(iter.hasNext()) { -#### switch - -##### 字符串 - -从 JDK 开始,switch 可以作用于字符串和枚举类: +### 执行引擎 -```java -switch (str) { - case "hello": { - System.out.println("h"); - break; - } - case "world": { - System.out.println("w"); - break; - } -} -``` +#### 基本介绍 -注意:**switch 配合 String 和枚举使用时,变量不能为null** +执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 -会被编译器转换为: +虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -```java -byte x = -1; -switch(str.hashCode()) { - case 99162322: // hello 的 hashCode - if (str.equals("hello")) { - x = 0; - } - break; - case 113318802: // world 的 hashCode - if (str.equals("world")) { - x = 1; - } -} -switch(x) { - case 0: - System.out.println("h"); - break; - case 1: - System.out.println("w"); - break; -} -``` +* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 +* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -总结: +Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: -* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 -* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 +* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 @@ -14307,249 +14383,116 @@ switch(x) { -##### 枚举 +#### 执行方式 -switch 枚举的例子,原始代码: +HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 -```java -enum Sex { - MALE, FEMALE -} -public class Candy7 { - public static void foo(Sex sex) { - switch (sex) { - case MALE: - System.out.println("男"); - break; - case FEMALE: - System.out.println("女"); - break; - } - } -} -``` +HostSpot JVM 的默认执行方式: -编译转换后的代码: +* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) +* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -```java -/** -* 定义一个合成类(仅 jvm 使用,对我们不可见) -* 用来映射枚举的 ordinal 与数组元素的关系 -* 枚举的 ordinal 表示枚举对象的序号,从 0 开始 -* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 -*/ -static class $MAP { - // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字 - static int[] map = new int[2]; - static { - map[Sex.MALE.ordinal()] = 1; - map[Sex.FEMALE.ordinal()] = 2; - } -} -public static void foo(Sex sex) { - int x = $MAP.map[sex.ordinal()]; - switch (x) { - case 1: - System.out.println("男"); - break; - case 2: - System.out.println("女"); - break; - } -} -``` +HotSpot VM 可以通过 VM 参数设置程序执行方式: +- -Xint:完全采用解释器模式执行程序 +- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 +- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) -*** +*** -#### 枚举类 -JDK 7 新增了枚举类: -```java -enum Sex { - MALE, FEMALE -} -``` +#### 热点探测 -编译转换后: +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 -```java -public final class Sex extends Enum { - public static final Sex MALE; - public static final Sex FEMALE; - private static final Sex[] $VALUES; - static { - MALE = new Sex("MALE", 0); - FEMALE = new Sex("FEMALE", 1); - $VALUES = new Sex[]{MALE, FEMALE}; - } - private Sex(String name, int ordinal) { - super(name, ordinal); - } - public static Sex[] values() { - return $VALUES.clone(); - } - public static Sex valueOf(String name) { - return Enum.valueOf(Sex.class, name); - } -} -``` +热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 +* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI +* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 +HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 -*** + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** +* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 + 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 -#### try-w-r -JDK 7 开始新增了对需要关闭的资源处理的特殊语法 `try-with-resources`,格式: -```java -try(资源变量 = 创建资源对象){ -} catch( ) { -} -``` +*** -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: -```java -try(InputStream is = new FileInputStream("d:\\1.txt")) { - System.out.println(is); -} catch (IOException e) { - e.printStackTrace(); -} -``` -转换成: +#### 分层编译 -`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(**fianlly 中如果抛出了异常**) +HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -```java -try { - InputStream is = new FileInputStream("d:\\1.txt"); - Throwable t = null; - try { - System.out.println(is); - } catch (Throwable e1) { - // t 是我们代码出现的异常 - t = e1; - throw e1; - } finally { - // 判断了资源不为空 - if (is != null) { - // 如果我们代码有异常 - if (t != null) { - try { - is.close(); - } catch (Throwable e2) { - // 如果 close 出现异常,作为被压制异常添加 - t.addSuppressed(e2); - } - } else { - // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e - is.close(); - } - } - } -} catch (IOException e) { - e.printStackTrace(); -} -``` +C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 + 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 -*** + ```java + private static int square(final int i) { + return i * i; + } + System.out.println(square(9)); + ``` + square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: + ```java + System.out.println(9 * 9); + ``` -#### 方法重写 + 还能够进行常量折叠(constant folding)的优化: -方法重写时对返回值分两种情况: + ```java + System.out.println(81); + ``` -* 父子类的返回值完全一致 -* 子类返回值可以是父类返回值的子类 +* 冗余消除:根据运行时状况进行代码折叠或削除 -```java -class A { - public Number m() { - return 1; - } -} -class B extends A { - @Override - // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 - public Integer m() { - return 2; - } -} -``` +* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -对于子类,java 编译器会做如下处理: +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 -```java -class B extends A { - public Integer m() { - return 2; - } - // 此方法才是真正重写了父类 public Number m() 方法 - public synthetic bridge Number m() { - // 调用 public Integer m() - return m(); - } -} -``` +C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 +VM 参数设置: +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 +- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: -*** +* 0 层,解释执行(Interpreter) +* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) -#### 匿名内部类 +* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) -##### 无参优化 +* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) -源代码: + 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 -```java -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok"); - } - }; - } -} -``` -转化后代码: -```java -// 额外生成的类 -final class Candy11$1 implements Runnable { - Candy11$1() { - } - public void run() { - System.out.println("ok"); - } -} -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Candy11$1(); - } -} -``` +参考文章:https://www.jianshu.com/p/20bd2e9b1f03 @@ -14557,181 +14500,138 @@ public class Candy11 { -##### 带参优化 +### 方法调用 -引用局部变量的匿名内部类,源代码: +#### 方法识别 + +Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) + +* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 +* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 + +JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 ```java -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok:" + x); - } - }; - } -} -``` - -转换后代码: - -```java -final class Candy11$1 implements Runnable { - int val$x; - Candy11$1(int x) { - this.val$x = x; - } - public void run() { - System.out.println("ok:" + this.val$x); - } +// 返回值类型不同,编译阶段直接报错 +public static Integer invoke(Object... args) { + return 1; } -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Candy11$1(x); - } +public static int invoke(Object... args) { + return 2; } ``` -局部变量在底层创建为内部类的成员变量,必须是 final 的原因: - -* 在 Java 中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 - -* 外部变量为 final 是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是**防止外部操作修改了变量而内部类无法随之变化**出现的影响 - - 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 - *** -#### 反射优化 +#### 调用机制 -```java -public class Reflect1 { - public static void foo() { - System.out.println("foo..."); - } - public static void main(String[] args) throws Exception { - Method foo = Reflect1.class.getMethod("foo"); - for (int i = 0; i <= 16; i++) { - System.out.printf("%d\t", i); - foo.invoke(null); - } - System.in.read(); - } -} -``` +方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 -foo.invoke 0 ~ 15次调用的是 MethodAccessor 的实现类 `NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 `sun.reflect.GeneratedMethodAccessor1` 代替 +在 JVM 中,将符号引用转换为直接引用有两种机制: -```java -public Object invoke(Object obj, Object[] args)throws Exception { - // inflationThreshold 膨胀阈值,默认 15 - if (++numInvocations > ReflectionFactory.inflationThreshold() - && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { - MethodAccessorImpl acc = (MethodAccessorImpl) - new MethodAccessorGenerator(). - generateMethod(method.getDeclaringClass(), - method.getName(), - method.getParameterTypes(), - method.getReturnType(), - method.getExceptionTypes(), - method.getModifiers()); - parent.setDelegate(acc); - } - // 【调用本地方法实现】 - return invoke0(method, obj, args); -} -private static native Object invoke0(Method m, Object obj, Object[] args); -``` +- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) +- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -```java -public class GeneratedMethodAccessor1 extends MethodAccessorImpl { - // 如果有参数,那么抛非法参数异常 - block4 : { - if (arrobject == null || arrobject.length == 0) break block4; - throw new IllegalArgumentException(); - } - try { - // 【可以看到,已经是直接调用方法】 - Reflect1.foo(); - // 因为没有返回值 - return null; - } - //.... -} -``` +对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: -通过查看 ReflectionFactory 源码可知: +- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 +- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 -* sun.reflect.noInflation 可以用来禁用膨胀,直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算 -* sun.reflect.inflationThreshold 可以修改膨胀阈值 +* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +非虚方法: +- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 +- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 +- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +动态类型语言和静态类型语言: +- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 -*** +- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 + ```java + String s = "abc"; //Java + info = "abc"; //Python + ``` -## 系统优化 +*** -### 性能调优 -#### 性能指标 -性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘 IO、网络 IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 +#### 调用指令 -几个重要的指标: +##### 五种指令 -1. 停顿时间(响应时间):提交请求和返回该请求的响应之间使用的时间,比如垃圾回收中 STW 的时间 -2. 吞吐量:对单位时间内完成的工作量(请求)的量度(可以对比 GC 的性能指标) -3. 并发数:同一时刻,对服务器有实际交互的请求数 -4. QPS:Queries Per Second,每秒处理的查询量 -5. TPS:Transactions Per Second,每秒产生的事务数 -6. 内存占用:Java 堆区所占的内存大小 +普通调用指令: +- invokestatic:调用静态方法 +- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokevirtual:调用所有虚方法(虚方法分派) +- invokeinterface:调用接口方法 +动态调用指令: -*** +- invokedynamic:动态解析出需要调用的方法 + - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 + - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +指令对比: +- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 +- 动态调用指令支持用户确定方法 +- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 +- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 -#### 优化步骤 +指令说明: -对于一个系统要部署上线时,则一定会对 JVM 进行调整,不经过任何调整直接上线,容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题 +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 +- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 -1. 性能监控:通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况 -2. 性能分析: - * 打印 GC 日志,通过 GCviewe r或者 http://gceasy.io 来分析异常信息 +*** - - 运用命令行工具、jstack、jmap、jinfo 等 - - dump 出堆文件,使用内存分析工具分析文件 - - 使用阿里 Arthas、jconsole、JVisualVM 来**实时查看 JVM 状态** +##### 符号引用 - - jstack 查看堆栈信息 +在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 -3. 性能调优: +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 - * 适当增加内存,根据业务背景选择垃圾回收器 +符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: - - 优化代码,控制内存使用 +```java +Constant pool: +... + #16 = InterfaceMethodref #27.#29 // 接口 +... + #22 = Methodref #1.#33 // 非接口 +... +``` - - 增加机器,分散节点压力 +对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: - - 合理设置线程池线程数量 +1. 在 C 中查找符合名字及描述符的方法 +2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 +3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 - - 使用中间件提高程序效率,比如缓存、消息队列等 +对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + +1. 在 I 中查找符合名字及描述符的方法 +2. 如果没有找到,在 Object 类中的公有实例方法中搜索 +3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 @@ -14739,3061 +14639,83 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { -#### 参数调优 +##### 执行流程 -对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 +```java +public class Demo { + public Demo() { } + private void test1() { } + private final void test2() { } -* 设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值 + public void test3() { } + public static void test4() { } - ```sh - -Xms:设置堆的初始化大小 - -Xmx:设置堆的最大大小 - ``` + public static void main(String[] args) { + Demo3_9 d = new Demo3_9(); + d.test1(); + d.test2(); + d.test3(); + d.test4(); + Demo.test4(); + } +} +``` -* 设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小,来减少 YGC 发生的次数,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优 +几种不同的方法调用对应的字节码指令: - ```sh - -XX:SurvivorRatio - ``` +```java +0: new #2 // class cn/jvm/t3/bytecode/Demo +3: dup +4: invokespecial #3 // Method "":()V +7: astore_1 +8: aload_1 +9: invokespecial #4 // Method test1:()V +12: aload_1 +13: invokespecial #5 // Method test2:()V +16: aload_1 +17: invokevirtual #6 // Method test3:()V +20: aload_1 +21: pop +22: invokestatic #7 // Method test4:()V +25: invokestatic #7 // Method test4:()V +28: return +``` -* 年轻代和老年代默认比例为 1:2,可以通过调整二者空间大小比率来设置两者的大小。 +- invokespecial 调用该对象的构造方法 :()V - ```sh - -XX:newSize 设置年轻代的初始大小 - -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 - ``` +- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 + - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 -* 线程堆栈的设置:**每个线程默认会开启 1M 的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 - ```sh - -Xss 对每个线程stack大小的调整,-Xss128k - ``` -* 一般一天超过一次 FullGC 就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整 JVM 参数 +*** -* 系统 CPU 持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 -* 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 -* 如果服务器配置还不错,JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器 +#### 多态原理 +##### 执行原理 +Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 +理解多态: +- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 +- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) +- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 -**** - - - - - -### 命令行篇 - -#### jps - -jps(Java Process Statu):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的 - -使用语法:`jps [options] [hostid]` - -options 参数: - -- -q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id,不显示主类的名称等 - -- -l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径 - -- -m:输出虚拟机进程启动时传递给主类 main()的参数 - -- -v:列出虚拟机进程启动时的JVM参数,比如 -Xms20m -Xmx50m是启动程序指定的 jvm 参数 - -ostid 参数:RMI注册表中注册的主机名,如果想要远程监控主机上的 java 程序,需要安装 jstatd - - - -**** - - - -#### jstat - -jstat(JVM Statistics Monitoring Tool):用于监视 JVM 各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 的图形界面,只提供了纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具,常用于检测垃圾回收问题以及内存泄漏问题 - -使用语法:`jstat -