Java虚拟机JVM性能优化(三):垃圾收集详解

Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源。在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制。她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的)。

垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java虚拟机动态内存管理机制的核心部分。在一个典型的垃圾收集周期里,所有仍然被引用的对象(因此是可达的)都将被保留,而那些不再被引用的对象将被释放、其所占用的空间将被回收用来分配给新的对象。

为了理解垃圾收集机制和各种垃圾收集算法,首先需要知道关于Java平台内存模型的一些知识。

垃圾收集和Java平台内存模型

当用命令行启动一个Java程序并指定启动参数-Xmx时(例如:java -Xmx:2g MyApp),指定大小的内存就分配给了Java进程,这就是所谓的Java堆。这个专用的内存地址空间用于存储Java程序(有时是JVM)所创建的对象。随着应用程序运行并不断为新对象分配内存,Java堆(即专门的内存地址空间)就会慢慢被填满。

最终Java堆会被填满,也就是说内存分配线程找不到一块足够大的连续空间为新对象分配内存,这时JVM决定要通知垃圾收集器并启动垃圾收集。垃圾收集也可以通过在程序中调用System.gc()来触发,但使用System.gc()并不能确保垃圾收集一定被执行。在任何一次垃圾收集之前,垃圾收集机制都会首先判断执行垃圾收集是否安全,当应用程序的所有活动线程都处于安全点时就可以开始执行一次垃圾收集。例如:当正在为对象分配内存时就不能执行垃圾收集,或者是正在优化CPU指令时也不能执行垃圾收集,因为这样很可能会丢失上下文从而搞错最终结果。

垃圾收集器不能回收任何一个有活动引用的对象,那将破坏Java虚拟机规范。也无需立即回收死对象,因为死对象最终还是会被后续的垃圾收集所回收。尽管有很多种垃圾收集的实现方法,但以上两点对所有垃圾收集实现都是相同的。垃圾收集真正的挑战在于如何识别对象是否存活以及如何在尽量不影响应用程序的情况下回收内存,因此垃圾收集器的目标有以下两个:

1.迅速释放没有引用的内存以满足应用程序的内存分配需要从而避免内存溢出。
2.回收内存时对正在运行的应用程序性能(延迟和吞吐量)的影响最小化。

两类垃圾收集

在本系列的第一篇中,我介绍了两种垃圾收集的方法,即引用计数和跟踪收集。接下来我们进一步探讨这两种方法,并介绍一些在生产环境中使用的跟踪收集算法。

引用计数收集器

引用计数收集器记录了指向每个Java对象的引用数,一旦指向某个对象的引用数为0,那么就可以立即回收该对象。这种即时性是引用计数收集器的主要优点,而且维护那些没有引用指向的内存几乎没有开销,不过为每个对象记录最新的引用数却是代价高昂的。

引用计数收集器的主要难点在于如何保证引用计数的准确性,另外一个众所周知的难点是如何处理循环引用的情况。如果两个对象彼此引用,而且没有被其他活动对象所引用,那么这两个对象的内存永远都不会被回收,因为指向这两个对象的引用数都不为0。对循环引用结构的内存回收需要major analysis(译者注:Java堆上的全局分析),这将增加算法的复杂性,从而也给应用程序带来额外的开销。

跟踪收集器

跟踪收集器基于这样的假设:所有的活动对象都可以通过一个已知的初始活动对象集合的迭代引用(引用以及引用的引用)找到。可以通过分析寄存器、全局对象和栈帧来确定初始活动对象集合(也被称为根对象)。确定了初始对象集合后,跟踪收集器顺着这些对象的引用关系依次将引用所指向的对象标注为活动对象,就这样已知的活动对象集合不断扩大。这一过程持续进行直到所有被引用的对象都被标注为活动对象,而那些没有被标注过的对象的内存就被回收。

跟踪收集器不同于引用计数收集器主要在于它可以处理循环引用结构。多数的跟踪收集器都是在标记阶段发现那些循环引用结构中的无引用对象。

跟踪收集器是动态语言中最常用的内存管理方式,也是目前Java中最常见的方式,同时在生产环境中也被验证了很多年。下面我将从实现跟踪收集的一些算法开始介绍跟踪收集器。

跟踪收集算法

