上周遇到一个莫名其妙的心态问题,浪费了我好几个小时。
我太生气了。这几个小时敲(摸)院子(鱼)不香吗?
主要是最后一个问题的解决方法也让我无语。我越想越生气。我会写一篇文章,吐槽一下。
先说结论,也就是标题:
当在本地调试模式下启动一个项目时,永远不要中断方法!请不要!
一、什么是方法断点?
例如,在方法名的行上命中断点:
请点击IDEA下面的图标,查看断点,它会为你弹出一个框。
这个弹出框显示了当前项目中的所有断点,并且有一个复选框JavaMethodBreakpoints,它是所有& quot方法断点& quot在当前项目中:
那么这个东西有什么问题呢?
当项目在调试模式下启动时,它会非常非常严重地降低启动速度。
我给你看两张截图。
以下是我所在地区一个非常简单的项目。当没有方法断点时,启动只需要1.753秒:
但是当我添加一个方法断点时,启动时间直接变成了35.035秒:
从1.7秒到35秒,启动时间增加了2000%。
你觉得难以忍受吗?
我很抱歉,对吗?
那么我是怎么踏入这个坑的呢?
一个同事说他的项目遇到了一个不可思议的BUG,想让我帮他看看。
所以我先把项目拉下来,然后简单看了一下代码,准备本地运行项目,先调试一下。
然而半个小时过去了,项目还没有开始。我问他:为什么这么久才在本地启动这个项目?
他回答:正常情况下,应该半分钟后开始。
然后他给我演示了一下,真的在他那边30多秒就启动成功了。
很明显,同样的代码,一个地方启动慢,一个地方启动快。首先,怀疑环境问题。
所以我要按照下面的程序走一遍。
检查设置-清除缓存-更改工作区-重启-更改电脑-退出。
我检查了所有的配置,启动项,网络连接等等,确保和他的本地环境完全一样。
这次行动过去了差不多一个小时,没有发现任何线索。
但我当时并没有慌,我还有终极一招:重启。
毕竟我电脑好几个月没关机了,重启一下就好了。
果然,重启电脑后,还是没有变化。
我正着急的时候,同事过来问我有什么进展。
我能说什么呢?
我只能说要及时解决,但实际上我连项目都还没成功启动。
听了这话,他坐在我的工位上,准备给我看一看。
半分钟后,神奇的一幕出现了。他直接在我的电脑上开始了这个项目。
追问之下,他并没有以调试模式启动,而是直接运行。
如果你用脚趾头想想,你一定是在调试模式下做什么。
然后,基于面向浏览器编程的原理,我现在有几个关键词:IDEAdebug启动慢。
然后我发现很多人都遇到了类似的问题,解决的办法就是取消& quot方法断点在项目启动时。
然而,很遗憾,并不是大多数文章都说这样做是好的。但如果你不告诉我你为什么这么做就好了。
我很想知道为什么会有这个坑,因为我还是用了很多方法来破点。关键是我在使用过程中根本没注意到这个坑。
"方法断点还是很实用的,比如我随便举个例子。
我之前写一篇关于交易的文章的时候,提到过这样一个方法:
Java . SQL . connection # set auto commit setauto commit有几个实现类,不知道取哪个:
因此,在调试时,可以在下面的接口上放一个断点:
然后重启程序,IDEA会自动帮你决定取哪个实现类:
但是,需要注意的是,并不是所有的方法断点都会导致启动缓慢。至少在我本地看起来是这样的。
当我向Mapper的接口添加方法断点时,我可以稳定地重现这个问题:
在给项目的其他方法添加方法断点时,没有必要,这个问题只是偶尔出现。
另外,实际上当你用方法断点启动调试模式时,IDEA会弹出这个提醒,告诉你方法断点会导致调试变慢:
然而,真正的男人从不看提醒。反正我就是不理会,完全不在乎弹出窗口的内容。
为什么要破坏Mapper接口上的方法?
都是我的错,好吗?
https://intellij-support . jetbrains.com/HC/en-us/articles/206544799-Java-启动时性能缓慢或挂起-调试器-单步调试这篇文章。
,是JetBrainsTeam发布的,关于Debug功能可能会导致的性能缓慢的问题。在这个帖子中,第一个性能点,就是Methodbreakpoints。
官方是怎么解释这个问题的呢?
我给你翻译一波。
MethodbreakpointswillslowdowndebuggeralotbecauseoftheJVMdesign,theyareexpensivetoevaluate.他们说由于JVM的设计,方法断点会大大降低调试器的速度,因为这玩意的“evaluate”成本很高。
evaluate,四级单词,好好记一下,考试会考:
大概就是说你要用方法断点的功能,在启动过程中,就涉及到一个关于该断点进行“评估”的成本。成本就是启动缓慢。
怎么解决这个“评估”带来的成本呢?
官方给出的方案很简单粗暴:
不要使用方法断点,不就没有成本了?
所以,Remove,完事:
Removemethodbreakpointsandconsiderusingtheregularlinebreakpoints.删除方法断点并考虑使用常规的linebreakpoints。
官方还是很贴心的,怕你不知道怎么Remove还专门补充了一句:
Toverifythatyoudon'thaveanymethodbreakpointsopen.idea/workspace.xmlfileintheprojectrootdirectory(or.iwsfileifyouareusingtheoldprojectformat)andlookforanybreakpointsinsidethemethod_breakpointsnode.
可以通过下面这个方法去验证你是否打开了方法断点。
就是去.idea/workspace.xml文件中,找到method_breakpoints这个Node,如果有就Remove一下。
然后我看了一下我项目里面对应的文件,没有找到method_breakpoints关键字,但是找到了下面这个。
应该是文档发生了变化,问题不大,反正是一个意思,
其实官方给出的这个方法,虽然逼格稍微高一点,但还是我前面给的这个操作更简单:
针对“到底为什么”这个问题。
在这里,官方给的回答,特别的模糊:becauseoftheJVMdesign。
别问,问就是由于JVM设计如此。
我觉得这不是我想要的答案,但是好在我在这个帖子下面找到了一个“好事之人”写的回复:
这个好事之人叫做Gabi老铁,我看到他回复的第一句话“Imadesomeresearch”,我就知道,这波稳了,找对地方了,答案肯定就藏在他附上的这个链接里面。
Gabi老铁说:哥子们,我研究了一下这个方法断点为啥会慢的原因,研究报告在这里:
http://www.smartik.net/2017/11/method-breakpoints-are-evil.html他甚至还来了一个概要:Tomakethelongstoryshort,长话短时。
他真的很贴心,我哭死。
他首先指出了问题的根本原因:
itseemsthattherootissueisthatMethodBreakpointsareimplementedbyusingJDPA'sMethodEntry&MethodExitfeature.根本问题在于方法断点是通过使用JDPA的MethodEntry&MethodExit特性实现的。
有同学就要问了,JDPA,是啥?
是个宝贝:
https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/index.htmlJPDA,全称JavaPlatformDebuggerArchitecture。
IDEA里面的各种Debug功能,就是基于这个玩意来实现的。
不懂也没关系,这个东西面试又不考,在这里知道有这个技术就行。
接着,他用了四个any来完成了跳句四押:
ThisimplementationrequirestheJVMtofireaneventeachtimeanythreadentersanymethodandwhenanythreadexitsanymethod.这个实现,要求JVM,每次,在任何(any)线程进入任何(any)方法时,以及在任何(any)线程退出任何(any)方法时触发事件。
好家伙,这不就是个AOP吗?
这么一说,我就明白为什么方法断点的性能这么差了。要触发这么多进入方法和退出方法的事件,可不得耗费这么多时间吗?
具体的细节,他在前面说的研究报告里面都写清楚了,如果你对细节感兴趣的话,可以咨询阅读一下他的那篇报告。
话说他这个报告的名字也起的挺唬人的:MethodBreakpointsareEvil。
我带你看两个关键的地方。
第一个是关于MethodEntry&MethodExit的:
IDE将断点添加到其内部方法断点list中IDE告诉前端启用MethodEntry&MethodExit事件前端(调试器)通过代理将请求传递给VM在每个MethodEntry&MethodExit事件中,通过整个链将通知转发到IDEIDE检查其方法断点list是否包含当前的这个方法。如果发现包含,说明这个方法上有一个方法断点,则IDE将向VM发送一个SetBreakpoint请求,打上断点。否则,VM的线程将被释放,不会发生任何事情这里是表明,前面我说的那个类似AOP的稍微具体一点的操作。
核心意思就一句话:触发的事件太多,导致性能下降厉害。
第二个关键的地方是这样的:
文章的最后给出了五个结论:
方法断点IDE的特性,不是JPDA的特性方法断点是真的邪恶,evil的一比方法断点将极大的影响调试程序只有在真正需要时才使用它们如果必须使用方法作为断点,请考虑关闭方法退出事件前面四个点没啥说的了。
最后一个点:考虑关闭方法退出事件。
这个点验证起来非常简单,在方法断点上右键可以看到这个选项,MethodEntry&MethodExit默认都是勾选上了:
所以我在本地随便用一个项目验证了一下。
打开MethodExit事件,启动耗时:113.244秒。
关闭MethodExit事件,启动耗时:46.754秒。
你别说,还真有用。
现在我大概是知道为什么方法断点这么慢了。
这真不是BUG,而是feature。
而关于方法断点的这个问题,我顺便在社区搜索了一下,最早我追溯到了2008年:
这个老哥说他调试Web程序的速度慢到无法使用的程度。他的项目只启用了一行断点,没有方法断点。
请求大佬帮他看看。
然后大佬帮他一顿分析也没找到原因。
他自己也特别的纳闷,说:
我啥也没动,太奇怪了。这玩意有时可以,有时不行。
像不像一句经典台词:
但是问题最后还是解决了。怎么解决的呢?
他自己说:
确实是有个方法断点,他也不知道怎么打上这个断点的,可能和我一样,是手抖了吧。
在前面出现的官方帖子的最下面,有这样的两个链接:
它指向了这个地方:
https://www.jetbrains.com/help/idea/debugging-code.html我把这部分链接都打开看了一遍,经过鉴定,这可真是好东西啊。
这是官方在手摸手教学,教你如何使用Debug模式。
我之前看过的一些调试小技巧相关的文章,原来就是翻译自官方这里啊。
我在这里举两个例子,算是一个导读,强烈推荐那些在Debug程序的时候,只知道不停的下一步、跳过当前断点等这样的基本操作的同学去仔细阅读,动手实操一把。
首先是这个:
针对Java的Streams流的调试。
官方给了一个调试的代码示例,我做了一点点微调,你粘过去就能跑:
classPrimeFinder{publicstaticvoidmain(String[]args){IntStream.iterate(1,n->n+1).limit(100).filter(PrimeTest::isPrime).filter(value->value>50).forEach(System.out::println);}}classPrimeTest{staticbooleanisPrime(intcandidate){returncandidate==91||IntStream.rangeClosed(2,(int)Math.sqrt(candidate)).noneMatch(n->(candidate%n==0));}}代码逻辑很简单,就是找100以内的,大于50的素数。
很明显,在isPrime方法里面对91这个非素数做了特殊处理,导致程序最终会输出91,也就是出BUG了。
虽然这个BUG一目了然,但是不要笑,要忍住,要假装不知道为什么。
现在我们要通过调试的方式找到BUG。
断点打在这个位置:
以Debug的模式运行的时候,有这样的一个图标:
点击之后会有这样的一个弹窗出来:
上面框起来的是对应着程序的每一个方法调用顺序,以及调用完成之后的输出是什么。
下面框起来的这个“FlatMode”点击之后是这样的:
最右边,也就是经过filter之后输出的结果。
里面就包含了91这个数:
点击这个“91”,发现在经过第一个filter之后,91这个数据还在。
说明这个地方出问题了。
而这个地方就是前面提到的对“91”做了特殊处理的isPrime方法。
这样就能有针对性的去分析这个方法,缩小问题排除范围。
这个功能怎么说呢,反正我的评论是:
总之,以上就是IDEA对于Streams流进行调试的一个简单示例。
接着再演示一个并发相关的:
官方给了这样的一个示例:
publicclassConcurrencyTest{staticfinalLista=Collections.synchronizedList(newArrayList());publicstaticvoidmain(String[]args){Threadt=newThread(()->addIfAbsent(17));t.start();addIfAbsent(17);t.join();System.out.println(a);}privatestaticvoidaddIfAbsent(intx){if(!a.contains(x)){a.add(x);}}}代码里面搞一个线程安全的list集合,然后主线程和一个异步线程分别往这个list里面塞同一个数据。
按照addIfAbsent方法的意思,如果要添加的元素在list里面存在了,则不添加。
你说这个程序是线程安全的吗?
肯定不是。
你想想,先判断,再添加,经典的非原子性操作。
但是这个程序你拿去直接跑,又不太容易跑出线程不安全的场景:
怎么办?
Debug就来帮你干这个事儿了。
在这里打一个断点,然后右键断点,选择“Thread”:
这样程序跑起来的时候主线程和异步线程都会在这个地方停下来:
可以通过“Frames”中的下拉框分别选择Debug主线程还是异步线程。
由于两个线程都执行到了add方法,所以最终的输出是这样的:
这不就出现线程不安全了吗?
即使你知道这个地方是线程不安全的,但是如果没有Debug来帮忙调试,要通过程序输出来验证还是比较困难的。
毕竟多线程问题,大多数情况下都不是每次都能必现的问题。
定位到问题之后,官方也给出了正确的代码片段:
好了,说好了是导读,这都是基本操作。还是那句话,如果感兴趣,自己去翻一下,跟着案例操作一下。
就算你看到有人把Debug源码,玩出花来了,也无外乎不过是这样的几个基础操作的组合而已。
让我们再次回到官方的“关于Debug功能可能会导致的性能缓慢的问题”这个帖子里面:
当我看到方框里面框起来的“Collectionsclasses”和“toString()”方法的时候,眼泪都快下来了。
我最早开始写文章的时候,曾经被这个玩意坑惨了。
三年前,2019年,我写了这篇文章《这道Java基础题真的有坑!我也没想到还有续集。》
当时Debug调试ArrayList的时候遇到一个问题,我一度以为我被质子干扰了:
一句话汇总就是在单线程的情况下,程序直接运行的结果和Debug输出的结果是不一样的。
当时我是百思不得其解。
直到8个月后,写《JDK的BUG导致的内存溢出!反正我是没想到还能有续集》这篇文章的时候才偶然间找到问题的答案。
根本原因就是在Debug模式下,IDEA会自动触发集合类的toString方法。而在某些集合类的toString方法里面,会有诸如修改头节点的逻辑,导致程序运行结果和预期的不匹配。
也就是对应这句话:
翻译过来就是:老铁请注意,如果toString方法中的代码更改了程序的状态,则在debug状态下运行时,这些方法也可以更改应用程序的运行结果。
最后的解决方案就是关闭IDEA的这两个配置:
同时,我也在官方文档中找到了这个两个配置的解释:
https://www.jetbrains.com/help/idea/customizing-views.html#renderers主要是为了在Debug的过程中用更加友好的形式显示集合类。
啥意思?
给你看个例子。
这是没有勾选前面说的配置的时候,map集合在Debug模式下的样子:
这是勾选之后,map集合在Debug模式下的样子:
很明显,勾选了之后的样子,更加友好。
但是,为了避免莫名其妙的BUG,我选择关闭这个功能。
好了,那本文的技术部分就到这里啦。
下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。
你要不喜欢,退出之前记得文末点个“在看”哦。
这是我上周五晚上拍的。路过一个工地,看到工人师傅们三三两两的就在工地门口的小推车前面吃饭喝酒,缓解一天的疲劳。
在流动路边摊上,吃的都是简单的填肚子的东西,比如一大碗炒饭,一大碗面条什么的,再配上一瓶冰冻的啤酒,一大口下肚,来宣告今天的活就算是干完了。
看到这个场景,让我想起了读大学的时候。
夏天的晚上,特别喜欢和朋友一起骑车到学校附近的工地上去吃晚饭。
在一张极具工地特色的长桌上吃饭,说是一个桌子其实就是工地上打围用的木板,四个角各支撑几个板凳,就算是一个餐桌了。
整个夏天,我们和工人师傅们在这一张极简的餐桌上吃过好几顿晚饭。
每次都是吃一碗扎扎实实的素面,五块钱一大碗,啤酒两块钱一瓶。一口面条,一口啤酒,物美价廉,特别带劲。
一次我们坐在还未完工的建筑下面,对面是赤膊吃饭,沉默不语的工人师傅。背后是学生如织,活力青春的大学校园。
坐在我旁边的几个工人师傅突然问我们:你们是哪里来的?
朋友指着背后的学校说:从学校过来。
拉扯了几句,说到我们是学计算机的。
接着师傅发起了一个直击灵魂的问题:你们学计算机,毕业后都干啥呢?
那个时候我才大二,我哪知道是干啥的?
说实在的,这个问题我大四快毕业了都没想清楚。
现在我明白了,毕业后,我就是国家认证的新生代农民工,也是一个“搬砖”的人而已。
原文链接:https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247543852&idx=1&sn=2466f7ab4c66c0b9ca1dcb9a6590c72e