讲真,我发现这本书有个地方写错了!

image.png
本文首发于公众号【why技术】,关注公众号,有更加优秀的排版方式,阅读体验更佳。
why技术java


可恶的标题党

首先,我先说一下我发现的《Java并发编程的艺术》写错的地方吧。
我手上这本《Java并发编程的艺术》的版次是:2019年3月第1版第14次印刷。
2019年3月第1版第14次印刷算法

我浏览目录的时候注意到了其中3.6.5小节的标题是:《为何final引用不能从构造函数内“溢出”》
.6.5小节的标题编程

很明显,做者这里是一个笔误。从做者该小节具体的描述也能够看出来,【溢出】应该是【逸出】。
溢出 -- 逸出数组

看到这里,你要说我是一个"可恶的标题党",我也不反驳。由于这个错误,结合上下文来看,确实无伤大雅。安全

可是,只看标题呢?若是只知道java有内存溢出,不知道java有引用逸出的读者呢?微信

他们可能抠破脑壳,也想不出"构造函数内的final引用"和"内存溢出"之间有什么联系吧?多线程

好了,这个不重要。并发

由于本文想要阐述的,不是这个笔误,而是这个笔误,背后隐藏的两大知识点:【引用逸出】和【内存溢出】。框架

主要是接合《Java并发编程的艺术》、《Java并发编程实战》、《深刻理解Java虚拟机》这三本书中的相关内容进行对比,而后展开描述。函数

同时须要强调的是:我认为,这个小小的笔误,彻底不妨碍这本书的优秀性。这是一本提高并发编程能力干货满满的书。


对象&引用逸出

在《Java并发编程实战》的3.2小节中是这样定义发布与逸出的:

“发布(Publish)”一个对象的意思是指,使对象可以在当前做用域以外的代码中使用。将一个指向该对象的引用保存到其余代码能够访问到的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其余类的方法中。

当某个不该该发布的对象被发布时,这种状况就被成为"逸出(Escape)"。

概念读起来老是让人摸不着头脑。
摸不着头脑

咱们直接看书里面给出的程序清单3-5:
程序清单3-5

如程序清单3-5所示:在initialize方法中实例化一个新的HashSet对象,并将对象的引用保存到knownSecrets中以发布该对象。

这段代码有什么问题?

当发布knownSecrets对象时,间接地发布了Secret对象。由于任何代码均可以遍历这个集合,并得到对这个新Secret对象的引用。因此Secret对象"逸出"了,这是不安全的。

再看书里给出的另一个程序清单3-6:
程序清单3-6
若是按照上述方式来发布states,就会出现问题,由于任何调用者都能修改这个数组的内容。在程序清单3-6中,数组states已经"逸出"了它所在的做用域,由于这个本应该是私有的变量已经被发布了。

当某个对象逸出后,你必须作最坏的打算,必须假设某个类或者线程可能会误用该对象。

同时书中也说到,这也正是须要使用封装的最主要的缘由:
封装可以使得对程序的正确性进行分析变得可能,并使得无心中破坏设计约束条件变得更难。

this引用逸出

在《Java并发编程实战》里面给出了一个"隐式地使this引用逸出"的例子。以下所示:
隐式地使this引用逸出
ThisEscape在发布其内部类EventListener时,由于EventListener这个内部类包含了对ThisEscape实例的引用,因此使ThisEscape实例发生了"this引用逸出"。

很差理解对不对?咱们再看看书中的描述:
书中的描述
对于不正确构造,做者给了一个备注说明:
具体来讲,只有当构造函数返回时,this引用才应该从线程中逸出。构造函数能够将this引用保存到某个地方,只要其余线程不会在构造函数完成以前使用它。

也不太好理解对不对?确实是,由于我以为这个代码片断少了几个关键的引导的地方;而这段话很难提炼出关键词,由于全是关键词。

可是我读到这段话的时候,有一句话直接吸引了个人注意力,仿佛把手举得高高的在喊:看我,看我!
即便发布对象的语句位于构造函数的最后一行也是如此

做者为何要感受是轻描淡写,其实是在强调"最后一行"呢?

做者没有明说,可是答案是重排序,由于有了重排序,因此一行代码看起来是在最后一行,实际上不是最后一行。

这里咱们接合《Java并发编程的艺术》发生笔误的这一章节里面的例子,来讲明【this引用逸出】和【即便发布对象的语句位于构造函数的最后一行也是如此】这两个问题,代码以下:
代码

假设一个线程A执行writer()方法,另外一个线程B执行reader()方法。

这里的操做2(obj=this)使得对象还未完成构造前就为线程B可见。即便这里的操做2(obj=this)是构造函数的最后一步。

且在程序中操做2(obj=this)排在操做1(i=1)后面,执行read()方法的线程仍然可能没法看到final域被初始化后的值。