复制垃圾收集器和标记-清除垃圾收集器并不是什么新东西,但它们仍然是目前实现跟踪收集的两种最常见算法。

复制垃圾收集器

传统的复制垃圾收集器使用堆中的两个地址空间(即from空间和to空间),当执行垃圾收集时from空间的活动对象被复制到to空间,当from空间的所有活动对象都被移出(译者注:复制到to空间或者老年代)后,就可以回收整个from空间了,当再次开始分配空间时将首先使用to空间(译者注:即上一轮的to空间作为新一轮的from空间)。

在该算法的早期实现中,from空间和to空间不断变换位置,也就是说当to空间满了,触发了垃圾收集,to空间就成为了from空间,如图1所示。

图1 传统的复制垃圾收集顺序

最新的复制算法允许堆内任意地址空间作为to空间和from空间。这样它们不需要彼此交换位置,而只是逻辑上变换了位置。

复制收集器的优点是在to空间被复制的对象紧凑排列,完全没有碎片。而碎片化正是其他垃圾收集器所面临的一个共同问题,也是我之后主要讨论的问题。

复制收集器的缺陷

通常来说复制收集器是stop-the-world的,也就是说只要垃圾收集在进行,应用程序就无法执行。对于这种实现来说,你需要复制的东西越多,对应用程序性能的影响就越大。对于那些响应时间敏感的应用来说这是个缺点。使用复制收集器时,你还要考虑最坏的场景(即from空间中的所有对象都是活动对象),这时你需要为移动这些活动对象准备足够大的空间,因此to空间必须大到可以装下from空间的所有对象。由于这个限制,复制算法的内存利用率稍有不足(译者注:在最坏的情况下to空间需要和from空间大小相同,所以只有50%的利用率)。

标记-清除收集器

部署在企业生产环境上的大多数商业JVM采用的都是标记-清除(或者叫标记)收集器,因为它没有复制垃圾收集器对应用程序性能的影响问题。其中最有名的标记收集器包括CMS、G1、GenPar和DeterministicGC。

标记-清除收集器跟踪对象引用,并且用标志位将每个找到的对象标记为live。这个标志位通常对应堆上的一个地址或是一组地址。例如:活动位可以是对象头的一个位(译者注:bit)或是一个位向量、一个位图。

在标记完成之后就进入了清除阶段。清除阶段通常都会再次遍历堆(不仅是标记为live的对象,而是整个堆),用来定位那些没有标记的连续内存地址空间(没有被标记的内存就是空闲并可回收的),然后收集器将它们整理为空闲列表。垃圾收集器可以有多个空闲列表(通常按照内存块的大小划分),有些JVM(例如:JRockit Real Time)的收集器甚至基于应用程序的性能分析和对象大小的统计结果来动态划分空闲列表。

清除阶段过后,应用程序就可以再次分配内存了。从空闲列表中为新对象分配内存时,新分配的内存块需要符合新对象的大小,或是线程的平均对象大小,或是应用程序的TLAB大小。为新对象找到大小合适的内存块有助于优化内存和减少碎片。

标记-清除收集器的缺陷

标记阶段的执行时间依赖于堆中活动对象的数量,而清除阶段的执行时间依赖于堆的大小。因此对于堆设置较大并且堆中活动对象较多的情况,标记-清除算法会有一定的暂停时间。

对于内存消耗很大的应用程序来说,你可以调整垃圾收集参数以适应各种应用程序的场景和需要。在很多情况下,这种调整至少推迟了标记阶段/清除阶段给应用程序或服务协议SLA(SLA这里指应用程序要达到的响应时间)带来的风险。但是调优仅仅对特定的负载和内存分配率有效,负载变化或是应用程序本身的修改都需要重新调优。

标记-清除收集器的实现

至少有两种已经在商业上验证的方法来实现标记-清除垃圾收集。一种是并行垃圾收集,另一种是并发(或者多数时间是并发)垃圾收集。

并行收集器

并行收集是指资源被垃圾收集线程并行使用。大多数并行收集的商业实现都是stop-the-world收集器,即所有的应用程序线程都暂停直到完成一次垃圾收集,因为垃圾收集器可以高效地使用资源,所以通常会在吞吐量的基准测试中得到高分,如SPECjbb。如果吞吐量对你的应用程序至关重要,那么并行垃圾收集器是一个很好的选择。

