码农戏码

新生代农民工的自我修养


  • 首页

  • 归档

  • 标签

  • 关于

  • 在线工具

  • 搜索

功夫在诗外

发表于 2017-12-17
字数统计: 1.6k 字数 | 阅读时长 ≈ 5 分钟

功夫在诗外

算是对今年,以及去年这两年的读书的一点思考,总结

为什么会有去年,因为今年读书过程中,让人不自然地想起了去年的类似内容。说的书类型不重复,但出现了很多相同的内容

可能与这些内容有缘分

迷思

每次跳槽的时候,都会去思考我的核心竞争力到底是什么?这个问题代表了两个对立的思想一方面代表了不自知,迷茫,对未来的焦虑;另一方面代表了自不知,三人有我师,前进的动力。

职场规划一片迷茫,股票所学也是一无长进,甚是苦恼。
想起了陆游的这句话,《汝果欲学诗,功夫在诗外》;所以去年开始读了很多与专业不相关的书籍。

名人传记,历史书籍,小说,这类书的价值在于引导成长;

心智,心理类也是所好,可惜所得甚少。一些书籍还得多读,多思

如《乌合之众》,知了很多年,却没读得很少,也没读懂。

路漫漫其修远兮

所得

在读《超级交易者》上半段时,突然有了一细细恍惚。好些内容都似曾相识,让人不得不想起一句老话,失败者的失败各有各的不同,而成功者的成功却具有惊人的相似之处。

责任

这个词就像是个空词,人人都会把这个词挂在嘴边,但何为责任,责任有何妙处

这在《人生五章》诗中也有体现

《人生五章》

1、

我走上街,

人行道上有一个深洞,

我掉了进去。

我迷失了……我绝望了。

这不是我的错,

费了好大的劲才爬出来。

2、

我走上同一条街。

人行道上有一个深洞,

我假装没看到,

还是掉了进去。

我不能相信我居然会掉在同样的地方。

但这不是我的错。

还是花了很长的时间才爬出来。

3、

我走上同一条街。

人行道上有一个深洞,

我看到它在那儿,

但还是掉了进去……

这是一种习惯。

我的眼睛张开着,

我知道我在那儿。

这是我的错。

我立刻爬了出来。

4、

我走上同一条街,

人行道上有一个深洞,

我绕道而过。

5、

我走上另一条街。

这首诗的内容在读《股票作手加快录》时,就有体会,大意就是一个人从意识到自身问题,到要改变,再到彻底改变的过程是相当艰辛的,习惯上升到性格再到内质的人性,是相当难以改变的。

这一切的改变,自省的起始就在于责任两字,无责任怎么会想到自身问题。

自己永无过错,一切都是别人的问题;代码没问题,是环境问题,自己买卖没问题,是庄家问题。

写作

为什么写作?其实有各种理由

  • 文字可以重塑人
  • 写作是与自己的对话,引人思考
  • 输出倒逼输入
  • 形成闭环,输入输出完整的IO系统
  • 写作是睡后收入

我算是写作的践行者,有什么意义呢?写作了很多年,有什么益处呢?

之前我是记录时间日志,是在看《时间就是朋友》这本书得来的。

后来李笑来又说要记注意力日志。还没有太多的实践,从时间开销日志来讲,只是发现了自我的浪费时间,这也算是所得之一吧。

从去年开始写博客,为什么要写呢?关键是“形成有效的输入输出系统,以输出倒逼输入”,“写作也记录下学习过程,防止狗咬尾巴”。

现在的博客目标是每篇文章写尽一个知识点,有广度,也得有深度。但任何知识都不是单点,‘台上一分钟,台下十年功’,‘冰山理论’。

晨写也没有以前的质量和数量了,上次交易玫瑰讲了量化:一是两张A4纸,二是得有三十分钟。

还有关键一点,我也疏忽的一点,没有及时回顾日记。需要温故知新。

冥想

这个之前实践了几天,后来也没有坚持。这以后还是得有空多练习,看看效果了

读书

很多人的理想生活就是 读万卷书,行万里路

我现在的读书方法,以及读书效果还是很差,不让人满意。关键是缺少总结,一本书,至少要自己组织语言总结其主旨。

不然读再多的书,还是一无所得,浪费时间,自我心理满足。不能学以致用。

上次读李笑来的财富自由之路,一个读书亮点:慢读。

现在很多的书籍都教人快速阅读以应对当代快速膨胀的知识,以快致胜。而李笑来提出,要慢读。

快速膨胀的是信息,而不是知识。知识的出现需要很久的时间积累。所以要慢读,要有深度,现在流行快餐文化,让世人缺少了深度思考的过程,以致人浮于思,任何事都没有深度,人云亦云。

导图

对于以上所得共同点,画了张思维导图更能一目了然

image

书单

这些书单是今年看过超出一半内容的书籍,有几本是年初读的,现在都已经忘记内容了。主要就是没有写一段自己理解的总结。这些书,明年还会再读,重读。吸取知识的能力实在是差。

《西藏生死书》

《刻意练习》

《简单思考》

《自卑与超越》

《超级交易员》

《我做散户这十年:三万赚到千万》

《走进我的交易室》

《炒股的智慧》

《红尘天幕》

今年没有达到目标,计划是一个月一本的,不找客观原因,明年还得继续努力。育儿方面的内容也得加强。

今年的博客也是打算一月一篇的,也没有正常发布。

计划是用来执行的。

微服务-监控

发表于 2017-10-15
字数统计: 1.9k 字数 | 阅读时长 ≈ 6 分钟

前言

这篇其实本来也打算放在《常识》系列中的,介绍一下分布式日志追踪系统,这在互联网界理论,技术,产品已经很成熟,国内外各大厂都有自己成熟的产品。是个不错的互联网门外汉科普知识点

微服务,已经火了多年,也已经落地实施。对服务的监控需求顺理成章。监控系统的本质其实也就是分布式日志追踪系统。就归类到《微服务》系列中吧

本篇大体内容

  1. 《微服务设计》第八章监控
  2. 监控理念Dapper
  3. 流行监控框架架构
  4. aspectj

《微服务》之监控

本来是说,要写个读书笔记的,但没有那么多完整的时间,正好学习监控,就把书拿出来,一并读了。理论结合实践,效果更好。

监控模型

三种监控模型

  1. 单一服务,单一服务器
  2. 单一服务,多个服务器
  3. 多个服务,多个服务器

单一服务单一服务器

  1. 主机状态

CPU、内存等,可以使用监控软件Nagios,Zabbix或者像New Relic这样的托管服务来帮助监控主机

  1. 服务状态

直接查看服务应用日志,或者web容器日志

单一服务多个服务器

  1. 主机状态

这种情况稍微复杂了一点,如前所述,如果我们想监控CPU,当CPU占用率过高时,如果这个问题发生在所有的服务器上,有可能是微服务本身的问题,但如果只发生在一台,则有可能是主机本身的问题。

我们需要关注每台服务器的日志数据,我们既想把数据聚合起来,又想深入分析每台主机,Nagios允许以这样的方式组织我们的主机。

  1. 服务状态

如果只有几个主机,可以用像ssh-multiplexers这样的工具,在多个主机上运行相同的命令。用一个大显示屏,运行grep “Error” app.log来定位错误。对于响应时间,可以在负载均衡器中跟踪,负载均衡器本身也需要跟踪。

多个服务多个服务器

这个情况就更复杂了,我们如何在多个主机上,成千上万行的日志中定位错误的原因?如果确定是一个服务器异常,还是一个系统性的问题?如何在多个主机跟踪一个错误的调用链,找出引起错误的原因?

答案是:从日志到应用程序指标,集中收集和聚合更可能多的数据

日志,更多的日志

需要将日志能够集中到一起方便使用

可以使用ELK

ELK由Elasticsearch、Logstash和Kibana三部分组件组成;

Elasticsearch是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。

Logstash是一个完全开源的工具,它可以对你的日志进行收集、分析,并将其存储供以后使用

kibana 是一个开源和免费的工具,它可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助您汇总、分析和搜索重要数据日志

image

监控指标

系统指标:比如cpu 内存等,这些可以collectd进行收集

服务指标:比如接口调用次数,线程池空闲线程数等

语义指标:类似业务指标,比如订单量,活动用户数等

关联标识

image

其实这就是服务追踪,调用链监控

因为微服务化后,各种系统之间的调用关系很复杂,因此排查一个问题会比较难受,你不需要一个系统一个系统去找问题。所以服务追踪就变得非常关键。他能够追踪一次会话的所有调用,哪里有了问题,一目了然

这个更详细的后面介绍google的dapper

其它

标准化:将监控api标准化。
考虑受众:谁看?运营还是开发?
更加实时:监控应该具有实时性,出问题第一时间反应。
避免级联危险:可以使用hystrix。

小结

对每个服务:跟踪请求响应时间、错误率和应用程序级指标;跟踪所有下游服务的健康状态,如调用时间、错误率;标准化如何收集和存储指标;以标准格式讲日志记录到一个标准位置;监控底层操作系统。

对系统:聚合CPU等主机层级的指标和程序级指标;确保指标存储工具可以在系统和服务级别做聚合,也能查看单台主机信息;指标存储工具允许维护数据足够长时间,以了解趋势;使用单个可查询工具对日志进行聚合和存储;强烈考虑标准化关联标识的使用;了解什么样的情况需要行动,并构造警报和仪表盘;调查对各种指标聚合和统一化的可能性。

Google Dapper

image

分布式服务的跟踪系统需要记录在一次特定的请求后系统中完成的所有工作的信息。举个例子,图展现的是一个和5台服务器相关的一个服务,包括:前端(A),两个中间层(B和C),以及两个后端(D和E)。当一个用户(这个用例的发起人)发起一个请求时,首先到达前端,然后发送两个RPC到服务器B和C。B会马上做出反应,但是C需要和后端的D和E交互之后再返还给A,由A来响应最初的请求。对于这样一个请求,简单实用的分布式跟踪的实现,就是为服务器上每一次你发送和接收动作来收集跟踪标识符(message identifiers)和时间戳(timestamped events)。

google dapper译文:http://bigbully.github.io/Dapper-translation/

Dapper有三个设计目标:

  1. 低消耗:跟踪系统对在线服务的影响应该做到足够小。

  2. 应用级的透明:对于应用的程序员来说,是不需要知道有跟踪系统这回事的。如果一个跟踪系统想生效,就必须需要依赖应用的开发者主动配合,那么这个跟踪系统显然是侵入性太强的。

  3. 延展性:Google至少在未来几年的服务和集群的规模,监控系统都应该能完全把控住。

监控框架

大的互联网公司都有自己的分布式跟踪系统,
比如Twitter的zipkin,淘宝的鹰眼,新浪的Watchman,京东的Hydra等

这些系统大多是基于dapper论文而来。

image

aspectj

监控系统,又名日志追踪系统,那主要还是打印日志嘛。

无侵入性的日志打印,AOP绝对是上选了

写了几个aspectj小示例
https://github.com/zhuxingsheng/aspectjdemo

当然,aspectj只是埋点,后面还有日志存储,实时计算,日志分析,监控展示

参考

http://www.cnblogs.com/gudi/p/6683653.html

http://blog.csdn.net/guwei9111986/article/details/51798394

微服务监控案例之一

深入浅出spring事务

发表于 2017-07-11 | 分类于 源码解读
字数统计: 9.4k 字数 | 阅读时长 ≈ 37 分钟

前言

这篇其实也要归纳到《常识》系列中,但这重点又是spring的介绍,故归档在spring系列中。

工作很多年,除了学生时代学过,事务还真没有用过。过去开发游戏时,完全不用事务;现在互联网开发,也没有使用事务的场景,不要见怪。

概念

对于事务(Transaction)的概念,网上有各种版本,大同小异,

事务就是是由一系列对系统中数据进行读写的操作组成的一个程序执行单元,狭义上的事务特指数据库事务。

事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。

在企业级应用程序开发中,事务管理必不可少的技术,用来确保数据的完整性和一致性。

比如你去ATM机取1000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉1000元钱;然后ATM出1000元钱。这两个步骤必须是要么都执行要么都不执行。如果银行卡扣除了1000块但是ATM出钱失败的话,你将会损失1000元;如果银行卡扣钱失败但是ATM却出了1000块,那么银行将损失1000元。所以,如果一个步骤成功另一个步骤失败对双方都不是好事,如果不管哪一个步骤失败了以后,整个取钱过程都能回滚,也就是完全取消所有操作的话,这对双方都是极好的。

事务的特性

大名鼎鼎的ACID

  1. 原子性(Atomicity),事务必须是一个原子的操作序列单元,一次事务只允许存在两种状态,全部成功或全部失败,任何一个操作失败都将导致整个事务失败
  2. 一致性(Consistency),事务的执行不能破坏系统数据的完整性和一致性,如果未完成的事务对系统数据的修改有一部分已经写入物理数据库,这时系统数据就处于不一致状态
  3. 隔离性(Isolation),在并发环境中,不同的事务操作相同的数据时,虚相互隔离不能相互干扰
  4. 持久性(Durability),事务一旦提交,对系统数据的变更就应该是永久的,必须被永久保存下来,即使服务器宕机了,只要数据库能够重新启动,就一定能够恢复到事务成功结束时的状态

事务并发处理问题

如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题。由于并发操作带来的数据不一致性包括:丢失数据修改、读”脏”数据(脏读)、不可重复读、产生幽灵数据:

假设数据库中有如下一张表:

image

第一类丢失更新(lost update)

回滚丢失

在完全未隔离事务的情况下,两个事物更新同一条数据资源,某一事物异常终止,回滚造成第一个完成的更新也同时丢失。
image

在T1时刻开启了事务1,T2时刻开启了事务2,

在T3时刻事务1从数据库中取出了id=”402881e535194b8f0135194b91310001”的数据,

T4时刻事务2取出了同一条数据,

T5时刻事务1将age字段值更新为30,

T6时刻事务2更新age为35并提交了数据,

但是T7事务1回滚了事务age最后的值依然为20,事务2的更新丢失了,

这种情况就叫做”第一类丢失更新(lost update)”。

脏读(dirty read)

事务没提交,提前读取

如果第二个事务查询到第一个事务还未提交的更新数据,形成脏读
image

