当前位置: 首页 > 后端技术 > Java

永远不要破坏方法!有个大洞!_1

时间:2023-04-02 00:53:06 Java

上周我遇到了一个莫名其妙的心态问题,花了我好几个小时。气死我了,花这几个小时敲(摸)码(鱼)不香吗?主要是最后一道题的解法也让我很无语。我越想越生气,于是写了一篇文章吐槽一下。先说结论,也就是标题:Debug模式下本地启动项目时,不要中断方法!请不要!首先,什么是方法断点?比如在方法名那一行打断点:你在IDEA中点击下面的图标,ViewBreakpoints,它会弹出一个框给你。此弹框显示当前项目中的所有断点。有个复选框,JavaMethodBreakpoints,就是当前项目中所有的“方法断点”:那么这个东西有什么坑呢?当项目以调试模式启动时,它会非常、非常、非常严重地减慢启动速度。给你看两张截图。下面是我本地的一个很简单的项目。没有方法断点的时候,启动只需要1.753秒:但是当我加了方法断点的时候,启动时间直接来了35.035秒:从1.7秒直接飙升到35秒,启动时间增加了2000%.你觉得你受得了?受不了是吧?那我是怎么踏入这个坑的呢?有个同事说他的项目遇到了一个莫名其妙的BUG,想让我帮忙看看。于是先把项目拉下来,然后简单看了下代码,打算在本地运行项目调试一下。然而,半个小时过去了,工程还没有开始。我问他:为什么这个项目本地启动时间这么长?他回答:正常情况下,半分钟就可以启动了。然后他给我演示,果然在他这边30多秒就成功启动了。很明显,同样的代码,一个地方启动慢,另一个地方启动快,所以首先怀疑是环境问题。所以我打算把下面的过程走一遍。检查设置->清除缓存->更改工作区->重启->更改计算机->退出我检查了所有的配置,启动项,网络连接等,保证和他本地的环境一模一样。这一套操作下来,差不多一个小时过去了,也没发现什么端倪。不过那时候我一点都不慌,还有绝招:重启。毕竟我的电脑已经几个月没关机了,重启一下就好了。果然,重启电脑后,还是没有任何变化。就在我万念俱灰的时候,同事过来问我有什么进展。whatcanisay我只能说:按时间来说,应该可以解决,但实际上我连项目启动都没有成功。闻言,他坐在我的办公桌前,准备帮我看看。半分钟后,神奇的一幕出现了,他直接在我的电脑上启动了项目。经过询问,他并没有以Debug模式启动,而是直接运行。用脚趾头想想就知道Debug模式是干什么的。那么基于面向浏览器编程的原则,我现在有几个关键词:IDEA调试启动慢。然后发现很多人都遇到过类似的问题。解决方法是在启动时取消项目中的“方法断点”。但是,可悲的是,并不是大多数文章都说只管去做。但它并没有告诉我为什么它会那样做。我很想知道为什么会有这个坑,因为我还是用了很多方法断点,关键是我在使用过程中完全没有注意到这个坑。“方法断点”还是很实用的,比如我随便举的例子。之前写事务相关文章的时候提到过这样一个方法:java.sql.Connection#setAutoCommitsetAutoCommit这个方法有几个实现类,不知道用哪个:所以,在调试的时候,可以使用如下在这个接口上打个断点:然后重启程序,IDEA会自动帮你判断去哪个实现类:但需要注意的是,并不是所有的方法断点都会导致启动慢。至少在当地是这样的。当我在Mapper的接口上添加方法断点时,这个问题可以稳定重现:在项目的其他方法上添加方法断点时,没有必要,只是偶尔出现这个问题。另外,其实,当你以方法断点的Debug模式启动时,IDEA会弹出这个提醒,告诉你方法断点会导致Debug变慢:但是,一个真正的男人,千万不要看这个提醒。反正我就是直接无视了,完全没在意弹窗的内容。至于为什么要在Mapper的接口上设置方法断点?都是我的错,好吧。为什么在寻找答案的过程中,找到了这个idea的官方社区链接:intellij-support.jetbrains.com/hc/en-us/ar...这个帖子是JetBrainsTeam发布的,关于可能性调试功能可能会导致性能下降问题。在这篇文章中,第一个性能点是方法断点。官方是怎么解释这个问题的?我会为你翻译。由于JVM设计,方法断点会大大降低调试器的速度,它们的评估成本很高。Evaluate,一个四级的词,好好记住,考试会考:大概意思就是你要使用方法断点功能。在启动过程中,它涉及到“评估”断点的成本。代价是启动缓慢。如何解决这个“考核”的成本?官方的解决方案很简单粗暴:不使用方法断点,那不是没有成本吗?So,Remove,done:移除方法断点并考虑使用常规行断点。删除方法断点并考虑使用常规行断点。官方还是很贴心的,怕你不知道怎么Remove,专门加了一句:Toverifythatyoudon'thaveanymethodbreakpointsopen.idea/workspace.xmlfileintheprojectrootdirectory(or.iws文件(如果您使用的是旧项目格式)并在method_breakpoints节点内查找任何断点。可以使用下面的方法来验证是否打开了方法断点。只要进入.idea/workspace.xml文件,找到节点method_breakpoints,有的话去掉。然后查看我项目中对应的文件,并没有找到method_breakpoints关键字,但是找到了下面这个。应该是文档改了。问题不大。无论如何,它具有相同的含义。其实官方的方法虽然高了一点,但是我之前给出的操作更简单:针对“为什么”这个问题。在这里,官方的回答特别模糊:因为JVM的设计。别问,是JVM的设计问题。我觉得这不是我想要的答案,幸好我在这个帖子下找到了一个“好人”写的回复:这个好人叫Gabi,我看到他回复的第一句话“我做了一些research”,就知道这一波稳了,找对地方了,答案一定藏在他附上的链接里。嘎比老铁说:兄弟们,这个方法下断点慢的原因我研究过了。研究报告在这里:www.smartik.net/2017/11/met...他甚至还附带了一个总结:长话短说,长话短说。他太可爱了,我哭死了。他首先指出了问题的根本原因:似乎根本问题是MethodBreakpoints是使用JDPA的MethodEntry&MethodExit特性实现的。.有同学会问,JDPA,是什么?这是一个婴儿:docs.oracle.com/javase/8/do…JPDA,JavaPlatformDebuggerArchitecture的缩写。IDEA中的各种Debug功能都是基于这个小工具实现的。不懂不要紧,这个东西面试不考,只要你知道你这边有这个技术就行。然后,他用四个any完成了跳过语句四笔:这个实现要求JVM在每次任何线程进入任何方法和任何线程退出任何方法时都触发一个事件。此实现要求JVM每次在任何(any)线程进入任何(any)方法时以及任何(any)线程退出任何(any)方法时触发Event。伙计们,这不就是AOP吗?说到这里,我明白为什么方法断点的性能这么差了。触发那么多事件进出方法不会花那么多时间吗?具体细节,在他之前提到的研究报告中,写的很清楚。如果你对细节感兴趣,可以查阅和阅读他的报告。对了,他的报告名字也挺唬人的:MethodBreakpointsareEvil。让我告诉你两个关键点。第一个是关于MethodEntry&MethodExit:IDE在其内部方法断点列表中添加断点事件,通知通过整个链条转发给IDE,IDE检查其方法断点列表是否包含当前方法。如果找到,说明这个方法上有方法断点,IDE会发送SetBreakpoint请求给VM设置断点。否则,VM的线程会被释放,什么也不会发生。这里有一个稍微具体一点的操作,类似于我前面提到的AOP。核心意思就一句话:触发的事件太多,导致性能下降严重。第二个重点是:文末给出五个结论:方法断点是IDE的特性,不是JPDA的特性。方法断点真的很邪恶,邪恶的方法断点会极大的影响调试器只有在真正需要的时候才使用它们如果必须使用方法作为断点,可以考虑关闭方法退出事件前四点没什么好说的。最后一点:考虑关闭方法退出事件。验证这一点非常简单。您可以通过右键单击方法断点来查看此选项。MethodEntry&MethodExit都是默认选中的:所以我在本地用一个随机项目验证了它。开启MethodExit事件,启动耗时113.244秒。关闭MethodExit事件,启动时间:46.754秒。别告诉我,它真的很有用。现在我大概知道为什么方法断点这么慢了。这真的不是BUG,而是一个特性。至于方法断点的问题,顺便搜了一下社区,最早追溯到2008年:这位老哥说他调试Web程序的速度太慢了,根本用不上。他的项目只启用了行断点,没有方法断点。请老板帮他看看。然后大佬帮他分析也找不到原因。他自己也很疑惑,说:我什么都没动,好奇怪。有时这些东西有效,有时却无效。就像一句经典台词:但问题最终还是解决了。如何解决?他自己说:确实有方法断点,他不知道怎么打这个断点。也许他和我一样在握手。意想不到的收获在之前出现的官方帖子的底部,有两个这样的链接:指向这个地方:www.jetbrains.com/help/idea/d...我打开这部分链接看了看,经过鉴定,这真是一件好事。这是官方的手把手教程,教你如何使用Debug模式。之前看的一些调试技巧相关的文章,原文是从官方这里翻译过来的。我这里举两个例子,可以作为一个指导。强烈建议那些在调试程序时只知道下一步基本操作和跳过当前断点的同学仔细阅读并动手做。.首先是这个:DebuggingStreamsforJava。官方给出了调试代码示例,我做了一点微调,贴上去就可以运行了: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));,大于50的质数。很明显,非质数91在isPrime方法中做了特殊处理,导致程序最终输出的是91,这是一个bug。虽然这个BUG一目了然,但是不要笑,憋着,假装不知道为什么。现在我们需要通过调试找到错误。断点设置在这个位置:在Debug模式下运行时,有这样一个图标:点击之后,会出现这样一个弹窗:上图是程序对应的各个方法的调用顺序,调用completion之后的输出是什么。下图框出的“FlatMode”点击后是这样的:最右边,是过滤后的输出结果。其中包含数字91:点击这个“91”,发现经过第一次过滤后,91的数据还在。意思是这里有问题。而这个地方就是上面提到的对“91”做了特殊处理的isPrime方法。这样可以有针对性地分析方法,缩小问题排查范围。这个功能怎么说呢,反正我的评论是:总之,以上就是一个IDEA调试Streams流程的简单例子。然后演示一个并发相关的:官方给出了这样一个例子:publicclassConcurrencyTest{staticfinalLista=Collections.synchronizedList(newArrayList());publicstaticvoidmain(String[]args){Threadt=newThread(()->addIfAbsent(17));t.开始();添加如果不存在(17);t.join();System.out.println(a);}privatestaticvoidaddIfAbsent(intx){if(!a.contains(x)){a.add(x);根据addIfAbsent方法,如果列表中存在要添加的元素,则不添加。你是说这个程序是线程安全的吗?当然不是。想一想,先判断,再添加,经典的非原子操作。但是如果拿这个程序直接运行,就不容易跑出线程不安全的场景:怎么办?调试在这里可以帮助您做到这一点。在这里打个断点,然后右击断点,选择“Thread”:程序运行时,主线程和异步线程都会停在这里:可以通过drop-选择Debug主线程或者异步线程“框架”中的下拉框。异步线程。由于两个线程都执行了add方法,所以最后的输出是这样的:这个线程不是不安全吗?即使知道这个地方不是线程安全的,如果没有Debug帮助调试,还是很难通过程序输出来验证。毕竟多线程的问题在大多数情况下并不是每次都必须出现的问题。定位到问题后,官方也给出了正确的代码片段:好了,就是一个引导,基本的操作就是这些。还是那句话,感兴趣的可以自己去看,跟着案例操作。就算是看到有人把玩Debug源码,也无非就是这么些基本操作的组合。回过头来,让我们回到官方帖子《关于Debug功能可能导致的性能低下》:当我看到方框里框起来的“Collections类”和“toString()”方法时,眼泪都快下来了。刚开始写文章的时候,曾经被这个东西坑过。三年前,也就是2019年,我写这篇文章《这道Java基础题真的有坑!我也没想到还有续集。》我在调试ArrayList的时候遇到了一个问题。曾经以为自己被质子打扰了:一句话,程序在单线程的情况下直接运行结果和Debug输出的不一样。当时,我很纳闷。直到8个月后,当我在写这篇《JDK的BUG导致的内存溢出!反正我是没想到还能有续集》的文章时,才机缘巧合地找到了我的问题的答案。根本原因是在Debug模式下,IDEA会自动触发集合类的toString方法。但是在某些集合类的toString方法中,会存在修改头节点等逻辑,导致程序运行结果与预期不匹配。那就是对应这句话:翻译过来就是:老铁,请注意,如果toString方法中的代码改变了程序的状态,在debug状态下运行时,这些方法也可以改变应用程序的运行结果。最后的解决办法是关闭IDEA的这两个配置:同时我在官方文档中也找到了对这两个配置的解释:www.jetbrains.com/help/idea/c...主要是为了调试process以更友好的形式显示集合类。你是什??么意思?让我给你举个例子。ThisisthemapcollectionlikeinDebugmodewiththeconfigurationabovementionednotchecked:ThisisthemapcollectionlikeinDebugmodedebugmodeafterchecked:显然,勾选后的外观更友好。