并行收集的主要代价(特别是对于生产环境)是应用程序线程在垃圾收集期间无法正常工作,就像复制收集器一样。因此那些对于响应时间敏感的应用程序使用并行收集器会有很大的影响。特别是在堆空间中有很多复杂的活动对象结构时,有很多的对象引用需要跟踪。(还记得吗标记-清除收集器回收内存的时间取决于跟踪活动对象集合的时间加上遍历整个堆的时间)对于并行方法来说,整个垃圾收集时间应用程序都会暂停。

并发收集器

并发垃圾收集器更适合那些对响应时间敏感的应用程序。并发意味着垃圾收集线程和应用程序线程并发执行。垃圾收集线程并不独占所有资源,因此需要决定何时开始一次垃圾收集,需要有足够的时间跟踪活动对象集合并在应用程序内存溢出前回收内存。如果垃圾收集没有及时完成,应用程序就会抛出内存溢出错误,另一方面又不希望垃圾收集执行时间太长因为那样会消耗应用程序的资源进而影响吞吐量。保持这种平衡是需要技巧的,因此在确定开始垃圾收集的时机以及选择垃圾收集优化的时机时都使用了启发式算法。

另一个难点在于确定何时可以安全执行一些操作(需要完整准确的堆快照的操作),例如:需要知道何时标记阶段完成,这样就可以进入清理阶段。对于stop-the-world的并行收集器来说这不成问题,因为世界已经暂停了(译者注:应用程序线程暂停,垃圾收集线程独占资源)。但对于并发收集器而言,从标记阶段立刻切换到清理阶段可能不安全。如果应用程序线程修改了一段内存,而这段内存已经被垃圾收集器跟踪并标注过了,这就可能产生了新的没有标注的引用。在一些并发收集实现中,这会使应用程序陷入长时间重复标注的循环,当应用程序需要这段内存时也无法获得空闲内存。

通过到目前为止的讨论我们知道有很多的垃圾收集器和垃圾收集算法,分别适合特定的应用程序类型和不同的负载。不仅是不同的算法,还有不同的算法实现。所以在指定垃圾收集器钱最好了解应用程序的需求以及自身特点。接下来我们将介绍Java平台内存模型的一些陷阱,这里陷阱的意思是,在动态变化的生产环境中Java程序员容易做出的一些使得应用程序性能变得更差的假设。

为什么调优无法代替垃圾收集

多数的Java程序员都知道如果要优化Java程序可以有很多选择。若干个可选的JVM、垃圾收集器和性能调优参数让开发者花费大量的时间在无休无尽的性能调优方面。这使有些人因此得出结论:垃圾收集是糟糕的,通过调优使垃圾收集较少发生或者持续时间较短是一个很好的变通办法,不过这样做是有风险的。

考虑一下针对具体应用程序的调优,多数的调优参数(例如内存分配率、对象大小、响应时间)都是基于当前测试的数据量对应用程序的内存分配率(译者注:或者其他参数)调整。最终可能造成以下两个结果:

1.在测试中通过的用例在生产环境中失败。
2.数据量的变化或者应用程序的变化要求重新调优。

调优是需要反复的,特别是并发垃圾收集器可能需要很多调优(尤其在生产环境中)。需要启发式方法来满足应用程序的需要。为了要满足最坏的情况,调优的结果可能是一个非常死板的配置,这也导致了大量的资源浪费。这种调优方法是一种堂吉诃德式的探索。事实上,你越是优化垃圾收集器来匹配特定的负载,越是远离了Java运行时的动态特性。毕竟有多少应用程序的负载是稳定的呢,你所预期的负载的可靠性又有多高呢?

那么如果你不将注意力放在调优上,能够做些什么来防止内存溢出错误和提高响应时间呢?首要的事情就是找到影响Java应用程序性能的主要因素。

碎片化

影响Java应用程序性能的因素不是垃圾收集器,而是碎片化以及垃圾收集器如何处理碎片化。所谓碎片化是这样一种状态:堆空间中有空闲可用的空间,但并没有足够大的连续内存空间,以至于无法为新对象分配内存。正如在第一篇中提到的,内存碎片要么是堆中残留的一段空间TLAB,要么是在长期存活对象中间被释放的小对象所占用的空间。