在T1时刻开启了事务1,T2时刻开启了事务2,

在T3时刻事务1从数据库中取出了id=”402881e535194b8f0135194b91310001”的数据,

在T5时刻事务1将age的值更新为30,但是事务还未提交,

T6时刻事务2读取同一条记录,获得age的值为30,但是事务1还未提交,

若在T7时刻事务1回滚了事务2的数据就是错误的数据(脏数据),

这种情况叫做” 脏读(dirty read)”。

虚读(phantom read)

一个事务执行两次查询,第二次结果集包含第一次中没有或者某些行已被删除,造成两次结果不一致,只是另一个事务在这两次查询中间插入或者删除了数据造成的

image

在T1时刻开启了事务1,T2时刻开启了事务2,

T3时刻事务1从数据库中查询所有记录,记录总共有一条,

T4时刻事务2向数据库中插入一条记录,T6时刻事务2提交事务。

T7事务1再次查询数据数据时,记录变成两条了。

这种情况是”虚读(phantom read)”。

不可重复读(unrepeated read)

一个事务两次读取同一行数据,结果得到不同状态结果,如中间正好另一个事务更新了该数据,两次结果相异,不可信任
image

在T1时刻开启了事务1,T2时刻开启了事务2,

在T3时刻事务1从数据库中取出了id=”402881e535194b8f0135194b91310001”的数据,此时age=20,

T4时刻事务2查询同一条数据,

T5事务2更新数据age=30,T6时刻事务2提交事务,

T7事务1查询同一条数据,发现数据与第一次不一致。

这种情况就是”不可重复读(unrepeated read)”

第二类丢失更新(second lost updates)

覆盖丢失

不可重复读的特殊情况,如果两个事务都读取同一行,然后两个都进行写操作,并提交,第一个事务所做的改变就会丢失。

image

在T1时刻开启了事务1,T2时刻开启了事务2,

T3时刻事务1更新数据age=25,

T5时刻事务2更新数据age=30,

T6时刻提交事务,

T7时刻事务2提交事务,把事务1的更新覆盖了。

这种情况就是”第二类丢失更新(second lost updates)”。

并发问题总结

不可重复读的重点是修改 :

同样的条件 , 你读取过的数据 , 再次读取出来发现值不一样了

幻读的重点在于新增或者删除

同样的条件 , 第 1 次和第 2 次读出来的记录数不一样

第一类更新丢失(回滚丢失)

第二类更新丢失(覆盖丢失)

隔离级别

解决并发问题的途径是什么?答案是:采取有效的隔离机制。怎样实现事务的隔离呢?隔离机制的实现必须使用锁

一般在编程的时候只需要设置隔离等级

数据库系统提供四种事务隔离级别:

  1. 未提交读(READ UNCOMMITTED )

最低隔离级别,一个事务能读取到别的事务未提交的更新数据,很不安全,可能出现丢失更新、脏读、不可重复读、幻读;

  1. 提交读(READ COMMITTED)

一个事务能读取到别的事务提交的更新数据,不能看到未提交的更新数据,不会出现丢失更新、脏读,但可能出现不可重复读、幻读;

  1. 可重复读(REPEATABLE READ)

保证同一事务中先后执行的多次查询将返回同一结果,不受其他事务影响,不可能出现丢失更新、脏读、不可重复读,但可能出现幻读;

  1. 序列化(SERIALIZABLE)

最高隔离级别,不允许事务并发执行,而必须串行化执行,最安全,不可能出现更新、脏读、不可重复读、幻读,但是效率最低。

image

隔离级别越高,数据库事务并发执行性能越差,能处理的操作越少。
所以一般地,推荐使用REPEATABLE READ级别保证数据的读一致性。
对于幻读的问题,可以通过加锁来防止

image

MySQL支持这四种事务等级,默认事务隔离级别是REPEATABLE READ。

Oracle数据库支持READ COMMITTED 和 SERIALIZABLE这两种事务隔离级别,
所以Oracle数据库不支持脏读

Oracle数据库默认的事务隔离级别是READ COMMITTED

不可重复读和幻读的区别是,不可重复读对应的表的操作是更改(UPDATE),而幻读对应的表的操作是插入(INSERT),两种的应对策略不一样。对于不可重复读,只需要采用行级锁防止该记录被更新即可,而对于幻读必须加个表级锁,防止在表中插入数据

锁

乐观锁(Optimistic Lock)和悲观锁(Pessimistic Lock)

最重要的分类就是乐观锁(Optimistic Lock)和悲观锁(Pessimistic Lock),这实际上是两种锁策略

乐观锁,顾名思义就是非常乐观,非常相信真善美,每次去读数据都认为其它事务没有在写数据,所以就不上锁,快乐的读取数据,而只在提交数据的时候判断其它事务是否搞过这个数据了,如果搞过就rollback。乐观锁相当于一种检测冲突的手段,可通过为记录添加版本或添加时间戳来实现。

悲观锁,对其它事务抱有保守的态度,每次去读数据都认为其它事务想要作祟,所以每次读数据的时候都会上锁,直到取出数据。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性,但随之而来的是各种开销。悲观锁相当于一种避免冲突的手段。

选择标准:如果并发量不大,或数据冲突的后果不严重,则可以使用乐观锁;而如果并发量大或数据冲突后果比较严重(对用户不友好),那么就使用悲观锁。

分共享锁(S锁,Shared Lock)和排他锁(X锁,Exclusive Lock)

从读写角度,分共享锁(S锁,Shared Lock)和排他锁(X锁,Exclusive Lock),也叫读锁(Read Lock)和写锁(Write Lock)。
理解:

持有S锁的事务只读不可写。

如果事务A对数据D加上S锁后,其它事务只能对D加上S锁而不能加X锁。

持有X锁的事务可读可写。

如果事务A对数据D加上X锁后,其它事务不能再对D加锁,直到A对D的锁解除。

表级锁(Table Lock)和行级锁(Row Lock)

从锁的粒度角度,主要分为表级锁(Table Lock)和行级锁(Row Lock)。

表级锁将整个表加锁,性能开销最小

用户可以同时进行读操作。当一个用户对表进行写操作时,用户可以获得一个写锁,写锁禁止其他的用户读写操作。写锁比读锁的优先级更高,即使有读操作已排在队列中,一个被申请的写锁仍可以排在所队列的前列。

行级锁仅对指定的记录进行加锁

这样其它进程可以对同一个表中的其它记录进行读写操作。行级锁粒度最小,开销大,能够支持高并发,可能会出现死锁。

MySQL的MyISAM引擎使用表级锁,而InnoDB支持表级锁和行级锁,默认是行级锁。
还有BDB引擎使用页级锁,即一次锁定一组记录,并发性介于行级锁和表级锁之间。

三级锁协议

三级加锁协议是为了保证正确的事务并发操作,事务在读、写数据库对象是需要遵循的加锁规则。

一级封锁协议:事务T在修改数据R之前必须对它加X锁,直到事务结束方可释放。而若事务T只是读数据,不进行修改,则不需加锁,因此一级加锁协议下可能会出现脏读和不可重复读。

二级加锁协议:在一级加锁协议的基础上,加上这样一条规则——事务T在读取数据R之前必须对它加S锁,直到读取完毕以后释放。二级加锁协议下可能会出现不可重复读。

三级加锁协议:在一级加锁协议的基础上,加上这样一条规则——事务T在读取数据R之前必须对它加S锁,直到事务结束方可释放。三级加锁协议避免了脏读和不可重复读的问题

Spring事务

Spring事务管理的实现有许多细节,如果对整个接口框架有个大体了解会非常有利于我们理解事务

image

image

Spring事务管理器

Spring事务管理涉及的接口的联系如下:
image

Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。

Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,
通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,

但是具体的实现就是各个平台自己的事情了

1
2
3
4
5
6
7
8
9
10
11
12
/**
* This is the central interface in Spring's transaction infrastructure.
* Applications can use this directly, but it is not primarily meant as API:
* Typically, applications will work with either TransactionTemplate or
* declarative transaction demarcation through AOP.
*/
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}

标准的jdbc处理事务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
Connection conn = DataSourceUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
try {
Object retVal = callback.doInConnection(conn);
conn.commit(); //提交事务
return retVal;
}catch (Exception e) {
conn.rollback();//回滚事务
throw e;
}finally {
conn.close();
}

spring对应的TranstactionTemplate处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TransactionTemplate extends DefaultTransactionDefinition
implements TransactionOperations, InitializingBean {

@Override
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Error err) {
// Transactional code threw error -> rollback
rollbackOnException(status, err);
throw err;
}
catch (Exception ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}

}

具体的事务管理机制对Spring来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以Spring事务管理的一个优点就是为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。

image

JDBC事务

如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会为你处理事务边界。为了使用DataSourceTransactionManager,你需要使用如下的XML将其装配到应用程序的上下文定义中:

1
2
3
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

实际上,DataSourceTransactionManager是通过调用java.sql.Connection来管理事务,而后者是通过DataSource获取到的。通过调用连接的commit()方法来提交事务,同样,事务失败则通过调用rollback()方法进行回滚。

Hibernate事务

如果应用程序的持久化是通过Hibernate实习的,那么你需要使用HibernateTransactionManager。对于Hibernate3,需要在Spring上下文定义中添加如下的声明:

1
2
3
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

sessionFactory属性需要装配一个Hibernate的session工厂,HibernateTransactionManager的实现细节是它将事务管理的职责委托给org.hibernate.Transaction对象,而后者是从Hibernate Session中获取到的。当事务成功完成时,HibernateTransactionManager将会调用Transaction对象的commit()方法,反之,将会调用rollback()方法。

事务属性

事务管理器接口PlatformTransactionManager通过getTransaction(TransactionDefinition definition)方法来得到事务,这个方法里面的参数是TransactionDefinition类,这个类就定义了一些基本的事务属性。

1
2
3
4
5
6
public interface TransactionDefinition {
int getPropagationBehavior(); // 返回事务的传播行为
int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
int getTimeout(); // 返回事务必须在多少秒内完成
boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
}

那么什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面

image

传播行为

事务的第一个方面是传播行为(propagation behavior)。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。

为什么需要定义传播?

在我们用SSH开发项目的时候,我们一般都是将事务设置在Service层 那么当我们调用Service层的一个方法的时候它能够保证我们的这个方法中执行的所有的对数据库的更新操作保持在一个事务中,在事务层里面调用的这些方法要么全部成功,要么全部失败。那么事务的传播特性也是从这里说起的。
如果你在你的Service层的这个方法中,除了调用了Dao层的方法之外,还调用了本类的其他的Service方法,那么在调用其他的Service方法的时候,这个事务是怎么规定的呢,我必须保证我在我方法里掉用的这个方法与我本身的方法处在同一个事务中,否则如果保证事物的一致性。事务的传播特性就是解决这个问题的,“事务是会传播的”在Spring中有针对传播特性的多种配置我们大多数情况下只用其中的一种:PROPGATION_REQUIRED:这个配置项的意思是说当我调用service层的方法的时候开启一个事务(具体调用那一层的方法开始创建事务,要看你的aop的配置),那么在调用这个service层里面的其他的方法的时候,如果当前方法产生了事务就用当前方法产生的事务,否则就创建一个新的事务。这个工作使由Spring来帮助我们完成的。
以前没有Spring帮助我们完成事务的时候我们必须自己手动的控制事务,例如当我们项目中仅仅使用hibernate,而没有集成进spring的时候,我们在一个service层中调用其他的业务逻辑方法,为了保证事物必须也要把当前的hibernate session传递到下一个方法中,或者采用ThreadLocal的方法,将session传递给下一个方法,其实都是一个目的。现在这个工作由spring来帮助我们完成,就可以让我们更加的专注于我们的业务逻辑。而不用去关心事务的问题。

Spring定义了七种传播行为:

PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY

使用当前的事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER

以非事务方式执行,如果当前存在事务,则抛出异常。

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

传播行为详细

通过实例尝试一下各个传播属性

1
2
3
4
5
6
7
8
9
10
11
12
ServiceA {

void methodA() {
ServiceB.methodB();
}
}

ServiceB {

void methodB() {
}
}
PROPAGATION_REQUIRED

如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务

1
2
3
4
5
6
7
8
9
10
//事务属性 PROPAGATION_REQUIRED
methodA{
……
methodB();
……
}
//事务属性 PROPAGATION_REQUIRED
methodB{
……
}

单独调用methodB方法:

1
2
3
main{ 
metodB();
}

相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Main{ 
Connection con=null;
try{
con = getConnection();
con.setAutoCommit(false);

//方法调用
methodB();

//提交事务
con.commit();
} Catch(RuntimeException ex) {
//回滚事务
con.rollback();
} finally {
//释放资源
closeCon();
}
}

Spring保证在methodB方法中所有的调用都获得到一个相同的连接。在调用methodB时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。

只是在ServiceB.methodB内的任何地方出现异常,ServiceB.methodB将会被回滚,不会引起ServiceA.methodA的回滚

单独调用MethodA时,在MethodA内又会调用MethodB.

执行效果相当于:

1
2
3
4
5
6
7
8
9
10
11
12
main{ 
Connection con = null;
try{
con = getConnection();
methodA();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
closeCon();
}
}

调用MethodA时,环境中没有事务,所以开启一个新的事务.

当在MethodA中调用MethodB时,环境中已经有了一个事务,所以methodB就加入当前事务

ServiceA.methodA或者ServiceB.methodB无论哪个发生异常methodA和methodB作为一个整体都将一起回滚

PROPAGATION_SUPPORTS

如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同

1
2
3
4
5
6
7
8
9
//事务属性 PROPAGATION_REQUIRED
methodA(){
methodB();
}

//事务属性 PROPAGATION_SUPPORTS
methodB(){
……
}

单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行

PROPAGATION_MANDATORY

如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常

1
2
3
4
5
6
7
8
9
//事务属性 PROPAGATION_REQUIRED
methodA(){
methodB();
}

//事务属性 PROPAGATION_MANDATORY
methodB(){
……
}

当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);

当调用methodA时,methodB则加入到methodA的事务中,事务地执行

PROPAGATION_REQUIRES_NEW

总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。

1
2
3
4
5
6
7
8
9
10
11
//事务属性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}

//事务属性 PROPAGATION_REQUIRES_NEW
methodB(){
……
}

