很难相信Java居然能和C++一样快甚至还能更快一些 据我自己的实践这种说法确实成立然而我也发现许多关于速度的怀疑都来自一些早期的实现方式由于这些方式并非特别有效所以没有一个模型可供参考不能解释Java速度快的原因 我之所以想到速度部分原因是由于C++模型C++将自己的主要精力放在编译期间静态发生的所有事情上所以程序的运行期版本非常短小和快速C++也直接建立在C模型的基础上(主要为了向后兼容)但有时仅仅由于它在C中能按特定的方式工作所以也是C++中最方便的一种方法最重要的一种情况是C和C++对内存的管理方式它是某些人觉得Java速度肯定慢的重要依据在Java中所有对象都必须在内存堆里创建 而在C++中对象是在堆栈中创建的这样可达到更快的速度因为当我们进入一个特定的作用域时堆栈指针会向下移动一个单位为那个作用域内创建的以堆栈为基础的所有对象分配存储空间而当我们离开作用域的时候(调用完毕所有局部构建器后)堆栈指针会向上移动一个单位然而在C++里创建内存堆(Heap)对象通常会慢得多因为它建立在C的内存堆基础上这种内存堆实际是一个大的内存池要求必须进行再循环(再生)在C++里调用delete以后释放的内存会在堆里留下一个洞所以再调用new的时候存储分配机制必须进行某种形式的搜索使对象的存储与堆内任何现成的洞相配否则就会很快用光堆的存储空间之所以内存堆的分配会在C++里对性能造成如此重大的性能影响对可用内存的搜索正是一个重要的原因所以创建基于堆栈的对象要快得多 同样地由于C++如此多的工作都在编译期间进行所以必须考虑这方面的因素但在Java的某些地方事情的发生却要显得动态得多它会改变模型创建对象的时候垃圾收集器的使用对于提高对象创建的速度产生了显着的影响从表面上看这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成影响但它正是JVM采取的重要手段之一这意味着在Java中为堆对象分配存储空间几乎能达到与C++中在堆栈里创建存储空间一样快的速度 可将C++的堆(以及更慢的Java堆)想象成一个庭院每个对象都拥有自己的一块地皮在以后的某个时间这种不动产会被抛弃而且必须再生但在某些JVM里Java堆的工作方式却是颇有不同的它更象一条传送带每次分配了一个新对象后都会朝前移动这意味着对象存储空间的分配可以达到非常快的速度堆指针简单地向前移至处女地所以它与C++的堆栈分配方式几乎是完全相同的(当然在数据记录上会多花一些开销但要比搜索存储空间快多了) 现在大家可能注意到了堆事实并非一条传送带如按那种方式对待它最终就要求进行大量的页交换(这对性能的发挥会产生巨大干扰)这样终究会用光内存出现内存分页错误所以这儿必须采取一个技巧那就是着名的垃圾收集器它在收集垃圾的同时也负责压缩堆里的所有对象将堆指针移至尽可能靠近传送带开头的地方远离发生(内存)分页错误的地点垃圾收集器会重新安排所有东西使其成为一个高速无限自由的堆模型同时游刃有余地分配存储空间 为真正掌握它的工作原理我们首先需要理解不同垃圾收集器(GC)采取的工作方案一种简单但速度较慢的GC技术是引用计数这意味着每个对象都包含了一个引用计数器每当一个句柄同一个对象连接起来时引用计数器就会增值每当一个句柄超出自己的作用域或者设为null时引用计数就会减值这样一来只要程序处于运行状态就需要连续进行引用计数管理——尽管这种管理本身的开销比较少垃圾收集器会在整个对象列表中移动巡视一旦它发现其中一个引用计数成为就释放它占据的存储空间但这样做也有一个缺点若对象相互之间进行循环引用那么即使引用计数不是仍有可能属于应收掉的垃圾为了找出这种自引用的组要求垃圾收集器进行大量额外的工作引用计数属于垃圾收集的一种类型但它看起来并不适合在所有JVM方案中采用 在速度更快的方案里垃圾收集并不建立在引用计数的基础上相反它们基于这样一个原理所有非死锁的对象最终都肯定能回溯至一个句柄该句柄要么存在于堆栈中要么存在于静态存储空间这个回溯链可能经历了几层对象所以如果从堆栈和静态存储区域开始并经历所有句柄就能找出所有活动的对象对于自己找到的每个句柄都必须跟蹤到它指向的那个对象然后跟随那个对象中的所有句柄跟蹤追击到它们指向的对象……等等直到遍历了从堆栈或静态存储区域中的句柄发起的整个链接网路为止中途移经的每个对象都必须仍处于活动状态注意对于那些特殊的自引用组并不会出现前述的问题由于它们根本找不到所以会自动当作垃圾处理 在这里阐述的方法中JVM采用一种自适应的垃圾收集方案对于它找到的那些活动对象具体采取的操作取决于当前正在使用的是什么变体其中一个变体是停止和复制这意味着由于一些不久之后就会非常明显的原因程序首先会停止运行(并非一种后台收集方案)随后已找到的每个活动对象都会从一个内存堆复制到另一个留下所有的垃圾除此以外随着对象复制到新堆它们会一个接一个地聚焦在一起这样可使新堆显得更加紧凑(并使新的存储区域可以简单地抽离末尾就象前面讲述的那样) 当然将一个对象从一处挪到另一处时指向那个对象的所有句柄(引用)都必须改变对于那些通过跟蹤内存堆的对象而获得的句柄以及那些静态存储区域都可以立即改变但在遍历过程中还有可能遇到指向这个对象的其他句柄一旦发现这个问题就当即进行修正(可想象一个散列表将老地址映射成新地址) 有两方面的问题使复制收集器显得效率低下第一个问题是我们拥有两个堆所有内存都在这两个独立的堆内来回移动要求付出的管理量是实际需要的两倍为解决这个问题有些JVM根据需要分配内存堆并将一个堆简单地复制到另一个 第二个问题是复制随着程序变得越来越健壮它几乎不产生或产生很少的垃圾尽管如此一个副本收集器仍会将所有内存从一处复制到另一处这显得非常浪费为避免这个问题有些JVM能侦测是否没有产生新的垃圾并随即改换另一种方案(这便是自适应的缘由)另一种方案叫作标记和清除Sun公司的JVM一直采用的都是这种方案对于常规性的应用标记和清除显得非常慢但一旦知道自己不产生垃圾或者只产生很少的垃圾它的速度就会非常快 标记和清除采用相同的逻辑从堆栈和静态存储区域开始并跟蹤所有句柄寻找活动对象然而每次发现一个活动对象的时候就会设置一个标记为那个对象作上记号但此时尚不收集那个对象只有在标记过程结束清除过程才正式开始在清除过程中死锁的对象会被释放然而不会进行任何形式的复制所以假若收集器决定压缩一个断续的内存堆它通过移动周围的对象来实现 停止和复制向我们表明这种类型的垃圾收集并不是在后台进行的相反一旦发生垃圾收集程序就会停止运行在Sun公司的文档库中可发现许多地方都将垃圾收集定义成一种低优先级的后台进程但它只是一种理论上的实验实际根本不能工作在实际应用中Sun的垃圾收集器会在内存减少时运行除此以外标记和清除也要求程序停止运行 正如早先指出的那样在这里介绍的JVM中内存是按大块分配的若分配一个大块头对象它会获得自己的内存块严格的停止和复制要求在释放旧堆之前将每个活动的对象从源堆复制到一个新堆此时会涉及大量的内存转换工作通过内存块垃圾收集器通常可利用死块复制对象就象它进行收集时那样每个块都有一个生成计数用于跟蹤它是否依然存活通常只有自上次垃圾收集以来创建的块才会得到压缩对于其他所有块如果已从其他某些地方进行了引用那么生成计数都会溢出这是许多短期的临时的对象经常遇到的情况会周期性地进行一次完整清除工作——大块头的对象仍未复制(只是让它们的生成计数溢出)而那些包含了小对象的块会进行复制和压缩JVM会监视垃圾收集器的效率如果由于所有对象都属于长期对象造成垃圾收集成为浪费时间的一个过程就会切换到标记和清除方案类似地JVM会跟蹤监视成功的标记与清除工作若内存堆变得越来越散乱就会换回停止和复制方案自定义的说法就是从这种行为来的我们将其最后总结为根据情况自动转换停止和复制/标记和清除这两种模式 JVM还采用了其他许多加速方案其中一个特别重要的涉及装载器以及JIT编译器若必须装载一个类(通常是我们首次想创建那个类的一个对象时)会找到class文件并将那个类的字节码送入内存此时一个方法是用JIT编译所有代码但这样做有两方面的缺点它会花更多的时间若与程序的运行时间综合考虑编译时间还有可能更长而且它增大了执行文件的长度(字节码比扩展过的JIT代码精简得多)这有可能造成内存页交换从而显着放慢一个程序的执行速度另一种替代办法是除非确有必要否则不经JIT编译这样一来那些根本不会执行的代码就可能永远得不到JIT的编译 由于JVM对浏览器来说是外置的大家可能希望在使用浏览器的时候从一些JVM的速度提高中获得好处但非常不幸JVM目前不能与不同的浏览器进行沟通为发挥一种特定JVM的潜力要么使用内建了那种JVM的浏览器要么只有运行独立的Java应用程序 |