随着时间的推移和应用程序的运行,这些碎片就会遍布在堆中。在某些情况下,使用了静态化调优的参数可能会更糟,因为这些参数无法满足应用程序的动态需要。应用程序无法有效利用这些碎片化的空间。如果不做任何事情,那么将导致接连不断的垃圾收集,垃圾收集器尝试释放内存分配给新对象。在最坏的情况下,即使是接连不断的垃圾收集也无法释放更多的内存(碎片太多),然后JVM不得不抛出内存溢出的错误。你可以通过重启应用程序来解决碎片化,这样Java堆就有连续的内存空间可以分配给新对象。重启程序导致宕机,而且一段时间后Java堆将再次充满碎片,不得不再次重启。

内存溢出错误会挂起进程,日志显示垃圾收集器正在超负荷工作,这些都显示垃圾收集正试图释放内存,也表明堆中碎片很多。一些程序员会试图通过再次优化垃圾收集器来解决碎片化问题。但我认为应该寻找更有新意的办法解决这个问题。接下来的部分将重点讨论解决碎片化的两个办法:分代垃圾收集和压缩。

分代垃圾收集

你可能听过这样的理论:在生产环境中绝大多数对象的存活时间都很短。分代垃圾收集正是由这一理论衍生出的一种垃圾收集策略。在分代垃圾收集中,我们将堆分为不同的空间(或者叫做代),每个空间中保存着不同年龄的对象,所谓对象的年龄就是对象存活的垃圾收集周期数(也就是该对象多少个垃圾收集周期后仍然被引用)。

当新生代没有剩余空间可分配时,新生代的活动对象会被移动到老年代中(通常只有两个代。译者注:只有满足一定年龄的对象才会被移动到老年代),分代垃圾收集常常使用单向的复制收集器,一些更现代的JVM新生代中使用的是并行收集器,当然也可以为新生代和老年代分别实现不同的垃圾收集算法。如果你使用并行收集器或复制收集器,那么你的新生代收集器就是一个stop-the-world的收集器(参见之前的解释)。

老年代分配给那些从新生代移出的对象,这些对象要么是被引用很长一段时间,要么是被一些新生代中对象集合所引用。偶尔也有大对象直接被分配到了老年代,因为移动大对象的成本相对较高。

分代垃圾收集技术

在分代垃圾收集中,老年代运行垃圾收集的频率较低,而在新生代运行垃圾收集的频率较高,而我们也希望在新生代中垃圾收集周期更短。在极少的情况下,新生代的垃圾收集可能会比老年代的垃圾收集更频繁。如果你将新生代设置的太大时并且应用程序中的多数对象都存活较长时间,这种情况就可能会发生。在这种情况下,如果老年代设置的太小以至于无法容纳所有的长时间存活的对象,老年代的垃圾收集也会挣扎于释放空间给那些被移动进来的对象。不过通常来说分代垃圾收集可以使应用程序获得更好的性能。

划分出新生代的另一个好处是某种程度上解决了碎片化问题,或者说将最坏的情况推迟了。那些存活时间短的小对象本来可能产生碎片化问题,但都在新生代的垃圾收集中被清理了。由于存活时间长的对象被移到老年代时被更紧凑的分配空间,老年代也更加紧凑了。随着时间推移(如果你的应用运行时间足够长),老年代也会产生碎片化,这时需要运行一次或是几次完全垃圾收集,同时JVM也有可能抛出内存溢出错误。但是划分出新生代推迟了出现最坏情况的时间,这对于很多应用程序来说已经足够了。对于多数应用程序而言,它的确降低了stop-the-world垃圾收集的频率和内存溢出错误的机会。

优化分代垃圾收集

正如之前提到的,使用分代垃圾收集带来了重复的调优工作,例如调整新生代大小、提升率等。我无法针对具体应用运行时来强调怎样做取舍:选择固定的大小固然可以优化应用程序,但同时也减少了垃圾收集器应对动态变化的能力,而变化是不可避免的。

对于新生代首要原则就是在确保stop-the-world垃圾收集期间延迟时间前提下尽可能的加大,同时也要为那些长期存活的对象在堆中保留足够大的空间。下面是在调整分代垃圾收集器时要考虑的一些额外因素:

1.新生代中多数都是stop-the-world垃圾收集器,新生代设置的越大,相应的暂停时间就越长。因此对于那些受垃圾收集暂停时间影响大的应用程序来说,要仔细考虑将新生代设置为多大合适。

2.可以在不同的代上使用不同的垃圾收集算法。例如在新生代中使用并行垃圾收集,在老年代中使用并发垃圾收集。

3.当发现频繁的提升(译者注:从新生代移动到老年代)失败时说明老年代中碎片太多了,也就是说老年代中没有足够的空间来存放从新生代移出的对象。这时你可以调整一下提升率(即调整提升的年龄),或者确保老年代中的垃圾收集算法会进行压缩(将在下一段讨论)并调整压缩以适应应用程序的负载。也可以增加堆大小和各个代大小,但是这样更会进一步延长老年代上的暂停时间。要知道碎片化是无法避免的。

4.分代垃圾收集最适合这样的应用程序,他们有很多存活时间很短的小对象,很多对象在第一轮垃圾收集周期就被回收了。对于这种应用程序分代垃圾收集可以很好的减少碎片化,并将碎片化产生影响的时机推迟。

压缩

尽管分代垃圾收集延迟了出现碎片化和内存溢出错误的时间,然而压缩才是真正解决碎片化问题的唯一办法。压缩是指通过移动对象来释放连续内存块的垃圾收集策略,这样通过压缩为创建新对象释放了足够大的空间。

移动对象并更新对象引用是stop-the-world操作,会带来一定的消耗(有一种情况例外,将在本系列的下一篇中讨论)。存活的对象越多,压缩造成的暂停时间就越长。在剩余空间很少并且碎片化严重的情况下(这通常是因为程序运行了很长的时间),压缩存活对象较多的区域可能会有几秒种的暂停时间,而当接近内存溢出时,压缩整个堆甚至会花上几十秒的时间。

压缩的暂停时间取决于需要移动的内存大小和需要更新的引用数量。统计分析表明堆越大,需要移动的活动对象和更新的引用数量就越多。每移动1GB到2GB活动对象的暂停时间大约是1秒钟,对于4GB大小的堆很可能有25%的活动对象,因此偶尔会有大约1秒的暂停。

压缩和应用程序内存墙

应用程序内存墙是指在垃圾收集产生的暂停(例如:压缩)前可以设置的堆大小。根据系统和应用的不同,大多数的Java应用程序内存墙都在4GB到20GB之间。这也是多数的企业应用都是部署在多个较小的JVM上,而不是少数较大的JVM上的原因。让我们考虑一下这个问题:有多少现代企业的Java应用程序设计、部署是根据JVM的压缩限制来定义的。在这种情况下,为了绕过整理堆碎片的暂停时间,我们接受了更耗费管理成本的多个实例部署方案。考虑到现在硬件的大容量存储能力和企业级Java应用对增加内存的需求,这就有点奇怪了。为什么为每个实例只设置了几个GB的内存。并发压缩将会打破内存墙,这也是我下一篇文章的主题。

总结

本文是一篇关于垃圾收集的介绍性文章,帮助你了解有关垃圾收集的概念和机制,并希望能够促使你进一步阅读相关文章。这里讨论的很多东西都已经存在了很久,在下一篇中将介绍一些新的概念。例如并发压缩,目前是由Azul‘s Zing JVM实现的。它是一项新兴的垃圾收集技术,甚至尝试重新定义Java内存模型,特别是在今天内存和处理能力都不断提高的情况下。

以下是我总结出的一些关于垃圾收集的要点:

1.不同的垃圾收集算法和实现适应不同的应用程序需要,跟踪垃圾收集器是商业Java虚拟机中使用的最多的垃圾收集器。

2.并行垃圾收集在执行垃圾收集时并行使用所有资源。它通常是一个stop-the-world垃圾收集器,因此有更高的吞吐量,但是应用程序的工作线程必须等待垃圾收集线程完成,这对应用程序的响应时间有一定影响。

3.并发垃圾收集在执行收集时,应用程序工作线程仍然在运行。并发垃圾收集器需要在应用程序需要内存之前完成垃圾收集。

4.分代垃圾收集有助于延迟碎片化,但无法消除碎片化。分代垃圾收集将堆分为两个空间,其中一个空间存放新对象,另一个空间存放老对象。分代垃圾收集适合有很多存活时间很短的小对象的应用程序。