由于这里的操做1(i=1)和操做2(obj=this)之间可能被重排序。实际的执行时序可能以下图所示:
多线程执行时序图

因此《Java并发编程的艺术》里面的示例代码和多线程下代码的执行时序图就很好的说明了【this引用逸出带来的问题(线程不安全)】,解答了【《Java并发编程实战》中没有明说的为何"即便最后一行"也不行(重排序)】。

这一小节就是我读完《Java并发编程实战》、《Java并发编程的艺术》以后,取出书中部份内容再加上本身对于对象&引用逸出的理解的总结、输出。

其实《深刻理解Java虚拟机》里面也有对逃逸描述的相关内容,有兴趣的能够翻阅一下。以下:
《深刻理解Java虚拟机》目录
《深刻理解Java虚拟机》目录


内存溢出

若是前面说的引用逸出让你云里雾里,快要瞌睡了。那接下咱们要谈的内存溢出,你们应该都是耳熟能详的了。

file

先上一个来自《深刻理解Java虚拟机》中第2章【Java内存区域与内存溢出异常】中的一张清晰的、牛逼的、经典的、一应俱全的大图:
file
Java 虚拟机运行时数据区

这个图包含的知识点能够说是很是多,全是"内功心法",咱们只讨论其中的一大分支---内存溢出。

因此,本小节内容的目的有两个:

第一,经过代码验证Java虚拟机规范中描述的各个运行时区域存储的内容。

第二,但愿读者在工做中遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪一个区域的内存溢出,知道什么样的代码可能会致使这些区域内存溢出,以及出现这些异常后该如何处理。

对于每一个区域具体的职能,就不铺开讲了,一铺开,又是一个万字长篇。我在保证质量的前提下,尽可能精简字数,让你们读起来不要那么耗时(实在耗时的话,说明我真的用心在写,能够收藏起来或者转发朋友圈慢慢看呀),一进来,一看完,半小时过去了。
file
file
话很少说,精彩继续。

程序计数器

此内存区域是惟一一个在Java虚拟机规范中没有规定任何OutOfMemoryError状况的区域。

因为没有OutOfMemoryError的状况,因此不作模拟。

虚拟机栈&本地方法栈

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

若是线程请求的栈深度大于虚拟机所容许的最大深度,将抛出StackOverflowError异常。

若是虚拟机在扩展栈时没法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分红两种状况,看似更加严谨,但却存在着一些互相重叠的地方:当栈空间没法继续分配时,究竟是内存过小,仍是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。

常言说的好:Talk is cheap.Show me the code(光说不练假把式)。咱们用代码说话:

在《深刻理解Java虚拟机》笔者的实验中,将实验范围限制于单线程中的操做,尝试了下面两种方法均没法让虚拟机产生OutOfMemoryError异常,尝试的结果都是得到StackOverflowError异常,测试代码以下所示。
file

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

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

虚拟机栈和本地方法栈OOM测试(仅做为第一点测试程序)
运行结果:
file

在单线程下,不管因为栈帧太大仍是虚拟机栈容量过小,当内存没法分配的时候,虚拟机抛出的都是StackOverflowError异常。

那咱们怎么去模拟OutOfMemoryError异常呢?

我查阅了一些其余的文章,他们的测试不限于单线程,经过不断地建立线程的方式产生内存溢出异常。举出的例子也是书中的例子,以下:
file

运行结果:
Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread

可是不少文章中没有把书中的特殊说明摆出来,我以为这里是混淆概念的问题,应该进行特殊说明,以下:
这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种状况下,为每一个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