调用A方法:

1
2
3
main(){
methodA();
}

相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}
//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}

在这里,我把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。

Ts2是否成功并不依赖于ts1

如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。

而除了 methodB之外的其它代码导致的结果却被回滚了

PROPAGATION_NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起,

1
2
3
4
5
6
7
8
9
10
11
//事务属性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}

//事务属性 PROPAGATION_NOT_SUPPORTED
methodB(){
……
}

当前不支持事务。比如ServiceA.methodA的事务级别是PROPAGATION_REQUIRED ,
而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,
那么当执行到ServiceB.methodB时,ServiceA.methodA的事务挂起,而他以非事务的状态运行完,再继续ServiceA.methodA的事务

PROPAGATION_NEVER

不能在事务中运行。假设ServiceA.methodA的事务级别是PROPAGATION_REQUIRED,
而ServiceB.methodB的事务级别是PROPAGATION_NEVER ,
那么ServiceB.methodB就要抛出异常了。

PROPAGATION_NESTED

开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.

比如我们设计ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_NESTED,那么当执行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的子事务并设置savepoint,等待ServiceB.methodB的事务完成以后,他才继续执行

因为ServiceB.methodB是外部事务的子事务,那么

  1. 如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB也将回滚。
  2. 如果ServiceB.methodB失败回滚,如果他抛出的异常被ServiceA.methodA的try..catch捕获并处理,ServiceA.methodA事务仍然可能提交;如果他抛出的异常未被ServiceA.methodA捕获处理,ServiceA.methodA事务将回滚。

理解Nested的关键是savepoint。

与PROPAGATION_REQUIRES_NEW的区别:

  1. RequiresNew每次都创建新的独立的物理事务,而Nested只有一个物理事务;
  2. Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而 RequiresNew由于都是全新的事务,所以之间是无关联的;
  3. Nested使用JDBC 3的保存点实现,即如果使用低版本驱动将导致不支持嵌套事务。
    使用嵌套事务,必须确保具体事务管理器实现的nestedTransactionAllowed属性为true,否则不支持嵌套事务,如DataSourceTransactionManager默认支持,而HibernateTransactionManager默认不支持,需要我们来开启。

在 spring 中使用 PROPAGATION_NESTED的前提:

  1. 我们要设置 transactionManager 的 nestedTransactionAllowed 属性为 true, 注意, 此属性默认为 false!!!
  2. java.sql.Savepoint 必须存在, 即 jdk 版本要 1.4+
  3. Connection.getMetaData().supportsSavepoints() 必须为 true, 即 jdbc drive 必须支持 JDBC 3.0

image

隔离规则

用来解决并发事务时出现的问题,其使用TransactionDefinition中的静态变量来指定

  1. ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
  2. ISOLATION_READ_UNCOMMITTED 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  3. ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  4. ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
  5. ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

可以使用DefaultTransactionDefinition类的setIsolationLevel(TransactionDefinition. ISOLATION_READ_COMMITTED)来指定隔离级别,其中此处表示隔离级别为提交读

也可以使用或setIsolationLevelName(“ISOLATION_READ_COMMITTED”)方式指定,其中参数就是隔离级别静态变量的名字,但不推荐这种方式

事务只读

将事务标识为只读,只读事务不修改任何数据;

对于JDBC只是简单的将连接设置为只读模式,对于更新将抛出异常;

对于一些其他ORM框架有一些优化作用,如在Hibernate中,Spring事务管理器将执行“session.setFlushMode(FlushMode.MANUAL)”
即指定Hibernate会话在只读事务模式下不用尝试检测和同步持久对象的状态的更新。

如果使用设置具体事务管理的validateExistingTransaction属性为true(默认false),将确保整个事务传播链都是只读或都不是只读
image

第二个addressService.save()不能设置成false

对于错误的事务只读设置将抛出IllegalTransactionStateException异常,并伴随“Participating transaction with definition [……] is not marked as read-only……”信息,表示参与的事务只读属性设置错误

事务超时

设置事务的超时时间,单位为秒,默认为-1表示使用底层事务的超时时间

使用如setTimeout(100)来设置超时时间,如果事务超时将抛出org.springframework.transaction.TransactionTimedOutException异常并将当前事务标记为应该回滚,即超时后事务被自动回滚

可以使用具体事务管理器实现的defaultTimeout属性设置默认的事务超时时间,如DataSourceTransactionManager. setDefaultTimeout(10)

回滚规则

spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务

默认配置下,Spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务

如何改变默认规则:

  1. 让checked例外也回滚:在整个方法前加上 @Transactional(rollbackFor=Exception.class)
  2. 让unchecked例外不回滚: @Transactional(notRollbackFor=RunTimeException.class)
  3. 不需要事务管理的(只查询的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)

事务状态

上面讲到的调用PlatformTransactionManager接口的getTransaction()的方法得到的是TransactionStatus接口的一个实现,这个接口的内容如下:

1
2
3
4
5
6
7
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事物
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}

可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态

编程式和声明式事务

Spring提供了对编程式事务和声明式事务的支持,编程式事务允许用户在代码中精确定义事务的边界

而声明式事务(基于AOP)有助于用户将操作与事务规则进行解耦。

简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。

编程式

Spring提供两种方式的编程式事务管理,分别是:使用TransactionTemplate和直接使用PlatformTransactionManager

使用TransactionTemplate

采用TransactionTemplate和采用其他Spring模板,如JdbcTempalte和HibernateTemplate是一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate是线程安全的。代码片段:

1
2
3
4
5
6
7
8
TransactionTemplate tt = new TransactionTemplate(); // 新建一个TransactionTemplate
Object result = tt.execute(
new TransactionCallback(){
public Object doTransaction(TransactionStatus status){
updateOperation();
return resultOfUpdateOperation();
}
}); // 执行execute方法进行事务管理

使用TransactionCallback()可以返回一个值。如果使用TransactionCallbackWithoutResult则没有返回值

使用PlatformTransactionManager

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
//定义一个某个框架平台的TransactionManager,如JDBC、Hibernate
dataSourceTransactionManager.setDataSource(this.getJdbcTemplate().getDataSource()); // 设置数据源
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition(); // 定义事务属性
transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为属性
TransactionStatus status = dataSourceTransactionManager.getTransaction(transDef); // 获得事务状态
try {
// 数据库操作
dataSourceTransactionManager.commit(status);// 提交
} catch (Exception e) {
dataSourceTransactionManager.rollback(status);// 回滚
}

声明式

有几种实现方式,不一一罗列了

使用tx拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 定义事务管理器(声明式的事务) --> 
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="interceptorPointCuts"
expression="execution(* com.bluesky.spring.dao.*.*(..))" />
<aop:advisor advice-ref="txAdvice"
pointcut-ref="interceptorPointCuts" />
</aop:config>

全注解

1
2
3
4
5
6
7
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager ">
<property name="dataSource">
<ref bean="basicDataSource" />
</property>
</bean>

Spring源码片段

在《BeanPostProcessor学习》中提到了AOP的实现方式,声明式事务实现是基于AOP

首先得解析xml配置,TxNamespaceHandler

1
2
3
4
5
6
@Override
public void init() {
registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser());
}

主要是TransactionInterceptor类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object invoke(final MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// Adapt to TransactionAspectSupport's invokeWithinTransaction...
//主要逻辑在父类
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
@Override
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
});
}

核心逻辑,还得看父类TransactionAspectSupport#invokeWithinTransaction

逻辑主干很清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
// 判断创建Transaction
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
//执行业务逻辑
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
// 出现异常,回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
//清除当前事务状态
cleanupTransactionInfo(txInfo);
}
//提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}

创建事务createTransactionIfNecessary

主要逻辑在PlatformTransactionManager#getTransaction()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
//得到各个不同数据源的事务对象,spring尽然没有把transaction对象抽象出来,很是奇怪
Object transaction = doGetTransaction();

// Cache debug flag to avoid repeated checks.
boolean debugEnabled = logger.isDebugEnabled();

if (definition == null) {
// Use defaults if no transaction definition given.
definition = new DefaultTransactionDefinition();
}

//此事务是否已经存在
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(definition, transaction, debugEnabled);
}

// Check definition settings for new transaction.
if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
}

// No existing transaction found -> check propagation behavior to find out how to proceed.
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
//这三种都是新建事务
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
}
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
//开始获取链接,开启事务,绑定资源到当前线程
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException | Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + definition);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
}
}

TransactionStatus

这儿返回的是TransactionStatus

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface TransactionStatus extends SavepointManager, Flushable {

boolean isNewTransaction();

boolean hasSavepoint();

void setRollbackOnly();

boolean isRollbackOnly();

void flush();

boolean isCompleted();

TransactionInfo

事务信息

1
2
3
4
5
6
7
8
9
10
11
12
protected final class TransactionInfo {

private final PlatformTransactionManager transactionManager;

private final TransactionAttribute transactionAttribute;

private final String joinpointIdentification;

private TransactionStatus transactionStatus;

private TransactionInfo oldTransactionInfo;
}

commitTransactionAfterReturning提交事务

逻辑到了AbstractPlatformTransactionManager#processRollback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;

try {
triggerBeforeCompletion(status);
//有savepoint,
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
//回滚事务
doRollback(status);
}
else {
// Participating in larger transaction
//在一个事务中,就先设置回滚标识,等父事务一起回滚
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status);
}
else {
if (status.isDebug()) {
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
}
}
}
else {
logger.debug("Should roll back transaction but cannot - no transaction available");
}
// Unexpected rollback only matters here if we're asked to fail early
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
}
catch (RuntimeException | Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}

triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}

completeTransactionAfterThrowing回滚事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
//有事务才能回滚
if (txInfo != null && txInfo.hasTransaction()) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
//回滚在 (ex instanceof RuntimeException || ex instanceof Error)
if (txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
catch (Error err) {
logger.error("Application exception overridden by rollback error", ex);
throw err;
}
}
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
catch (Error err) {
logger.error("Application exception overridden by commit error", ex);
throw err;
}
}
}
}

总结

一个完整的事务介绍结束了。框架就是封装一切,透明一切,简化一切。本质的流程不会变

参考资料

http://blog.sina.com.cn/s/blog_7ed8b1e90101mgas.html

https://my.oschina.net/wanyuxiang000/blog/277568

http://www.mamicode.com/info-detail-1248286.html

http://jinnianshilongnian.iteye.com/blog/1496953

http://www.cnblogs.com/yldIndex/p/spring_Transactional.html

http://jinnianshilongnian.iteye.com/blog/1441271

http://www.cnblogs.com/chihirotan/p/6739748.html

计数器算法

发表于 2017-05-29
字数统计: 3.3k 字数 | 阅读时长 ≈ 12 分钟

《微服务-熔断机制》中提到了计数器,这篇详细学习一下计数器算法

之前的有次面试,碰到了计数器的的题目

Q:线上服务,设计一个拦截器,一个IP如果短时间内请求次数过多,就屏蔽

A:使用map,key为ip,值为次数与时间

Q:请求相当大,会直接冲垮内存,怎么办?

A:使用redis,像redis cluster,绝对可以满足

Q: 写下伪代码

A:bbbbbbb

其实计数器在互联网开发中很常见,当时刚转互联网比较无知,面试得很烂。

计数器法

计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter,具体算法的示意图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Counter {
public long timeStamp = getNowTime();
public int reqCount = 0;
public final int limit = 100; // 时间窗口内最大请求数
public final long interval = 1000; // 时间窗口ms
public boolean grant() {
long now = getNowTime();
if (now < timeStamp + interval) {
// 在时间窗口内
reqCount++;
// 判断当前时间窗口内是否超过最大请求控制数
return reqCount <= limit;
}
else {
timeStamp = now;
// 超时后重置
reqCount = 1;
return true;
}
}
}

以上是示意代码,先忽视其中的并发问题,最大的问题是临界问题

从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。

滑动窗口

滑动窗口,又称rolling window

在上图中,整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口 划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器counter,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。

那么滑动窗口怎么解决刚才的临界问题的呢?我们可以看上图,0:59到达的100个请求会落在灰色的格子中,而1:00到达的请求会落在橘黄色的格 子中。当时间到达1:00时,我们的窗口会往右移动一格,那么此时时间窗口内的总请求数量一共是200个,超过了限定的100个,所以此时能够检测出来触 发了限流。

我再来回顾一下刚才的计数器算法,我们可以发现,计数器算法其实就是滑动窗口算法。只是它没有对时间窗口做进一步地划分,所以只有1格。

由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

实现一

根据上面滑动窗口的定义,实现很简单了

  1. 需要一个map,当然是并发安全的,key为时间
  2. 统计窗口内的请求总数

这儿有个以这种方式实现的
https://github.com/zhuxingsheng/yammer-metrics/blob/master/metrics-core/src/main/java/com/codahale/metrics/SlidingTimeWindowReservoir.java

摘点核心的片段:

1
2
//一个并发安全map,skip list有序
this.measurements = new ConcurrentSkipListMap<Long, Long>();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 获取map的key,以时间纳秒值为key
* @return
*/
private long getTick() {
for (; ; ) {
final long oldTick = lastTick.get();
//每纳秒处理的请求很多,减少compareAndSet的失败次数,这儿*COLLISION_BUFFER
final long tick = clock.getTick() * COLLISION_BUFFER;
// ensure the tick is strictly incrementing even if there are duplicate ticks
final long newTick = tick > oldTick ? tick : oldTick + 1;
if (lastTick.compareAndSet(oldTick, newTick)) {
return newTick;
}
}
}
1
2
3
4
private void trim() {
//清除window之前的计数
measurements.headMap(getTick() - window).clear();
}
缺点

实现简单,但有个问题,map的key是在不停的增加,删除,给GC带来了压力

实现二

考虑key的复用,使用环形结构

环形结构

通过取模来达到这个效果

初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int window; //计算窗口
//整个循环数组窗口
private int ringWindow=window+30;


requestCounter = new AtomicInteger[ringWindow];
failedCounter = new AtomicInteger[ringWindow];
for (int i = 0; i < ringWindow; i++) {
requestCounter[i] = new AtomicInteger(0);
failedCounter[i] = new AtomicInteger(0);
}

initCounterTimeInSecond =
TimeUnit.NANOSECONDS.toSeconds(System.nanoTime());