5.压缩是解决碎片化的唯一方法。多数的垃圾收集器都是以stop-the-world的方式执行压缩的,程序运行时间越久,对象引用越是复杂,对象的大小越是分布不均匀都将导致压缩时间延长。堆的大小也会影响压缩时间,因为可能有更多的活动对象和引用需要更新。

6.调优有助于延迟内存溢出错误。但是过度调优的结果是僵化的配置。在通过试错的方式开始调优之前,要确保清楚生产环境的负载、应用程序的对象类型以及对象引用的特性。过于僵化的配置很可能无法应付动态负载,因此在设置非动态值时一定要了解这样做的后果。

本系列的下一篇是:深入探讨C4(Concurrent Continuously Compacting Collector)垃圾收集算法,敬请期待!

(全文完)

(0)

相关推荐

  • Java虚拟机JVM性能优化(二):编译器

    本文将是JVM 性能优化系列的第二篇文章(第一篇:传送门),Java 编译器将是本文讨论的核心内容. 本文中,作者(Eva Andreasson)首先介绍了不同种类的编译器,并对客户端编译,服务器端编译器和多层编译的运行性能进行了对比.然后,在文章的最后介绍了几种常见的JVM优化方法,如死代码消除,代码嵌入以及循环体优化. Java最引以为豪的特性"平台独立性"正是源于Java编译器.软件开发人员尽其所能写出最好的java应用程序,紧接着后台运行的编译器产生高效的基于目标平台的可执行代

  • 浅谈Java的虚拟机结构以及虚拟机内存的优化

    工作以来,代码越写越多,程序也越来越臃肿,效率越来越低,对于我这样一个追求完美的程序员来说,这是绝对不被允许的,于是除了不断优化程序结构外,内存优化和性能调优就成了我惯用的"伎俩". 要对Java程序进行内存优化和性能调优,不了解虚拟机的内部原理(或者叫规范更严谨一点)是肯定不行的,这里推荐一本好书<深入Java虚拟机(第二版)>(Bill Venners著,曹晓刚 蒋靖 译,实际上本文正是作者阅读本书之后,对Java虚拟机的个人理解阐述).当然了,了解Java虚拟机的好处

  • Java虚拟机装载和初始化一个class类代码解析

    在 java 应用程序开发中,只有被 java 虚拟机装载的 Class 类型才能在程序中使用.只要生成的字节码符合 java 虚拟机的指令集和文件格式,就可以在 JVM 上运行,这为 java 的跨平台性提供条件.下面,我们来看看虚拟机是如何装载和初始化一个 class 类的. 装载一个类 学习过C/C++语言的读者知道,C/C++源代码必须首先别编译成本地的机器代码,然后还需要一个链接代码过程.该链接过程的主要任务就是:合并不同的源码文件产出的中间代码,并最终获得一个可直接执行的应用程序.然

  • Java虚拟机最多支持多少个线程的探讨

    McGovernTheory在StackOverflow提了这样一个问题: Java虚拟机最多支持多少个线程?跟虚拟机开发商有关么?跟操作系统呢?还有其他的因素吗? Eddie的回答: 这取决于你使用的CPU,操作系统,其他进程正在做的事情,你使用的Java的版本,还有其他的因素.我曾经见过一台Windows服务器在宕机之前有超过6500个线程.当然,大多数线程什么事情也没有做.一旦一台机器上有差不多6500个线程(Java里面),机器就会开始出问题,并变得不稳定. 以我的经验来看,JVM容纳的

  • Java虚拟机JVM性能优化(一):JVM知识总结

    Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化.之后的文章会讲JVM性能优化,包括最新的JVM设计--支持当今高并发Java应用的性能和扩展. 如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法.我个人很喜欢学习新知识带来的这种感觉.我已经有过很多次这样

  • 了解Java虚拟机JVM的基本结构及JVM的内存溢出方式

    JVM内部结构图 Java虚拟机主要分为五个区域:方法区.堆.Java栈.PC寄存器.本地方法栈.下面 来看一些关于JVM结构的重要问题. 1.哪些区域是共享的?哪些是私有的? Java栈.本地方法栈.程序计数器是随用户线程的启动和结束而建立和销毁的, 每个线程都有独立的这些区域.而方法区.堆是被整个JVM进程中的所有线程共享的. 2.方法区保存什么?会被回收吗? 方法区不是只保存的方法信息和代码,同时在一块叫做运行时常量池的子区域还 保存了Class文件中常量表中的各种符号引用,以及翻译出来的

  • Java虚拟机JVM性能优化(三):垃圾收集详解

    Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源.在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制.她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的). 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java

  • java并发编程专题(三)----详解线程的同步

    有兴趣的朋友可以回顾一下前两篇 java并发编程专题(一)----线程基础知识 java并发编程专题(二)----如何创建并运行java线程 在现实开发中,我们或多或少的都经历过这样的情景:某一个变量被多个用户并发式的访问并修改,如何保证该变量在并发过程中对每一个用户的正确性呢?今天我们来聊聊线程同步的概念. 一般来说,程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价.如果程序并行化后, 连基本的执行结果的正确性都无法保证, 那么并行程序本身也就没有任何意义了.因此,

  • Java for循环常见优化方法案例详解

    目录 方法一:最常规的不加思考的写法 方法二:数组长度提取出来 方法三:数组长度提取出来 方法四:采用倒序的写法 方法五:Iterator 遍历 方法六:jdk1.5后的写法 方法七:循环嵌套外小内大原则 方法八:循环嵌套提取不需要循环的逻辑 方法九:异常处理写在循环外面 前言 我们都经常使用一些循环耗时计算的操作,特别是for循环,它是一种重复计算的操作,如果处理不好,耗时就比较大,如果处理书写得当将大大提高效率,下面总结几条for循环的常见优化方式. 首先,我们初始化一个集合 list,如下

  • IntelliJ IDEA 性能优化的教程详解

    idea打开的多了 内存占用也就多了 下边是亲试的优化ide性能的方法 1.设置JVM的启动参数: 进入idea的安装目录的bin文件夹 打开 idea.exe.vmoptions 文件, 修改-Xmx 的 值为2048m 打开 idea64.exe.vmoptions 文件, 修改-Xmx 的 值为2048m 打开idea.properties文件,找到idea.max.intellisense.filesize,默认是2500,改为25000(数值仅供参考,具体数值根据自己文件大小来定) 参

  • Webpack性能优化 DLL 用法详解

    前言 在用 Webpack 打包的时候,对于一些不经常更新的第三方库,比如 react,lodash,我们希望能和自己的代码分离开,Webpack 社区有两种方案 CommonsChunkPlugin DLLPlugin 对于 CommonsChunkPlugin,webpack 每次打包实际还是需要去处理这些第三方库,只是打包完之后,能把第三方库和我们自己的代码分开.而DLLPlugin 则是能把第三方代码完全分离开,即每次只打包项目自身的代码. 用法 要使用 DLLPlugin,需要额外新建

  • Java 虚拟机(JVM)之基本概念详解

    1.类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间. 2.方法区:就是存放类信息.常量信息.常量池信息.包括字符串字面量和数字常量等.方法区是辅助堆栈的块永久区,解决堆栈信息的产生,是先决条件. 3.Java堆:再java虚拟机启动的时候建立Java堆,它是java程序最主要的内存工作区域,几乎所有的对象实例都存放到Java堆中,堆空间是所有线程共享的.堆解决的是数据存储问题,即数据怎么放.放在哪儿. 4.直接内存:Java的NIO库允许Ja

  • 详解Java虚拟机(JVM)运行时

    JVM(Java虚拟机)是一个抽象的计算模型.就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域.目的是为构建在其上运行的应用程序提供一个运行环境.JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构.本文主要介绍Java虚拟机(JVM)运行时详解. 我们知道的JVM内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享. 线程不共享区域 每个线程的数据区域包括

  • Java 处理高并发负载类优化方法案例详解

    java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,java高负载数据) 一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF.尤其是Web2.0的应用,数据库的响应是首先要解决的. 一般来说MySQL是最常用的,可能最初是一个mysql主机,当数据增加到100万以上,那么,MySQL的效能急剧下降.常用的优化措施是M-S(主-从)方式进行同步复制,将查询和操作和分别在不同的服务器上进行操作.我推荐的是M-M-Slaves

随机推荐