那做者为何说这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系呢?
其实缘由不难理解,操做系统分配给每一个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部份内存的最大值。剩余的内存为2GB(操做系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,能够忽略掉。若是虚拟机进程自己耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈"瓜分"了。每一个线程分配到的栈容量越大,能够创建的线程数量天然就越少,创建线程时就越容易把剩下的内存耗尽。

因此,书中提醒读者须要在开发多线程的应用时特别注意,若是是创建过多线程致使的内存溢出,在不能减小线程数或者更换64位虚拟机的状况下(如今用32位的应该是极少数了吧),就只能经过减小最大堆和减小栈容量来换取更多的线程。若是没有这方面的处理经验,这种经过"减小内存"的手段来解决内存溢出的方式会比较难以想到。

方法区溢出

怎么让方法区溢出?

咱们不妨先换个问法,方法区里面放的是什么东西?

这样一问,你们都知道:方法区用于存放Class的相关信息,好比类名、 访问修饰符、 常量池、 字段描述、 方法描述等。

知道它存放的东西是Class相关信息了,那咱们不停的往里面放入类,不就溢出了吗。

接下来问题又来了,咱们怎么在运行时产生大量的类去往方法区里面放呢?

在书中做者给出的示例代码,是借助CGLib直接操做字节码运行时生成了大量的动态类。 以下:
file

须要多说一句的是,书中的JDK版本是1.7,个人JDK版本是1.8。由于JDK1.8中用Metaspace代替了Permsize,所以在咱们设置VM Args的时候须要有所变化,正如上面图片展现的那样。

JDK1.8运行结果:

file

JDK1.7运行结果:

file

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,断定条件是比较苛刻的。在常常动态生成大量Class的应用中,须要特别注意类的回收情况。

这类场景常见有以下几种:

1.上面提到的程序使用了CGLib字节码加强和动态语言

2.大量JSP或动态产生JSP文件的应用(JSP第一次运行时须要编译为Java类)

3.基于OSGi的应用(即便是同一个类文件,被不一样的加载器加载也会视为不一样的类)等。

而对于使用CGLib字节码加强技术的这种场景,能够说是很是常见了。咱们经常使用的Spring框架中就有大量的CGLib技术的应用。随便截个源码的图片,好比这个CglibAopProxy。
file

Java堆溢出

这块区域的OOM异常,能够说是咱们在实际开发的过程当中最多见的内存溢出异常状况。
file

众所周知,Java堆里面放的是对象实例,按照以前的想法,咱们只要不断的建立对象,这样当建立的对象数量足够多的时候,就会产生内存溢出异常。

再读一读上面的话,这个描述对吗?

这样说是不彻底正确的。若是咱们建立的时对象被垃圾回收机制清除了呢?
file

因此书中给出的完整的描述是这样的:

java堆用于存储对象实例,只要不断地建立对象,而且保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大的容量限制后就会产生内存溢出异常。
这里涉及到的GC Root和可达性分析算法也是很是重要的知识点。不展开讲了,若是不了解的读者,建议了解一下,都是知识点啊,朋友们。

咱们再看书中给出的示例代码:
file

运行结果(多么熟悉、亲切、辨识度高的异常啊):
file

Java堆内存的OOM异常是实际应用中常见的内存溢出异常状况。当出现Java堆内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"会跟着进一步提示"Java heap space"。

要解决这个区域的异常,通常的手段是先经过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是不是必要的,也就是要先分清楚究竟是出现了内存泄漏( Memory Leak)仍是内存溢出( Memory Overflow) 。

内存泄漏解决思路:
若是是内存泄露,可进一步经过工具查看泄露对象到GC Roots的引用链。因而就能找到泄露对象是经过怎样的路径与GC Roots相关联并致使垃圾收集器没法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息, 就能够比较准确地定位出泄露代码的位置。

内存溢出解决思路:
若是不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还能够调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的状况,尝试减小程序运行期的内存消耗。

通过上面的对各个区域的一顿操做后,再来细细品味这一张清晰的、牛逼的、经典的、一应俱全的大图:
file
Java 虚拟机运行时数据区

每次读完《深刻理解Java虚拟机》都会耐人寻味。对于其做者周志明先生:在下佩服!
file


最后说两点

送书

第一点:文章提到的《Java并发编程实战》、《Java并发编程的艺术》、《深刻理解Java虚拟机》这三本书,我认为都是很是优秀的,值得反复翻阅的技术书籍。能够关注我后在后台回复关键字【Java】,便可得到这三本书的电子版。可是,对于这类工具书,强烈建议购买实体书,以便作读书笔记和随手翻阅。

因此,我打算自掏腰包送一本书给个人读者。读者能够关注我公众号后在后台回复关键字【书籍】,便可参与抽奖。中奖后的读者能够从《Java并发编程的艺术》《Java并发编程实战》《深刻理解Java虚拟机》三本中任选一本。

加群

第二点:由于加我我的微信的人愈来愈多,不少人的问题都具备类似性,因此我建立了一个技术分享的群,咱们能够在这里交流技术,品味生活,感悟人生。欢迎你进来一块儿学习,相互交流,共同进步,愿你我一块儿早日成为真正的大佬。

file

若群二维码失效,能够加我我的微信(公众号菜单栏有个人微信二维码),我拉你入群。

谢谢您的阅读,感谢您的关注。我的能力有限,文章中不免有纰漏,错误的地方,若是您发现了,烦请指出,我对其加以修改。若是你以为文章写的不错,你的点赞、转发、赞扬就是对我最大的鼓励。

以上。

PS:说出来你可能不信,这篇文章我已经很收敛的在写了,仍是有6359个字,真的是我用心在写。

file

这篇文章特别耗时,由于在写以前我把文中提到的三本书的相关章节又仔细的阅读了一次,写的过程当中也在反复翻阅。快餐时代下,修炼内功心法,仍是须要细嚼慢咽。

欢迎关注公众号【why技术】。在这里我会分享一些技术相关的东西,主攻java方向,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评,影评。愿你我共同进步。

公众号-why技术

相关文章
相关标签/搜索