计算窗口内的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private long countTotal(AtomicInteger[] caculateCounter){
int currentIndex = getIndex();

long sum = 0;

for (int i = 0; i < window; i++) {
//这儿并不是直接计算window中的所有counter,
//而是从currentIndex向前倒取window个
int index = ((currentIndex + ringWindow) -i)
% this.ringWindow;
sum += caculateCounter[index].get();
}
return sum;
}

为什么需要ringWindow,直接window就可以?这儿有个奇技淫巧。

1
2
3
4
5
6
7
8
9
10
11
CLEAN_UP_BUFFER=10;

public void cleanupFutureCounter() {
int currentIndex = getIndex();

for (int i = 1 ; i <= CLEAN_UP_BUFFER; i++) {
int index = (currentIndex + i) % this.ringWindow;
requestCounter[index].set(0);
failedCounter[index].set(0);
}
}

这儿会有个定时任务,每5秒会去清空未来10秒的数据

因为在一环数组全部填充完成后,下一轮开始时,需要清空,哪个地方是起点,无法区分,所以ringwindow预留点位置用来清空

实现三

还有一些是加锁,当然会是轻量的CAS;每一个轮回完成后,都需要标记开始位置,并清空环。

漏桶算法

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:

我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,我们可以保证接口会以一个常速速率来处理请求。所以漏桶算法天生不会出现临界问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LeakyDemo {
public long timeStamp = getNowTime();
public int capacity; // 桶的容量
public int rate; // 水漏出的速度
public int water; // 当前水量(当前累积请求数)
public boolean grant() {
long now = getNowTime();
water = max(0, water - (now - timeStamp) * rate); // 先执行漏水,计算剩余水量
timeStamp = now;
if ((water + 1) < capacity) {
// 尝试加水,并且水还未满
water += 1;
return true;
}
else {
// 水满,拒绝加水
return false;
}
}
}

令牌桶算法(Token Bucket)

令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务

令牌桶算法

令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TokenBucketDemo {
public long timeStamp = getNowTime();
public int capacity; // 桶的容量
public int rate; // 令牌放入速度
public int tokens; // 当前令牌数量
public boolean grant() {
long now = getNowTime();
// 先添加令牌
tokens = min(capacity, tokens + (now - timeStamp) * rate);
timeStamp = now;
if (tokens < 1) {
// 若不到1个令牌,则拒绝
return false;
}
else {
// 还有令牌,领取令牌
tokens -= 1;
return true;
}
}
}

临界问题

我 们再来考虑一下临界问题的场景。在0:59秒的时候,由于桶内积满了100个token,所以这100个请求可以瞬间通过。但是由于token是以较低的 速率填充的,所以在1:00的时候,桶内的token数量不可能达到100个,那么此时不可能再有100个请求通过。所以令牌桶算法可以很好地解决临界问 题。

下图比较了计数器(左)和令牌桶算法(右)在临界点的速率变化。我们可以看到虽然令牌桶算法允许突发速率,但是下一个突发速率必须要等桶内有足够的 token后才能发生:

Guava RateLimiter

在guava中,有现成的实现

RateLimiter使用的是一种叫令牌桶的流控算法,RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌。

有两种方式,SmoothBursty和SmoothWarmingUp

1
2
3
4
5
6
7
8
9
create(double permitsPerSecond)
根据指定的稳定吞吐率创建RateLimiter
这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少查询)

create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
根据指定的稳定吞吐率和预热期来创建RateLimiter
这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少个请求量),
在这段预热时间内,
RateLimiter每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率。(只要存在足够请求数来使其饱和)

SmoothBursty通过平均速率和最后一次新增令牌的时间计算出下次新增令牌的时间的,另外需要一个桶暂存一段时间内没有使用的令牌(即可以突发的令牌数)。另外RateLimiter还提供了tryAcquire方法来进行无阻塞或可超时的令牌消费。

因为SmoothBursty允许一定程度的突发,会有人担心如果允许这种突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。因此需要SmoothWarmingUp一种平滑速率的限流工具,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于我们设置的固定速率)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void testSmoothWarmingUp(){
//每秒5个,1500ms后达到正常速率
RateLimiter rateLimiter = RateLimiter.create(5,1500, TimeUnit.MILLISECONDS);

List<Runnable> tasks = new ArrayList<Runnable>();
for (int i = 0; i < 10; i++) {
tasks.add(new Request(i));
}

ExecutorService executorService = Executors.newCachedThreadPool();
for(Runnable task:tasks) {
System.out.println("等待时间:" + rateLimiter.acquire());
executorService.execute(task);
}
executorService.shutdown();

}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
等待时间:0.0
0 handle request 1528693920502
等待时间:0.54311
1 handle request 1528693921052
等待时间:0.433531
2 handle request 1528693921486
等待时间:0.332679
3 handle request 1528693921819
等待时间:0.229785
4 handle request 1528693922049
等待时间:0.199668
5 handle request 1528693922249
等待时间:0.199845
6 handle request 1528693922449
等待时间:0.199757
7 handle request 1528693922649
等待时间:0.19981
8 handle request 1528693922849
等待时间:0.199732
9 handle request 1528693923049

可以看出前面几个等待时间长,速度慢;1500ms后,速率达到正常的每秒5的速度

其实这算是一种漏桶算法的变异,在令牌桶中控制一个移除令牌的速度就是个漏桶了。

总结

计数器 VS 滑动窗口

计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。

漏桶算法 VS 令牌桶算法

漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允许100个请求通过。

令牌桶算法由于实现简单,且允许某些流量的突发,对用户友好,所以被业界采用地较多。当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法

参考

接口限流算法总结

Hashmap源码解析

发表于 2017-04-06 | 分类于 java
字数统计: 4.6k 字数 | 阅读时长 ≈ 19 分钟

前言

做什么都怕进入狗咬尾巴的怪圈,上次看hashmap源码还是2012年,这次出去面试时被问到了hashmap的问题,整体思路还是记得的,巴拉巴拉一堆。回来再看一下源码,温习一下

想要了解hashmap,就得先知道一下他的数据结构理论

哈希数据结构

哈希表(Hash table,也叫散列表),是根据key而直接进行访问的数据结构。也就是说,它通过把key映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

哈希表的做法其实很简单,就是把key通过一个固定的算法函数即所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。

哈希函数

哈希查找第一步就是使用哈希函数将键映射成索引。这种映射函数就是哈希函数。如果我们有一个保存0-M数组,那么我们就需要一个能够将任意键转换为该数组范围内的索引(0~M-1)的哈希函数

R.W.Floyed给出的衡量散列思想的三个标准:

  1. 一个好的hash算法的计算应该是非常快的
  2. 一个好的hash算法应该是冲突极小化, 如果存在冲突,应该是冲突均匀化。

哈希冲突

通过哈希函数,我们可以将键转换为数组的索引(0-M-1),但是对于两个或者多个键具有相同索引值的情况,我们需要有一种方法来处理这种冲突。

拉链法

一种比较直接的办法就是,将大小为M 的数组的每一个元素指向一个条链表,链表中的每一个节点都存储散列值为该索引的键值对,这就是拉链法

image

该方法的基本思想就是选择足够大的M,使得所有的链表都尽可能的短小,以保证查找的效率。对采用拉链法的哈希实现的查找分为两步,首先是根据散列值找到等一应的链表,然后沿着链表顺序找到相应的键。

实现基于拉链表的散列表,目标是选择适当的数组大小M,使得既不会因为空链表而浪费内存空间,也不会因为链表太而在查找上浪费太多时间。拉链表的优点在于,这种数组大小M的选择不是关键性的,如果存入的键多于预期,那么查找的时间只会比选择更大的数组稍长,另外,我们也可以使用更高效的结构来代替链表存储。如果存入的键少于预期,索然有些浪费空间,但是查找速度就会很快。所以当内存不紧张时,我们可以选择足够大的M,可以使得查找时间变为常数,如果内存紧张时,选择尽量大的M仍能够将性能提高M倍。

线性探测法

线性探测法是开放寻址法解决哈希冲突的一种方法,基本原理为,使用大小为M的数组来保存N个键值对,其中M>N,我们需要使用数组中的空位解决碰撞冲突。如下图所示:

image

对照前面的拉链法,在该图中,”Ted Baker” 是有唯一的哈希值153的,但是由于153被”Sandra Dee”占用了。而原先”Snadra Dee”和”John Smith”的哈希值都是152的,但是在对”Sandra Dee”进行哈希的时候发现152已经被占用了,所以往下找发现153没有被占用,所以存放在153上,然后”Ted Baker”哈希到153上,发现已经被占用了,所以往下找,发现154没有被占用,所以值存到了154上。

开放寻址法中最简单的是线性探测法:当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中的下一个位置即将索引值加1,这样的线性探测会出现三种结果:

  1. 命中,该位置的键和被查找的键相同
  2. 未命中,键为空
  3. 继续查找,该位置和键被查找的键不同。

线性探查(Linear Probing)方式虽然简单,但是有一些问题,它会导致同类哈希的聚集。在存入的时候存在冲突,在查找的时候冲突依然存在。

hashmap 源码分析

hashmap原理角度看就是基于哈希数据结构的一种实现,解决冲突的办法是使用拉链法

从源码基于JDK1.7看

1
2
3
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

HashMap是基于哈希表的Map接口的非同步实现,它提供所有可选的映射操作,并允许使用null键和null值。
此集合不保证映射的顺序,特别不保证其顺序永久不变。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);

this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}

两个重要的初始化参数,[初始容量,负载因子]

这个JDK7大版本里面的小版本对hashmap改动好好几次。

这个源码是基于JDK7.0_80的。有些版本在构造函数里面就确保容量是2的N次方,但这个版本没有,很简单的赋值

初始容量16,负载因子0.75

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

2的N次方

很多算法书上都认为想要降低key冲突,最好容量为素数。为什么java却取了个2的N次方呢。

为了将各元素的hashCode保存至长度为Length的key数组中,一般采用取模的方式,即index = hashCode % Length。不可避免的,存在多个不同对象的hashCode被安排在同一位置,这就是我们平时所谓的“冲突”。
如果仅仅是考虑元素均匀化与冲突极小化,似乎应该将Length取为素数(尽管没有明显的理论来支持这一点,但数学家们通过大量的实践得出结论,对素数取模的产生结果的无关性要大于其它数字)。为此,Craig Larman and Rhett Guthrie《Java Performence》中对此也大加抨击。

为了弄清楚这个问题,Bruce Eckel(Thinking in JAVA的作者)专程采访了java.util.hashMap的作者Joshua Bloch,此设计的原因
取模运算在包括Java在内的大多数语言中的效率都十分低下,而当除数为2的N次方时,取模运算将退化为最简单的位运算,其效率明显提升(按照Bruce Eckel给出的数据,大约可以提升5~8倍)

1
2
3
4
5
6
7
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}

看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

image

put操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

当table == EMPTY_TABLE时,先填充table

1
2
3
4
5
6
7
8
9
10
11
/**
* Inflates the table.
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);

threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}

这儿看到了2的N次方,没有跟之前一样使用while循环

1
2
3
int capacity = 1;  
while (capacity < initialCapacity)
capacity <<= 1;

threshold的值=容量*负载因子

看下roundUpToPowerOf2()

1
2
3
4
5
6
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

主要是Integer.highestOneBit

1
2
3
4
5
6
7
8
9
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}

这个方法单纯的使用位运算,性能自然提高
解析下这个方法,这个方法的作用是求构成一个整数的最大的位所代表的整数的值
接下来举个简单的例子,128来讲二进制是1000 0000。下面以他为例子算下:
移1位
1000 0000
0100 0000
|————-
移2位
1100 0000
0011 0000
|————
移4位
1111 0000
0000 1111
|————
移8位
1111 1111
0000 0000
|————
移动16位
1111 1111
0000 0000
|————
1111 1111
最终的结果如你所看到的,后面的位全部填充为1,把后面的位全部减掉就得到了最高的位代表的整数。

回到put方法,如果key==null,putForNullKey(value);说明hashmap支持key为null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Offloaded version of put for null keys
*/
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}

这个逻辑比较简单

  1. 第一步,如果找到之前有key为null的key,进行一下value替换,返回oldValue
  2. 如果没有为null的key,那进行addEntry() 添加元素的主要方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * Adds a new entry with the specified key, value and hash code to
    * the specified bucket. It is the responsibility of this
    * method to resize the table if appropriate.
    *
    * Subclass overrides this to alter the behavior of put method.
    */
    void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
    resize(2 * table.length);
    hash = (null != key) ? hash(key) : 0;
    bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
    }

新添加元素时,先看下有没有达到threshold && bucketIndex元素不为null

如果成立,那就需要resize,这是个耗性能的操作,所以在初始化时,一般计算好元素多少,给一个合适的初始容量

按照上面的条件,一个合适的容量应该这样计算了

我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

先看createEntry

1
2
3
4
5
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}

这个很简单,新建一个Entry指向bucketIndex,老元素e,被指向新元素的next。

这儿也看出hashmap是使用的拉链法

看Entry,就是一个链表,一个元素接着另一个元素

1
2
3
4
5
6
7
8
9
10
11
12
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;

Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}

resize方法

耗性能的地方,重新散列,看看这个方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//在put方法里,当容量达到threshold时,进行双倍的扩容。
resize(2 * table.length);


void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}

Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

如果容量已经达到MAX_VALUE,那就不扩容了,只能处理冲突

transfer(),这是核心方法,把以前的元素,按新的capacity,计算的hashCode,重新放置元素。

这儿看到e.next=newTable[i],明显使用的是链表头部插入法

get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);

return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}

int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}

get很好理解了,跟算法一致,先hash找bucket,如果有冲突的元素,再使用equals来判定

hashSeed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* A randomizing value associated with this instance that is applied to
* hash code of keys to make hash collisions harder to find. If 0 then
* alternative hashing is disabled.
*/
transient int hashSeed = 0;

/**
* Initialize the hashing mask value. We defer initialization until we
* really need it.
*/
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}

initHashSeedAsNeeded在inflateTable中被调用过,这个hashSeed到底有什么用?

hashSeed就好像我们加密时使用的密钥,在StackOverfolw上是这样描述hashSeed的:

The seed parameter is a means for you to randomize the hash function. You should provide the same seed value for all calls to the hashing function in the same application of the hashing function. However, each invocation of your application (assuming it is creating a new hash table) can use a different seed, e.g., a random value.

Why is it provided?

One reason is that attackers may use the properties of a hash function to construct a denial of service attack. They could do this by providing strings to your hash function that all hash to the same value destroying the performance of your hash table. But if you use a different seed for each run of your program, the set of strings the attackers must use changes.

虽然seed近似随机,但在同一个HashMap中必须保证每次的计算Hash值的时候使用的同一个seed,也就相当于保证我们在一个密码系统中加密时,使用同一个密钥。同时使用seed可以抵御攻击,因为每个应用的seed都会一样。

对于安全方面的了解几乎是0,有机会再学习了。

ConcurrentModificationException

hashmap不是线程安全的

1
2
3
4
5
6
7
8
9
10
 private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current; // current entry


final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();

这个内部遍历类使用了Fail-Fast机制

1
2
3
4
5
6
7
8
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;

modCount为HashMap的一个实例变量,并且被声明为volatile,表示任何线程都可以看到该变量被其它线程修改的结果

(根据JVM内存模型的优化,每一个线程都会存一份自己的工作内存,此工作内存的内容与本地内存并非时时刻刻都同步,因此可能会出现线程间的修改不可见的问题)

使用Iterator开始迭代时,会将modCount的赋值给expectedModCount,在迭代过程中,通过每次比较两者是否相等来判断HashMap是否在内部或被其它线程修改

在并发条件下,还是要使用并发包下的ConcurrentHashmap,有时间也得写一写

总结

(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。

(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。

(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。

参考资料

浅谈算法和数据结构: 十一 哈希表

单链表—java实现

HashMap的存取之美

游戏小传序

发表于 2017-04-01
字数统计: 476 字数 | 阅读时长 ≈ 1 分钟

序言

在游戏行业足足待了八年了。从大学毕业就开始开发游戏。一直到现在还在一线岗位。

从小小的开发,到掌握后端全局的主程;从胎死腹中的产品,到月流水过亿的大作。

游戏人太苦,996工作制是正常的,凌晨下班也是常有的,为什么可以坚持,可能是心中都有个游戏梦,要大成,要大作。但每次都是充满期待,结果却无声奈何。想想那些从别的行业转来做游戏的同事,一个一个期望变成失落,坚持了几年,又都失望地离开了游戏行业,而我是幸运的,至少经历了大作,经过了一款月流水过亿的产品。

人常说七年就是一辈子,那我这算在工作经历上过完了一辈子,下辈子怎么过呢?不清楚。但至少不能再重复上辈子

对于一辈子想有个总结,开始想写本书,至少是个系列《游戏开发实战》,写的过程又是重学的过程,而之前定的一周一篇技术文章的目标总是不能执行,因为时间不够,工作很忙,要写一篇好的技术文章很废时间。所以不想再以技术文章为系列去写,写技术文章太枯燥,由浅入深,面面俱到,太累!

所以这次就以时间为轴线,以技术内容为补充,记录一下我从小鸟变成一个老兵的成长过程,在此过程中遇到的一些问题,思考心得。

ThreadLocal解析

发表于 2017-03-15
字数统计: 3.3k 字数 | 阅读时长 ≈ 14 分钟

前言

最近对于职业规划做了些思考,时间不等人,时机不等人;又是金三银四的好时机,今天当面试官虐人,明天被面试被人虐,相互伤害,爱恨情仇,一切都是缘分。

一份简历,一两张A4纸,就要体现出一个人的真实水平,没点功力,还真是难。

技术人都比较实在,不明白之前,感觉很高深,有难点,学了之后必有进步;但在搞过之后,发现不过如此,感觉都是理所当然,简单如是,不好意思跟人讲研究了什么,实在是太简单;

正因为有如此思维,所以在写简历时无处下笔,感觉没什么优势可写,毫无亮点。
要么就是简单写写项目经历,要么对着招聘简介看看,摘抄一些了

不管怎么写,还是要写出自己的特长,把自己拿手的写出来,少不可怕,只要写出来的不被面试官难倒,就算成功。

当然也不能为了体现多面手,乱写,熟悉XX,精通OO,了解MM,掌握GG;到面试时,被问得打脸就不好了。

面试时态度端正也很重要,正面回答问题,有时其实对问题答案掌握不好,回避问题甚至绕弯弯,当然这还是看度的,不可绕太多,行家一出手就知有没有,诚恳点,知道多少说多少,知之为知之。


面试官,我当得也不好,层次不清,岗位定位不清,有时为了招些优秀的,会问些不合岗位的问题,问得过难;

还是要好好挖掘候选人简历,毕竟简历是展现的第一窗口,技术人书写能力差,不能很好的推销自己,所以还是要从简历中挖掘亮点,有时很精通,但写出来的很笼统,需要面试官去发现亮点了

ThreadLocal的意义

这个类,好早就有了,JDK1.2就出现了。有时也会用一用,但他的作用是什么,很难表达了,难以表达,不能形成文字,说明了解的深度不够。

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路;

ThreadLocal的目的是为了解决多线程访问资源时的共享问题。

这基本上搜索到的threadlocal文章开头都是这样写的。

然,谎言说多了就成了真理。

但在JDK文档里面

该类提供了线程局部 (thread-local)变量。
这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。
ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocal和多线程并发没有什么关系。ThreadLocal模式是为了解决单线程内的跨类跨方法调用的

ThreadLocal不是用来解决对象共享访问问题的,而是为了处理在多线程环境中,某个方法处理一个业务,需要递归依赖其他方法时,而要在这些方法中共享参数的问题。

例如有方法a(),在该方法中调用了方法b(),而在b方法中又调用了方法c(),即a–>b—>c,如果a,b,c都需要使用用户对象,那么我们常用做法就是a(User user)–>b(User user)—c(User user)。

但是如果使用ThreadLocal我们就可以用另外一种方式解决:
在某个接口中定义一个静态的ThreadLocal 对象,
例如 public static ThreadLocal threadLocal=new ThreadLocal ();
然后让a,b,c方法所在的类假设是类A,类B,类C都实现1中的接口
在调用a时,使用A.threadLocal.set(user) 把user对象放入ThreadLocal环境
这样我们在方法a,方法b,方法c可以在不用传参数的前提下,在方法体中使用threadLocal.get()方法就可以得到user对象。

上面的类A,类B ,类C就可以分别对应我们做web开发时的 web层的Action—>业务逻辑层的Service–>数据访问层的DAO,当我们要在这三层中共享参数时,那么我们就可以使用ThreadLocal 了。

在使用hibernate中常见的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定义SessionFactory

static {
try {
// 通过默认配置文件hibernate.cfg.xml创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}

//创建线程局部变量session,用来保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();

/**
* 获取当前线程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果Session还没有打开,则新开一个Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的Session保存到线程局部变量中
}
return s;
}

public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为Session类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}

Threadlocal源码

这个类有以下方法:

  1. get():返回当前线程拷贝的局部线程变量的值。
  2. initialValue():返回当前线程赋予局部线程变量的初始值。
  3. remove():移除当前线程赋予局部线程变量的值。
  4. set(T value):为当前线程拷贝的局部线程变量设置一个特定的值。

get()方法

1
2
3
4
5
6
7
8
9
10
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}

根据当前线程,拿到ThreadLocalMap,通过当前threadloal对象取到value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Thread.java 里面

ThreadLocalMap是Thread的属性

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

这个还跟之前的理解不太一样,以为是以当前threadId为key,取到值

数据结构是这样的:

threadlocal<threadId,value>

事实上结构是这样的:

thread <-> threadlocalmap<threadlocal,value>

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。

这样设计的主要有以下几点优势:

  1. 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点
  2. 当 Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

内存泄露

有些人认为在使用此类时,容易出现OOM

image

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄露。

进一步分析一下ThreadlocalMap

1
2
3
4
5
6
7
8
9
10
11
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {

根据注释和代码,发现ThreadLocalMap并没有使用HashMap,而是重新实现了一个map
,里面放的Entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
//默认16长度
table = new Entry[INITIAL_CAPACITY];
//hashcode取模,得到坑位
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

private void set(ThreadLocal key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);

for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();

if (k == key) {
e.value = value;
return;
}
//key在gc时,会被回收,变成null
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
//替换掉之前有key=null的
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;

// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal k = e.get();

// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。将key为null的这些Entry都删除,防止内存泄露。

但是光这样还是不够的,上面的设计思路依赖一个前提条件:
要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。
所以JDK建议将ThreadLocal变量定义成private static的,
这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

为什么使用弱引用

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

总结

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

  • 不是为了解决多线程共享同步之类的作用

  • ThreadLocal变量定义成private static,不要定义成局部变量

参考资料

http://www.iteye.com/topic/617368

http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/

JIT优化之道

发表于 2017-03-02
字数统计: 2.9k 字数 | 阅读时长 ≈ 11 分钟

碎碎念

《JIT优化之道》是去年在公司的一次分享,对于公司组织分享我是赞同又不赞同,怎么讲呢?

技术分享当然是好的,这是一个双赢,分享者教学相长,而收听者也能更快的了解进步。

但以前在原先的公司也做过些类事情,但没有想象的好,大家对分享主题的探索也只限于在分享时间段内,过后很少有人,几乎没人去做进一步的探索。填鸭式的学习效果甚微。后来只涉及一些项目中使用到的知识点,让项目中人去发现项目中的一些亮点,盲区

聪明人从旁人的错误中吸取教训,愚笨人则从自身的错误中吸取教训,有多少聪明人呢?
不经历风雨又怎么见彩虹?

JIT主要关注三个点

  1. JIT是什么
  2. JIT的原理
  3. JIT的意义

JIT是什么

JIT是just in time,即时编译器;使用该技术,能够加速java程序的执行速度

image

编译器

image

Java编译器总的来说分为

  1. 前端编译器
  2. JIT(just in time compiler)编译器
  3. AOT(Ahead Of Time Compiler)编译器

前端编译器: 将Java文件编译为class文件的编译器,目前主要有以下两个,
Sun提供的Javac 和Eclipse JDT中的增量式编译器(ECJ)

JIT编译器: 虚拟机后端运行期编译器,把字节码转换为机器码的过程。
HotSpot Vm中提供的C1, C2编译器

AOT编译器:直接把Java文件转换为本地机器码的过程

解释器与编译器

image

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。

在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率

分层编译的策略TieredCompilation

为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启动分层编译的策略。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,包括:

  1. 第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译。
  2. 第1层,也称为C1编译,将字节码编译成本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
  3. 第2层,也称为C2编译,也是将字节码编译为本地代码,但是会启动一下编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

实施分层编译后,Client Compiler和Server Compiler 将会同时工作,许多代码都可能会多次编译,C1获取更高的编译速度,用C2获取更好的编译质量,在解释时候的时候也无须再承担收集性能监控信息的任务。

1
2
3
4
Oracle JDK从JDK 6u25以后的版本支持了多层编译(-XX:+TieredCompilation)
可以用jinfo -flag或-XX:+PrintFlagsFinal来确认是否打开

JDK8是默认打开的

image

图表描绘了纯解析、客户端、服务器端和混合编译的性能区别。X轴代码执行时间,Y轴代表性能

和单纯的代码解析相比,使用客户端编译器可以提高大约5-10倍的执行性能,实际上提高了应用的性能。当然,收益的变化还是依赖于编译器性能如何,哪些优化生效了或者被实现了,还有就是对于目标执行平台来说应用程序设计的有多好。后者是Java开发人员从来不需要担心的。

和客户端编译器相比,服务器端编译器通常能够提升可度量的30%-50%的代码效率。在大部分情况下,这性能的提高将平衡掉多余的资源开销。

分层编译结合了两种编译器的优点。客户端编译产生了快速的启动时间和及时的优化,服务器端编译在执行周期的后期,可以提供更多的高级优化

JIT开关

image

3种执行方式,分别是解释执行、混合模式和编译执行,默认情况下处于混合模式中

1
通过-Xint  -Xcomp改变执行方式

通过代码也可以

1
2
3
java.lang.Compiler.disable();

java.lang.Compiler.enable();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Pi {
public static double calcPi() {
double re = 0;
for (int i = 1; i < 100000; i++) {
re += ((i & 1) == 0 ? -1 : 1) * 1.0 / (2 * i - 1);
}
return re * 4;
}

public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 100000; i++)
calcPi();
long e = System.currentTimeMillis();
System.err.println("spend:" + (e - b) + "ms");
}
}
mixed:spend:418ms
int:spend:2547ms
comp:spend:416ms

jstat -compiler 显示VM实时编译的数量等信息。
显示列名
具体描述
Compiled
编译任务执行数量
Failed
编译任务执行失败数量
Invalid
编译任务执行失效数量
Time
编译任务消耗时间
FailedType
最后一个编译失败任务的类型
FailedMethod
最后一个编译失败任务所在的类及方法

1
2
3
jstat -compiler 55417
Compiled Failed Invalid Time FailedType FailedMethod
6296 6 0 50.37 1 org/eclipse/jdt/internal/core/CompilationUnitStructureRequestor createTypeInfo

JIT原理

image

寻找热点代码

在运行过程中,会被即时编译器编译的“热点代码”有两类,即:

  1. 被多次调用的方法
  2. 被多次执行的循环体(OSR On StackReplacement)

判断是否是热点代码的行为成为热点探测:hot spotdetection,主要的热点探测方式主要有两种:

  1. 基于采样的热点探测,JVM会周期性检查各个线程的栈顶,如果某个方法经常出现在栈顶,那就认定为热点方法。简单高效,精度不够。

  2. 基于计数器的热点探测,统计方法执行次数。(HOTSPOT使用这种方式)

计数器

HOTSPOT有两个计数器:

1、方法调用计数器

2、回边计数器

方法调用计数器

image

1
2
3
4
5
方法调用计数器
client默认1500次,
server默认10000次,

-XX:CompileThreshold

方法调用计数器并不是统计方法调用绝对次数,而是一个相对执行频率,
超过一定时间,如果方法调用次数不足以让它提交给编译器,则计数器就会被减少一半,这种现象称为热度衰减(Counter Decay),
进行热度衰减的动作是在垃圾回收时顺便进行的,而这段时间就被称为半衰周期(Counter Half Life Time)

1
2
可用-XX:-UseCounterDecay来关闭热度衰减,
用-XX:CounterHalfLifeTime来设置半衰时间。

要不要关闭这个衰减?

HotSpot VM的触发JIT的计数器的半衰(counter decaying)是一种很好的机制,保证只有真正热的代码才会被编译,而那种偶尔才被调用一次的方法则不会因为运行时间长而积累起虚假的热度。

不建议关闭这个选项,除非它在具体案例中造成了问题。

Counter decaying是伴随GC运行而执行的。过多的手动调用System.gc()倒是有可能会干扰了这个衰减,导致方法达不到触发JIT编译的热度。

回边计数器

image

回边计数器阈值计算公式

(1)Client模式下
方法调用计数器阈值(CompileThreshold)* OSR比率(OnStackReplacePercentage)/100.
其中OnStackReplacePercentage默认值为933,如果都取默认值,那Client模式虚拟机回边数的阈值为13995。

(2)Server模式下
方法调用计数器阈值(CompileThreshold)*(OSR比率(OnStackReplacePercentage)减去解释器监控比率(InterpreterProfilePercentage)的差值)/100。

其中OnStackReplacePercentage默认值为140,InterpreterProfilePercentage默认值为33,
如果都取默认值,那Server模式虚拟机回边计数器的阈值为10700

1
2
-XX:BackEdgeThreshold
-XX:OnStackReplacePercentage

与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数

学习JIT意义

大方法 与 小方法?

java中一般建议一个方法不要写的过长,不方便维护和阅读是其中的一个原因,但是其真正性能的原因大家知道吗?

我们知道,JVM一开始是以解释方式执行字节码的。当这段代码被执行的次数足够多以后,它会被动态优化并编译成机器码执行,执行速度会大大加快,这就是所谓的JIT编译。
hotsopt源码中有一句

1
if (DontCompileHugeMethods && m->code_size() > HugeMethodLimit) return false;

当DontCompileHugeMethods=true且代码长度大于HugeMethodLimit时,方法不会被编译

DontCompileHugeMethods与HugeMethodLimit的值在globals.hpp中定义:

1
2
3
4
product(bool, DontCompileHugeMethods, true,
"don't compile methods > HugeMethodLimit")
develop(intx, HugeMethodLimit, 8000,
"don't compile methods larger than this if +DontCompileHugeMethods")

上面两个参数说明了Hotspot对字节码超过8000字节的大方法有JIT编译限制,这就是大方法不会被JIT编译的原因。由于使用的是product mode的JRE,

我们只能尝试关闭DontCompileHugeMethods,即增加VM参数”-XX:-DontCompileHugeMethods”来强迫JVM编译大方法。

但是不建议这么做,因为一旦CodeCache满了,HotSpot会停止所有后续的编译任务,虽然已编译的代码不受影响,但是后面的所有方法都会强制停留在纯解释模式。

查看jit工作的参数

1
2
3
4
5
6
7
8
9
10
-XX:-CITime 打印发费在JIT编译上的时间

$ java -server -XX:+CITime Benchmark
[...]
Accumulated compiler times (for compiled methods only)
------------------------------------------------
Total compilation time : 0.178 s
Standard compilation : 0.129 s, Average : 0.004
On stack replacement : 0.049 s, Average : 0.024
[...]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-XX:+PrintCompilation 我们可以简单的输出一些关于从字节码转化成本地代码的编译过程

$ java -server -XX:+PrintCompilation Benchmark
1 java.lang.String::hashCode (64 bytes)
2 java.lang.AbstractStringBuilder::stringSizeOfInt (21 bytes)
3 java.lang.Integer::getChars (131 bytes)
4 java.lang.Object::<init> (1 bytes)
--- n java.lang.System::arraycopy (static)
5 java.util.HashMap::indexFor (6 bytes)
6 java.lang.Math::min (11 bytes)
7 java.lang.String::getChars (66 bytes)
8 java.lang.AbstractStringBuilder::append (60 bytes)
9 java.lang.String::<init> (72 bytes)
10 java.util.Arrays::copyOfRange (63 bytes)
11 java.lang.StringBuilder::append (8 bytes)
12 java.lang.AbstractStringBuilder::<init> (12 bytes)
13 java.lang.StringBuilder::toString (17 bytes)
14 java.lang.StringBuilder::<init> (18 bytes)
15 java.lang.StringBuilder::append (8 bytes)
[...]
29 java.util.regex.Matcher::reset (83 bytes)

每当一个方法被编译,就输出一行-XX:+PrintCompilation。每行都包含顺序号(唯一的编译任务ID)和已编译方法的名称和大小。

因此,顺序号1,代表编译String类中的hashCode方法到原生代码的信息。根据方法的类型和编译任务打印额外的信息。例如,本地的包装方法前方会有”n”参数,像上面的System::arraycopy一样。注意这样的方法不会包含顺序号和方法占用的大小,因为它不需要编译为本地代码。

同样可以看到被重复编译的方法,例如StringBuilder::append顺序号为11和15。输出在顺序号29时停止 ,这表明在这个Java应用运行时总共需要编译29个方法。

GC及JVM参数

发表于 2017-02-22
字数统计: 13.9k 字数 | 阅读时长 ≈ 51 分钟

前言

这段时间懈怠了,罪过!

最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我也不推广,默默的静静的,主要是担心自己坚持不了。以前写过时间事件日志现在也不写了;写过博客也不写了;月记也不写了。

坚持平凡事就是伟大,本来计划一周一篇的,这次没有严格执行。懈怠了

这个GC跟JVM内容太多了,理论性东西多些,少年时还能记个八九成,好久没弄,都忘记了。这次权当整理温习,再看看《深入理解JVM虚拟机》,找些过去写的博客挖点东西过来!

GC

Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对虚拟机中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证虚拟机中的内存空间,防止出现内存泄露和溢出问题。

主要从这几个问题入手,就差不多了

  1. Java内存区域
  2. 哪些内存需要回收?
  3. 什么时候回收
  4. 如何回收
  5. 监控和优化GC

Java内存区域

image

  1. 程序计数器(Program Counter Register)

程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区 域中唯一一个没有定义OutOfMemoryError的区域。

  1. 虚拟机栈(JVM Stack)

一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定 好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,知道内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。

  1. 本地方法栈(Native Method Statck):

本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。本地方法栈也是线程私有的。

  1. 堆区(Heap)

堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

1
2
3
4
5
6
7
-Xms 参数设置最小值

-Xmx 参数设置最大值
例:VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

若-Xms=-Xmx,则可避免堆自动扩展。
-XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出是dump出当前的内存堆转储快照。

  1. 方法区(Method Area)

在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。
方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上 执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。在方法区上定义了OutOfMemoryError:PermGen space异常,
在内存不足时抛出。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

1
2
3
-XX:MaxPermSize 设置上限
-XX:PermSize 设置最小值
例:VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M

  1. 直接内存(Direct Memory)

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

Direct Memory满了之后,系统不会自动回收这段内存; 而是要等Tenured Generation满触发GC时,Direct Memory才会被跟着回收。

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

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

1
2
3
-XX:MaxDirectMemorySize 设置最大值,默认与java堆最大值一样。

例 :-XX:MaxDirectMemorySize=10M -Xmx20M

哪些内存被回收

根据运行时数据区域的各个部分,程序计数器、虚拟机栈、本地方法栈三个区域随着线程而生,随线程灭而灭。栈中的栈帧随着方法的进入和退出而进栈出栈。每个栈帧分配多少内存在类结构确定下来的时候就基本已经确定。所以这个三个区域内存回收时方法或者线程结束而回收的,不需要太多关注;而java堆和方法区则不一样,一个接口不同实现类,一个方法中不同的分支,在具体运行的时候才能确定创建那些对象,所以这部分内存是动态的,也是需要垃圾回收机制来回收处理的。

  1. 堆内存

判断堆内的对象是否可以回收,要判断这个对象实例是否确实没用,判断算法有两种:引用计数法和根搜索算法。

  • 引用计数法:就是给每个对象加一个计数器,如果有一个地方引用就加1,当引用失效就减1;当计数器为0,则认为对象是无用的。这种算法最大的问题在于不能解决相互引用的对象,如:A.b=B;B.a=A,在没有其他引用的情况下,应该回收;但按照引用计数法来计算,他们的引用都不为0,显然不能回收。

  • 根搜索算法:这个算法的思路是通过一系列名为“GC Roots”的对象作为起点,
    从这个节点向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(图论的不可达)时,则证明该对象不可用。

java等一大部分商用语言是用根搜索算法来管理内存的,java中可以做为GC Roots的对象有如下几种:

虚拟机栈(栈帧中的本地变量表)中的引用的对象;

方法区中的类静态属性引用的对象;

方法区中常量引用的对象;

本地方法栈JNI(Native)的引用对象;

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK1.2以前,Java中的引用的定义很传统如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 强引用

只要强引用还存在,垃圾收集器永远不会收掉被引用的对象

  • 软引用

在系统将要发生内存异常之前,将会把这些对象列进回收范围之中进行第二次回收。

  • 弱引用

被弱引用关联的对象只能生存道下一次垃圾收集发生之前。

  • 虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。

finalize()方法

在Object类中

1
protected void finalize() throws Throwable { }

注意下这个访问控制符是protected

finalize()在什么时候被调用?

有三种情况

  1. 所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候.
  2. 程序退出时为每个对象调用一次finalize方法。
  3. 显式的调用finalize方法

当一个对象不可到达时,并不是马上就被回收的。

image

当对象没有覆盖finalize()方法,或者finalized()已经被JVM调用过,那就是没有必要执行finalzied()
;Finalizer线程执行它,但并不保证等待它执行结束,这主要是防止finalize()出现问题,导致Finalizer线程无限等待,整个内存回收系统崩溃

具体的finalize流程:

对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义如下:
unfinalized: 新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的
finalizable: 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行
finalized: 表示GC已经对该对象执行过finalize方法
reachable: 表示GC Roots引用可达
finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达
unreachable:对象不可通过上面两种途径可达
image

  1. 新建对象首先处于[reachable, unfinalized]状态(A)
  2. 随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态
  3. 若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)。
  4. 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N), 这就是对象重生
  5. 处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable。这也是图中只有八个状态点的原因
  6. 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
  7. 若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)
  8. 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)

注:System.runFinalizersOnExit()等方法可以使对象即使处于reachable状态,JVM仍对其执行finalize方法

对finalize()的一句话概括:
JVM能够保证一个对象在回收以前一定会调用一次它的finalize()方法。这句话中两个陷阱:回收以前一定和一次

但有很多地方是讲,JVM不承诺这一定调用finalize(),这就是上面的陷阱造成的

你永远不知道它什么时候被调用甚至会不会调用(因为有些对象是永远不会被回收的,或者被回收以前程序就结束了),但如果他是有必要执行finalize()的,那在GC前一定调用一次且仅一次,如果在第一次GC时没有被回收,那以后再GC时,就不再调用finalize()

  1. 方法区

很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集*++一般可以回收70%~95%的空间++,而永久代的垃圾收集效率远低于此。

方法区回收主要有两部分:废弃的常量和无用的类。废弃的常量判断方法和堆中的对象类似,只要判断没有地方引用就可以回收。相比之下,判断一个类是否无用,条件就比较苛刻,需要同时满足下面3个条件才能算是“无用的类”:

  • 该类的所有实例都已经被回收,也就是java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。是否对类进行回收,

1
2
3
4
5
6
HotSpot虚拟机提供了
-Xnoclassgc参数进行控制,
还可以使用
-verbose:class
-XX:+TraceClassLoading
-XX:+TraceClassUnLoading查看类的加载和卸载信息。

在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出

如何回收

选择合适的GC collector是JVM调优最重要的一项,前提是先了解回收算法

“标记-清除”(Mark-Sweep)

image

算法分为“标记”和“清除”两个阶段:
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象

主要缺点有两个

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

“复制”(Copying)

image

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

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

1
2
3
-XX:SurvivorRatio=4
设置年轻代中Eden区与Survivor区的大小比值。
设置为4,则Eden区与两个Survivor区的比值为4:1:1,一个Survivor区占整个年轻代的1/6

为什么新生代有两个survivor?
StackOverflow上面给出的解释是:

The reason for the HotSpot JVM’s two survivor spaces is to reduce the need to deal with fragmentation. New objects are allocated in eden space. All well and good. When that’s full, you need a GC, so kill stale objects and move live ones to a survivor space, where they can mature for a while before being promoted to the old generation. Still good so far. The next time we run out of eden space, though, we have a conundrum. The next GC comes along and clears out some space in both eden and our survivor space, but the spaces aren’t contiguous. So is it better to

  1. Try to fit the survivors from eden into the holes in the survivor space that were cleared by the GC?
  2. Shift all the objects in the survivor space down to eliminate the fragmentation, and then move the survivors into it?
  3. Just say “screw it, we’re moving everything around anyway,” and copy all of the survivors from both spaces into a completely separate space–the second survivor space–thus leaving you with a clean eden and survivor space where you can repeat the sequence on the next GC?

Sun’s answer to the question is obvious.

“标记-整理”(Mark-Compact)

image

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,

  1. 第一阶段从根节点开始标记所有被引用对象,
  2. 第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

“分代收集”(Generational Collection)

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,
这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

image

  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具
    备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

  • 老年代 GC(Major GC):指发生在老年代的 GC,出现了 Major GC,经常
    会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
    就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10
    倍以上。
    虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。

    1
    对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

垃圾收集器

image

按系统线程分

image

注意并发(Concurrent)和并行(Parallel)的区别:

  1. 并发是指用户线程与GC线程同时执行(不一定是并行,可能交替,但总体上是在同时执行的),不需要停顿用户线程(其实在CMS中用户线程还是需要停顿的,只是非常短,GC线程在另一个CPU上执行);
  2. 并行收集是指多个GC线程并行工作,但此时用户线程是暂停的;

这个跟传统的并发并行概念不同
并行是物理的,并发是逻辑的。
并行是和串行对立。

Serial收集器

image
Serial是最基本、历史最悠久的垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾收集器。目前也是ClientVM下 ServerVM 4核4GB以下机器的默认垃圾回收器。
串行收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需要中断所有的用户线程,知道它回收结束为止,因此又号称“Stop The World” 的垃圾回收器。注意,JVM中文名称为java虚拟机,因此它就像一台虚拟的电脑一样在工作,而其中的每一个线程就被认为是JVM的一个处理器,因此大家看到图中的CPU0、CPU1实际为用户的线程,而不是真正机器的CPU,大家不要误解哦。

串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。

Serial收集器默认新旧生代的回收器搭配为Serial+ SerialOld

新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩

1
2
3
在J2SE5.0上,在非server模式下,JVM自动选择串行收集器。
也可以显示进行选择,在Java启动参数中增加:
-XX:+UseSerialGC

Serial Old收集器

SerialOld是旧生代Client模式下的默认收集器,单线程执行,使用“标记-整理”算法
在Server模式下,主要有两个用途:

  1. 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
  2. 作为年老代中使用CMS收集器的后备垃圾收集方案。

ParNew收集器

image
ParNew收集器其实就是多线程版本的Serial收集器,

Stop The World

他是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收效率远远低于Serial收集器,所以一定要注意场景哦)

Server模式下的默认收集器。

新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

1
2
3
4
-XX:+UseParNewGC  ParNew收集器

ParNew收集器默认开启和CPU数目相同的线程数
-XX:ParallelGCThreads 限制线程数量

Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,也称吞吐量优先的收集器

所提到的吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。在当今网络告诉发达的今天,良好的响应速度是提升用户体验的一个重要指标,多核并行云计算的发展要求程序尽可能的使用CPU和内存资源,尽快的计算出最终结果,因此在交互不多的云端,比较适合使用该回收器。

可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例

新生代复制算法、老年代标记-压缩

1
2
3
4
5
6
7
8
9
10
-XX:+UseParallelGC  使用Parallel收集器+ 老年代串行

Parallel Scavenge收集器提供了两个参数用于精准控制吞吐量:
a.-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,是一个大于0的毫秒数。
b.-XX:GCTimeRation:直接设置吞吐量大小,是一个大于0小于100的整数,
也就是程序运行时间占总时间的比率,默认值是99,即垃圾收集运行最大1%(1/(1+99))的垃圾收集时间

-XX:+UseAdaptiveSizePolicy,这是个开关参数,
打开之后就不需要手动指定新生代大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、
新生代晋升年老代对象年龄(-XX:PretenureSizeThreshold)等细节参数

Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供

在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略

1
参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图:
image

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

优点:并发收集、低停顿

缺点:产生大量空间碎片、并发阶段会降低吞吐量

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

  1. 初始标记(CMS initial mark)

  2. 并发标记(CMS concurrent mark)

  3. 重新标记(CMS remark)

  4. 并发清除(CMS concurrent sweep)

a.初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

b.并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

c.重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

d.并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。

CMS收集器工作过程:

image

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

CMS收集器有以下三个不足:

  • CMS收集器对CPU资源非常敏感,其默认启动的收集线程数=(CPU数量+3)/4,在用户程序本来CPU负荷已经比较高的情况下,如果还要分出CPU资源用来运行垃圾收集器线程,会使得CPU负载加重。

  • CMS无法处理浮动垃圾(Floating Garbage),可能会导致Concurrent ModeFailure失败而导致另一次Full GC。由于CMS收集器和用户线程并发运行,因此在收集过程中不断有新的垃圾产生,这些垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好等待下一次GC时再将其清理掉,这些垃圾就称为浮动垃圾。
    CMS垃圾收集器不能像其他垃圾收集器那样等待年老代机会完全被填满之后再进行收集,需要预留一部分空间供并发收集时的使用,可以通过参数-XX:CMSInitiatingOccupancyFraction来设置年老代空间达到多少的百分比时触发CMS进行垃圾收集,默认是68%。
    如果在CMS运行期间,预留的内存无法满足程序需要,就会出现一次ConcurrentMode Failure失败,此时虚拟机将启动预备方案,使用Serial Old收集器重新进行年老代垃圾回收。

  • CMS收集器是基于标记-清除算法,因此不可避免会产生大量不连续的内存碎片,如果无法找到一块足够大的连续内存存放对象时,将会触发因此Full GC。CMS提供一个开关参数-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后进行内存整理,内存整理会使得垃圾收集停顿时间变长,CMS提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不压缩的Full GC之后,跟着再来一次内存整理

    1
    2
    3
    4
    -XX:+UseConcMarkSweepGC  使用CMS收集器
    -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
    -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
    -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

G1收集器

G1可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位;同时,他也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。

image

与CMS收集器相比G1收集器有以下特点:

  1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

  2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。

和CMS类似,G1收集器收集老年代对象会有短暂停顿。

  1. 标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)

  2. Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。

  3. Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
    image

  4. Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

  5. Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
    image

  6. 复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。
    image

1
2
3
4
5
-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC        #开启
-XX:MaxGCPauseMillis =50 #暂停时间目标
-XX:GCPauseIntervalMillis =200 #暂停间隔目标
-XX:+G1YoungGenSize=512m #年轻代大小
-XX:SurvivorRatio=6 #幸存区比例

什么时候回收

Minor GC触发

  1. Eden区域满了,或者新创建的对象大小 > Eden所剩空间
  2. CMS设置了CMSScavengeBeforeRemark参数,这样在CMS的Remark之前会先做一次Minor GC来清理新生代,加速之后的Remark的速度。这样整体的stop-the world时间反而短
  3. Full GC的时候会先触发Minor GC

啥时候会触发CMS GC?

CMS不等于Full GC,很多人会认为CMS肯定会引发Minor GC。CMS是针对老年代的GC策略,原则上它不会去清理新生代,只有设置CMSScavengeBeforeRemark优化时,或者是concurrent mode failure的时候才会去做Minor GC

1、旧生代或者持久代已经使用的空间达到设定的百分比时(CMSInitiatingOccupancyFraction这个设置old区,perm区也可以设置);

2、JVM自动触发(JVM的动态策略,也就是悲观策略)(基于之前GC的频率以及旧生代的增长趋势来评估决定什么时候开始执行),如果不希望JVM自行决定,可以通过-XX:UseCMSInitiatingOccupancyOnly=true来制定;

3、设置了 -XX:CMSClassUnloadingEnabled 这个则考虑Perm区;

啥时候会触发Full GC?

一、旧生代空间不足:java.lang.outOfMemoryError:java heap space;

二、Perm空间满:java.lang.outOfMemoryError:PermGen space;

三、CMS GC时出现promotion failed 和concurrent mode failure(Concurrent mode failure发生的原因一般是CMS正在进行,但是由于old区内存不足,需要尽快回收old区里面的死的java对象,这个时候foreground gc需要被触发,停止所有的java线程,同时终止CMS,直接进行MSC。);

四、统计得到的minor GC晋升到旧生代的平均大小大于旧生代的剩余空间;

五、主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题;

六、调用System.gc时,系统建议执行Full GC,但是不必然执行,-XX:+DisableExplicitGC 禁用System.gc()调用

GC策略选择总结

jvm有client和server两种模式,这两种模式的gc默认方式是不同的:

client模式下,新生代选择的是串行gc,旧生代选择的是串行gc

server模式下,新生代选择的是并行回收gc,旧生代选择的是并行gc

一般来说我们系统应用选择有两种方式:吞吐量优先和暂停时间优先,对于吞吐量优先的采用server默认的并行gc方式,对于暂停时间优先的选用并发gc(CMS)方式。

监控与调优

GC日志

1
2
3
4
5
6
7
8
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
-verbose.gc开关可显示GC的操作内容。
打开它,可以显示最忙和最空闲收集行为发生的时间、收集前后的内存大小、收集需要的时间等

-XX:+PrintGCTimeStamps和-XX:+PrintGCDateStamps

使用-XX:+PrintGCTimeStamps可以将时间和日期也加到GC日志中。表示自JVM启动至今的时间戳会被添加到每一行中。例子如下:

1
2
3
1 0.185: [GC 66048K->53077K(251392K), 0.0977580 secs]
2 0.323: [GC 119125K->114661K(317440K), 0.1448850 secs]
3 0.603: [GC 246757K->243133K(375296K), 0.2860800 secs]

如果指定了-XX:+PrintGCDateStamps,每一行就添加上了绝对的日期和时间。

1
2
3
1 2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0.0959470 secs]
2 2014-01-03T12:08:38.239-0100: [GC 119125K->114661K(317440K), 0.1421720 secs]
3 2014-01-03T12:08:38.513-0100: [GC 246757K->243133K(375296K), 0.2761000 secs]

如果需要也可以同时使用两个参数。推荐同时使用这两个参数,因为这样在关联不同来源的GC日志时很有帮助

每一种收集器的日志形式都是由它们自身的实现所决定的,换而言之,每个收集器的日志格式都可以不一样。

但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性,

例如以下两段典型的GC日志:

1
2
3
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K),
[Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。

GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,

而不是用来区分新生代GC还是老年代GC的。

如果有“Full”,说明这次GC是发生了Stop-The-World的,

例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。

如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC (System)”。

1
[Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs]

接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的

例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。

如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。

如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量-> GC后该内存区域已使用容量 (该内存区域总容量)”。

而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)”。

再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。

有的收集器会给出更具体的时间数据

如“[Times: user=0.01 sys=0.00, real=0.02 secs]”,
这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。

CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。

分析工具

可以使用一些离线的工具来对GC日志进行分析

比如sun的gchisto( https://java.net/projects/gchisto)

gcviewer( https://github.com/chewiebug/GCViewer ),

这些都是开源的工具,用户可以直接通过版本控制工具下载其源码,进行离线分析   

JVM参数

HotSpot JVM 提供了三类参数。
第一类包括了标准参数。顾名思义,标准参数中包括功能和输出的参数都是很稳定的,很可能在将来的JVM版本中不会改变。你可以用java命令(或者是用 java -help)检索出所有标准参数。我们在第一部分中已经见到过一些标准参数,例如:-server。

第二类是X参数,非标准化的参数在将来的版本中可能会改变。所有的这类参数都以-X开始,并且可以用java -X来检索。注意,不能保证所有参数都可以被检索出来,其中就没有-Xcomp。

第三类是包含XX参数(到目前为止最多的),它们同样不是标准的,甚至很长一段时间内不被列出来(最近,这种情况有改变 ,我们将在本系列的第三部分中讨论它们)。然而,在实际情况中X参数和XX参数并没有什么不同。X参数的功能是十分稳定的,然而很多XX参数仍在实验当中(主要是JVM的开发者用于debugging和调优JVM自身的实现)。值的一读的介绍非标准参数的文档 HotSpot JVM documentation,其中明确的指出XX参数不应该在不了解的情况下使用。这是真的,并且我认为这个建议同样适用于X参数(同样一些标准参数也是)。不管类别是什么,在使用参数之前应该先了解它可能产生的影响。
用一句话来说明XX参数的语法。所有的XX参数都以”-XX:”开始,但是随后的语法不同,取决于参数的类型。

  • 对于布尔类型的参数,我们有”+”或”-“,然后才设置JVM选项的实际名称。例如,-XX:+用于激活选项,而-XX:-用于注销选项。
  • 对于需要非布尔值的参数,如string或者integer,我们先写参数的名称,后面加上”=”,最后赋值。例如, -XX:=给赋值。
1
-XX:+PrintFlagsFinal and -XX:+PrintFlagsInitial
1
2
3
4
5
6
7
8
9
[Global flags]
uintx AdaptivePermSizeWeight = 20 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}[...]
uintx YoungGenerationSizeSupplementDecay = 8 {product}
uintx YoungPLABSize = 4096 {product}
bool ZeroTLAB = false {product}
intx hashCode = 0 {product}

表格的每一行包括五列,来表示一个XX参数。第一列表示参数的数据类型,第二列是名称,第四列为值,第五列是参数的类别。第三列”=”表示第四列是参数的默认值,而”:=” 表明了参数被用户或者JVM赋值了。

1
-XX:+PrintCommandLineFlags

这个参数让JVM打印出那些已经被用户或者JVM设置过的详细的XX参数的名称和值。

换句话说,它列举出 -XX:+PrintFlagsFinal的结果中第三列有”:=”的参数。

以这种方式,我们可以用-XX:+PrintCommandLineFlags作为快捷方式来查看修改过的参数

监控jvm

使用自带工具就行,jstat,jmap,jstack

  • jstack主要用来查看某个Java进程内的线程堆栈信息
  • jmap用来查看堆内存使用状况,一般结合jhat使用
  • jstat利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对进程的classloader,compiler,gc情况

优化

  1. 选择合适的GC collector
  2. 整个JVM heap的大小
  3. young generation在整个JVM heap中所占的比重

参数实例

1
2
3
4
5
6
7
public static void main(String[] args) throws InterruptedException{
//通过allocateDirect分配128MB直接内存
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);

TimeUnit.SECONDS.sleep(10);
System.out.println("ok");
}

测试用例1:设置JVM参数-Xmx100m,运行异常,因为如果没设置-XX:MaxDirectMemorySize,则默认与-Xmx参数值相同,分配128M直接内存超出限制范围

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:658)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)
at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)

#
#
为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:

+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled

默认CMS是在tenured generation沾满68%的时候开始进行CMS收集,如果你的年老代增长不是那么快,并且希望降低CMS次数的话,可以适当调高此值:

-XX:CMSInitiatingOccupancyFraction=80

遇到两种fail引起full gc:

Prommotion failed和Concurrent mode failed时:

promotion failed是在进行Minor GC时,survivor space放不下、
对象只能放入旧生代,而此时old gen 的碎片太多为进行过内存重组和压缩,无法提供一块较大的、连续的内存空间存放来自新生代对象

Prommotion failed的日志输出大概是这样:

1
2
42576.951: [ParNew (promotion failed): 320138K->320138K(353920K), 0.2365970 secs]
42576.951: [CMS: 1139969K->1120688K( 166784K), 9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs]

因为
解决这个问题的办法有两种完全相反的倾向:增大救助空间、增大年老代或者去掉救助空间。

1
2
3
解决方法可以通过设置参数
-XX:+UseCMSCompactAtFullCollection(打开对年老代的压缩)
-XX:CMSFullGCsBeforeCompaction(设置运行多少次FULL GC以后对内存空间进行压缩、整理)

1
2
直接关了servivor空间
-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0

concurrent mode failure

发生在当CMS已经在工作并处于concurrent阶段中,而Java堆的内存不够用需要做major GC(full GC)的时候。换句话说,old gen内存的消耗速度比CMS的收集速度要高,CMS收集器跟不上分配速度的时候会发生concurrent mode failure

Concurrent mode failed的日志大概是这样的:

1
2
(concurrent mode failure): 1228795K->1228598K(1228800K), 7.6748280 secs] 1911483K->1681165K(1911488K), 
[CMS Perm : 225407K->225394K(262144K)], 7.6751800 secs]

避免这个现象的产生就是调小-XX:CMSInitiatingOccupancyFraction参数的值,
让CMS更早更频繁的触发,降低年老代被沾满的可能。

#
#
full gc频繁说明old区很快满了。

如果是一次full gc后,剩余对象不多。那么说明你eden区设置太小,导致短生命周期的对象进入了old区

如果一次full gc后,old区回收率不大,那么说明old区太小

#
#

1
2
3
4
5
6
7
8
已知虚拟机的一些参数设置如下: 
-Xms:1G;
-Xmx:2G;
-Xmn:500M;
-XX:MaxPermSize:64M;
-XX:+UseConcMarkSweepGC;
-XX:SurvivorRatio=3;
求Eden区域的大小?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
分析这是网易2016年在线笔试题中的一道选择题。 
先分析一下里面各个参数的含义:
-Xms:1G , 就是说初始堆大小为1G
-Xmx:2G , 就是说最大堆大小为2G
-Xmn:500M ,就是说年轻代大小是500M(包括一个Eden和两个Survivor)
-XX:MaxPermSize:64M , 就是说设置持久代最大值为64M
-XX:+UseConcMarkSweepGC , 就是说使用使用CMS内存收集算法
-XX:SurvivorRatio=3 , 就是说Eden区与Survivor区的大小比值为3:1:1

题目中所问的Eden区的大小是指年轻代的大小,直接根据-Xmn:500M和-XX:SurvivorRatio=3可以直接计算得出
解500M*(3/(3+1+1))
=500M*(3/5)
=500M*0.6
=300M
所以Eden区域的大小为300M。

#
#
-Xmn200m -server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
jmap -heap pid

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4282384384 (4084.0MB)
NewSize = 209715200 (200.0MB)
MaxNewSize = 209715200 (200.0MB)
OldSize = 58720256 (56.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
capacity = 92274688 (88.0MB)
used = 37931936 (36.174713134765625MB)
free = 54342752 (51.825286865234375MB)
41.107628562233664% used
From Space:
capacity = 58720256 (56.0MB)
used = 20305320 (19.364662170410156MB)
free = 38414936 (36.635337829589844MB)
34.57975387573242% used
To Space:
capacity = 56623104 (54.0MB)
used = 0 (0.0MB)
free = 56623104 (54.0MB)
0.0% used
PS Old Generation
capacity = 176160768 (168.0MB)
used = 65785064 (62.737525939941406MB)
free = 110375704 (105.2624740600586MB)
37.34376544044132% used

发现为什么from space与to space不对呢。应该是20M才对

1
2
3
4
5
6
7
8
-server默认使用ParallelScavenge系的GC

HotSpot VM里,ParallelScavenge系的GC(UseParallelGC / UseParallelOldGC)
默认行为是SurvivorRatio如果不显式设置就没啥用。
显式设置到跟默认值一样的值则会有效果

因为ParallelScavenge系的GC最初设计就是默认打开AdaptiveSizePolicy的,
它会自动、自适应的调整各种参数
1
需要显示配置 -XX:SurvivorRatio=8

参考资料

http://yinwufeng.iteye.com/blog/2157787

http://itindex.net/detail/47030-cms-gc-%E9%97%AE%E9%A2%98

http://www.cnblogs.com/ityouknow/p/5614961.html

motan服务端

发表于 2017-02-06 | 分类于 源码解读
字数统计: 1.8k 字数 | 阅读时长 ≈ 8 分钟

前引

新年回来上班,记录下服务端的处理过程,就当热个身

服务端的处理也有套路,不管上层怎么玩,最后还得是通过反射得到Method对象,再调用invoke()

image
根据这张序列图,可以把服务端分为两部分

  1. NettyServer前面的算一部分,搭基础构建Exporter对象
  2. nettyserver后面的算一部分,找到对应method,invoke,通过网络返回

构建Exporter对象

结合spring

其实在《motan客户端》时有提过,但没有深究;spring扩展自定义xml是个很老的技术了
spring扩展xml文档

spring通过XML解析程序将其解析为DOM树,通过NamespaceHandler指定对应的Namespace的BeanDefinitionParser将其转换成BeanDefinition。再通过Spring自身的功能对BeanDefinition实例化对象。

在期间,Spring还会加载两项资料:

  1. META-INF/spring.handlers

指定NamespaceHandler(实现org.springframework.beans.factory.xml.NamespaceHandler)接口,或使用org.springframework.beans.factory.xml.NamespaceHandlerSupport的子类。

  1. META-INF/spring.schemas

在解析XML文件时将XSD重定向到本地文件,避免在解析XML文件时需要上网下载XSD文件。通过现实org.xml.sax.EntityResolver接口来实现该功能。

ConfigHandler

解析完xml后,服务器通过ServiceConfigBean来监听spring容器加载完成。

1
2
3
4
5
6
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!getExported().get()) {
export();
}
}

调用export(),构建Exporter;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public synchronized void export() {
if (exported.get()) {
LoggerUtil.warn(String.format("%s has already been expoted, so ignore the export request!", interfaceClass.getName()));
return;
}

checkInterfaceAndMethods(interfaceClass, methods);

List<URL> registryUrls = loadRegistryUrls();
if (registryUrls == null || registryUrls.size() == 0) {
throw new IllegalStateException("Should set registry config for service:" + interfaceClass.getName());
}

Map<String, Integer> protocolPorts = getProtocolAndPort();
for (ProtocolConfig protocolConfig : protocols) {
Integer port = protocolPorts.get(protocolConfig.getId());
if (port == null) {
throw new MotanServiceException(String.format("Unknow port in service:%s, protocol:%s", interfaceClass.getName(),
protocolConfig.getId()));
}
doExport(protocolConfig, port, registryUrls);
}

afterExport();
}

这个方法主要就做了两件事

  1. loadRegistryUrls(),根据motan:registry,生成URL对象。URL也算是整个框架的核心,一个url包含了配置中的所有内容。就像一个领域对象一样。

如果使用的是zookeeper,对就的URL就是:zookeeper://127.0.0.1:2181/com.weibo.api.motan.registry.RegistryService?group=default_rpc

这是注册中心的URL

而服务URL是以参数为embed为key包含在里面,里面包含了所有的服务参数

motan://127.0.0.1:8002/com.share.rpc.service.DEMOService?module=match-rpc&loadbalance=activeWeight&nodeType=service&accessLog=true&minWorkerThread=2&protocol=motan&isDefault=true&maxWorkerThread=10&refreshTimestamp=1486448597095&id=com.weibo.api.motan.config.springsupport.ServiceConfigBean&export=protocolMatch:8002&requestTimeout=60000&group=match-rpc&

  1. doExport()
    1
    2
    ConfigHandler configHandler = ExtensionLoader.getExtensionLoader(ConfigHandler.class).getExtension(MotanConstants.DEFAULT_VALUE);
    exporters.add(configHandler.export(interfaceClass, ref, urls));

到这儿就是委托给ConfigHandler处理了。

默认的实现类:SimpleConfigHandler

这个类里面有两个重要的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public <T> T refer(Class<T> interfaceClass, List<Cluster<T>> clusters, String proxyType) {
ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getExtension(proxyType);
return proxyFactory.getProxy(interfaceClass, new RefererInvocationHandler<T>(interfaceClass, clusters));
}

@Override
public <T> Exporter<T> export(Class<T> interfaceClass, T ref, List<URL> registryUrls) {

String serviceStr = StringTools.urlDecode(registryUrls.get(0).getParameter(URLParamType.embed.getName()));
URL serviceUrl = URL.valueOf(serviceStr);

// export service
// 利用protocol decorator来增加filter特性
String protocolName = serviceUrl.getParameter(URLParamType.protocol.getName(), URLParamType.protocol.getValue());
Protocol protocol = new ProtocolFilterDecorator(ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(protocolName));
Provider<T> provider = new DefaultProvider<T>(ref, serviceUrl, interfaceClass);
Exporter<T> exporter = protocol.export(provider, serviceUrl);

// register service
register(registryUrls, serviceUrl);

return exporter;
}

refer()就是RefererConfig配置完后的调用的方法,就看到了客户端的核心类RefererInvocationHandler

export()就是服务端使用的方法了。

构造provider

1
2
3
4
public interface Provider<T> extends Caller<T> {

Class<T> getInterface();
}

其实就是外面服务类的代理类,里面就一个服务类的引用。在初始化时,把所有的方法全部缓存

1
2
3
4
5
6
7
8
private void initMethodMap(Class<T> clz) {
Method[] methods = clz.getMethods();

for (Method method : methods) {
String methodDesc = ReflectUtil.getMethodDesc(method);
methodMap.put(methodDesc, method);
}
}

Protocol的export就是启动一个netty server,提供对外服务。

register 注册服务

按上面export(),先启动一个netty server,服务一切就绪后,再向注册中心进行注册了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void register(List<URL> registryUrls, URL serviceUrl) {

for (URL url : registryUrls) {
// 根据check参数的设置,register失败可能会抛异常,上层应该知晓
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getExtension(url.getProtocol());
if (registryFactory == null) {
throw new MotanFrameworkException(new MotanErrorMsg(500, MotanErrorMsgConstant.FRAMEWORK_REGISTER_ERROR_CODE,
"register error! Could not find extension for registry protocol:" + url.getProtocol()
+ ", make sure registry module for " + url.getProtocol() + " is in classpath!"));
}
Registry registry = registryFactory.getRegistry(url);
registry.register(serviceUrl);
}
}

根据注册协议选择注册中心
RegistryFacotry有三种:1. direct 2.local 3.zookeeper

对应的registry也是这三种

在zookeeperregistry里面,

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void doRegister(URL url) {
try {
serverLock.lock();
// 防止旧节点未正常注销
removeNode(url, ZkNodeType.AVAILABLE_SERVER);
removeNode(url, ZkNodeType.UNAVAILABLE_SERVER);
createNode(url, ZkNodeType.UNAVAILABLE_SERVER);
} catch (Throwable e) {
throw new MotanFrameworkException(String.format("Failed to register %s to zookeeper(%s), cause: %s", url, getUrl(), e.getMessage()), e);
} finally {
serverLock.unlock();
}
}

这儿注册后,这个服务还是unavailable,需要调用
MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, true);才能变成available

NettyServer

一样的套路,有一个默认ChannelHandler,其实什么事都不干,只是包装一下自定义Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final NettyChannelHandler handler = new NettyChannelHandler(NettyServer.this, messageHandler,
standardThreadExecutor);

bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
// FrameDecoder非线程安全,每个连接一个 Pipeline
public ChannelPipeline getPipeline() {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("channel_manage", channelManage);
pipeline.addLast("decoder", new NettyDecoder(codec, NettyServer.this, maxContentLength));
pipeline.addLast("encoder", new NettyEncoder(codec, NettyServer.this));
pipeline.addLast("handler", handler);
return pipeline;
}
});

这儿有个有意思的MessageHandler:ProviderProtectedMessageRouter
从名字看,有保护功能

1) 如果接口只有一个方法,那么直接return true
2) 如果接口有多个方法,那么如果单个method超过 maxThread / 2 && totalCount > (maxThread 3 / 4),那么return false;
3) 如果接口有多个方法(4个),同时总的请求数超过 maxThread
3 / 4,同时该method的请求数超过 maxThead * 1 / 4, 那么return false
4) 其他场景return true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected boolean isAllowRequest(int requestCounter, int totalCounter, int maxThread, Request request) {
if (methodCounter.get() == 1) {
return true;
}

// 该方法第一次请求,直接return true
if (requestCounter == 1) {
return true;
}

// 不简单判断 requsetCount > (maxThread / 2) ,因为假如有2或者3个method对外提供,
// 但是只有一个接口很大调用量,而其他接口很空闲,那么这个时候允许单个method的极限到 maxThread * 3 / 4
if (requestCounter > (maxThread / 2) && totalCounter > (maxThread * 3 / 4)) {
return false;
}

// 如果总体线程数超过 maxThread * 3 / 4个,并且对外的method比较多,那么意味着这个时候整体压力比较大,
// 那么这个时候如果单method超过 maxThread * 1 / 4,那么reject
return !(methodCounter.get() >= 4 && totalCounter > (maxThread * 3 / 4) && requestCounter > (maxThread * 1 / 4));

}

拒绝的结果就是放一个异常:

1
2
3
4
5
6
7
8
9
10
11
private Response reject(String method, int requestCounter, int totalCounter, int maxThread) {
DefaultResponse response = new DefaultResponse();
MotanServiceException exception =
new MotanServiceException("ThreadProtectedRequestRouter reject request: request_counter=" + requestCounter
+ " total_counter=" + totalCounter + " max_thread=" + maxThread, MotanErrorMsgConstant.SERVICE_REJECT);
exception.setStackTrace(new StackTraceElement[0]);
response.setException(exception);
LoggerUtil.error("ThreadProtectedRequestRouter reject request: request_method=" + method + " request_counter=" + requestCounter
+ " =" + totalCounter + " max_thread=" + maxThread);
return response;
}

other

RpcContext

1
2
3
4
5
6
7
8
9
private static final ThreadLocal<RpcContext> localContext = new ThreadLocal<RpcContext>() {
protected RpcContext initialValue() {
return new RpcContext();
}
};

public static RpcContext getContext() {
return localContext.get();
}

这个ThreadLocal尽然还可以设置默认初始值,以前尽然没用过

总结

服务端相对客户端还是很简单的。

没有ha,loadbalance,就是原生netty就把请求处理完了。

1…141516
朱兴生

朱兴生

154 日志
3 分类
53 标签
© 2016 — 2023 朱兴生 | Site words total count: 349.2k
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
沪ICP备18040647号-1