Java虚拟机的内存管理与垃圾收集——主要内容归纳

2018-09-23  本文已影响839人  JustBull

第1章 运行时区域介绍

  内存区域介绍:
    1.1 程序计数器: Program Counter Register
        是线程私有的内存,线程执行字节码的指示器,线程切换恢复到正确的位置需要程序计数器。程序计数器区域是没有OutOfMemoryError的区域。
        native方法是非Java语言写的方法,Java只是提供了接口,并没有具体的实现,实现是由其它语言实现的。
    
    1.2 Java的虚拟机栈 Java Virtual Machine Stacks
        是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法执行都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的执行过程就是栈帧的入栈到出栈的过程。
        栈的局部变量表存放了编译期可知的8种基本数据类型、对应引用、returnAddress类型(指向一条字节码指令的地址)。
        若线程请求栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果程序运行所需容量大于虚拟机的内存,将会抛出OutOfMemoryError异常。
        
    1.3 本地方法栈 Native Method Stack
        与虚拟机栈相似,区别是虚拟机栈为Java方法服务,本地方法栈为Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。 有的虚拟机会把本地方法栈和虚拟机栈合二为一。
        
    1.4 Java堆  Java Heap   GC堆
        是被所有线程共享的,是虚拟机管理内存最大的一块,在虚拟机启动时创建。所有的对象的实例及数组都要在堆上分配,但是随着技术的发展,现在也能在栈上分配和标量替换,也不是那么绝对了。
        GC堆采用的分类收集算法,GC堆分为新生代和年老代,再细致还可以分为 Eden空间、From Survivor空间、To Survivor空间. 共享的GC堆还可以划分出多个线程私有的分配缓冲区。
        
    1.5 方法区
        线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。
        原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束。但把方法区放到永久代不是一个好的做法,目前jdk 1.7的HotSpot已经把原来放在永久代的字符串常量池移出。
        
        1.6 运行时常量池
            是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池。
        
    1.7 直接内存
        Native堆外内存,通过存储在Java堆中的DirectByteBuffer 对象作为和堆外内存的交互,避免Java堆和Natice堆来回复制数据,提高性能。

第2章Java内存区域与内存溢出异常

  HotSpot 虚拟机
    2.1 对象的创建
        垃圾收集器是否有压缩整理功能--Java堆是否规整--分为 指针碰撞和空闲列表
        并发情况下给对象分配内存:1、同步处理,CAS配上失败重试的方式保证更新操作的原子性;2、把内存分配的动作按照线程分在不同的空间中进行,使用本地线程分配缓冲。
    2.2 对象的内存布局
        对象在内存中的布局分为以下3各部分:
            对象头:1、存储对象自身的运行时数据, 2、类型指针:确定对象属于哪个类
            实例数据:是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
            对齐填充:仅仅起着占位符的作用
        
        
        OutOfMemoryError的堆溢出,首先要区分是内存泄露还是内存溢出。
        虚拟机提供了参数来控制Java堆和方法区这两部分的最大值。操作系统为每个进程分配的分配的内存是有限的,内存量减去Xmx(最大堆内存)和MaxPernSize最大方法区容量,程序计数器消耗内存很小,可以忽略,虚拟机本身耗费的进程忽略,剩下的内存就虚拟机栈和本地方法栈瓜分了。
        
    2.3 方法区和u、运行时常量池溢出
        方法区用于存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述。

第3章 垃圾收集器与内存分配策略

    3.1 引用技术算法:每当有一个地方引用它时;计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
        主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
    3.2 可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GCRoots没有任何引用链相连(用图论的话来说,就是从GCRoots到这个对象不可达)时,则证明此对象是不可用的。
    
    3.3 在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为
        强引用(Strong Reference)
        软引用(Soft Reference)
        弱引用(Weak Reference)
        虚引用(Phantom Reference)   这4种引用强度依次逐渐减弱。

    3.4 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
        废弃常量:没有对象引用。
        无用的类:
            该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
            加载该类的ClassLoader已经被回收。
            该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    3.5 垃圾手机算法:
        标记-清除算法:标记之后清除,两个动作,且清除后内存碎片化,内存不连续,保存不了的大的对象等,所以存储前还需回收一次,效率不高。
        复制算法:把可用内存分为相同的两块,每次使用一块,清除时把某块存活的对象复制到另一块,效率较高,只是内存减少一半。
        
            现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。  

        标记整理算法:根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
        分代收集算法:堆划分为几块区域,新生代采用复制算法,老年代使用标记-清除或标记-整理 算法进行回收。
        
    3.6 HotSpot算法实现
        3.6.1 枚举根节点:
        3.6.2 安全点:安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
            抢先式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
            主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
        3.6.3 安全区域:安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,我们也可以把Safe Region看做是被扩展了的Safepoint。
        
        3.6.4 垃圾收集器:
            3.6.4.1 Serial收集器:一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
                    优点:它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
            3.6.4.2 ParNew收集器:是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
                    论垃圾收集器的上下文语境中,它们可以解释如下。
                    ·并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
                    ·并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
            3.6.4.3 Parallel Scavenge收集器:
                    在一些方面,类似ParNew收集器。
                    不同于其它收集器的地方,更关注于达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
                    Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
                    Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GCErgonomics)。
                    
            3.6.4.4 Parallel Old收集器:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。
            
            3.6.4.5 Serial Old(MSC)收集器:Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器。新生代使用复制算法,暂停所有用户进程;老年代使用标记-整理算法,暂停所有用户进程。
            
            3.6.4.6 CMS收集器:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
                    收集过程:
                        初始标记:仅仅只是标记一下GCRoots 能直接关联到的对象,速度很快.
                        并发标记:进行GCRoets  Tracing的过程.
                        重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
                        并发清除:
                    CMS收集器的不足:
                        CMS收集器对CPU资源非常敏感。默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。
                        CMS收集器无法处理浮动垃圾(Floating Garbage),由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CM SInitiatingOccupancyFraction 设置得太高很容易导致大量“Concurent Mode Failure”失败,性能反而降低。
                        CMS是一款基于“标记一清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的FullGC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

            3.6.4.7 G1收集器:
                    G1是一款面向服务端应用的垃圾收集器。 
                        优点:并发与并行
                              分代收集:
                              空间整合:标记-整理收集
                              可预测的停顿:建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
                              
                        使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
                        Gl收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最天的Region(这也就是Garbage-First名称的来由)。这种使用Regio-划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限的时间内可以获取尽可能高的收集效率。
                        在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
                        
                        如果不计算维护Remembered Set的操作,Gl收集器的运作大致可划分为以下几个步骤:
                            初始标记(Initial Marking)
                            并发标记(Concurrent Marking)
                            最终标记(Final Marking)
                            筛选回收(Live Data Counting and Evacuation)
                        
    3.7、内存分配与回收策略
      常见配置汇总 
        堆设置 
            -Xms:初始堆大小 
            -Xmx:最大堆大小 
            -Xmn:年轻代大小 eden+ 2 survivor space
            -XX:NewSize=n:设置年轻代大小 
            -XX:NewRatio=n:设置年轻代和年老代的比值.如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 
            -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值.注意Survivor区有两个.如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 
            -XX:MaxPermSize=n:设置持久代大小
        收集器设置 
            -XX:+UseSerialGC:设置串行收集器 
            -XX:+UseParallelGC:设置并行收集器 
            -XX:+UseParalledlOldGC:设置并行年老代收集器 
            -XX:+UseConcMarkSweepGC:设置并发收集器
        垃圾回收统计信息 
            -XX:+PrintGC 
            -XX:+PrintGCDetails 
            -XX:+PrintGCTimeStamps 
            -Xloggc:filename
        并行收集器设置 
            -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数.并行收集线程数. 
            -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 
            -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比.公式为1/(1+n)
        并发收集器设置 
            -XX:+CMSIncrementalMode:设置为增量模式.适用于单CPU情况. 
            -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数.并行收集线程数.


    
        分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
        
        对象优先在Eden区分配:
            当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
        大对象直接进入老年代:
            最典型的大对象就是很长的字符串及数组,不要随意创建“朝生夕死”的大对象。
                XX:PretenureSize Threshold参数,令大于这个设置值的对象直接在老年代分配。   
                    
        动态对象年龄判断:
            为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到Max Tenuring Threshold中要求的年龄。
                    
        空间分配担保:
            JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Ful GC。
                    
    内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为。因此,学习虚拟机内存知识,如果要到实践调优阶段,那么必须了解每个具体收集器的行为、优势和劣势、调节参数。                

第4章 虚拟机性能监控与故障处理工具

    4.1 概述
        给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)等。
    
    4.2 JDK的命令行工具
        SunJDK监控和故障处理工具
            jps     JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
            jstat   JVM Statistics Monitoring Tool,用于收集HotSpot 虚拟机各方面的运行数据
                    它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
                    jstat命令格式为:
                        jstat [ option vmid [interval[slms] [count]]]
                            参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。
                            option代表着用户希望查询的虚拟机信息,主要分为3类:类装载、垃圾收集、运行期编译状况.
                            
            jinfo   Configuration Info for Java,显示虚拟机配置信息
                    jinfo 命令格式:
                        jinfo [ option ] pid
            jmap    Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)
                    jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
                    命令格式:jmap [ option ] vmid
                    
            jhat    JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
                    
            jstack  Stack Trace for Java,显示虚拟机的线程快照
                    线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做了什么事情,等待了什么资源。
                    jstack命令格式:
                        jstack [ option ] vmid

            HSDIS:JIT生成代码反汇编
                
    4.3 JDK的可视化工具:
        4.3.1 JConsole:Java 可视化监视与管理控制台
        
        4.3.2 VisualVM:多合一故障处理工具
                BTrace 动态日志跟踪:在不停止目标程序运行的前提下,通过HotSpot 虚拟机的HotSwap技术e动态加入原本并不存在的调试代码。
                    HotSwap技术:代码热替换技术,HotSpot虚拟机允许在不停止运行的情况下,更新已经加载的类的代码。

第5章 调优案例分析与实战

    5.2案例分析
        5.2.1 高性能硬件上的程序部署策略
            控制Full GC的频率低,且在用于访问量少的时候执行Full GC.
            相同程序在64位JDK消耗的内存一般比32位JDK大,这是由于指针膨胀,以及数据类型对齐补白等因素导致的。
            
            解决方案:介绍完这两种部署方式,再重新回到这个案例之中,最后的部署方案调整为建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了10GB内存。另外建立一个Apache服务作为前端均衡代理访问门户。考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度较低,因此改为CMS收集器进行垃圾回收。部署方式调整后,服务再没有出现长时间停顿,速度比硬件升级前有较大提升。
        
        5.2.2集群间同步导致的内存溢出           
            要注意频繁的写操作和异常产生的写操作的堆积。

        5.2.3 堆外内存导致的溢出错误
            从实践经验的角度出发,除了Java堆和永久代之外,我们注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。
            口Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError 或者OutOfMemoryError:Direct buffer memory。
            口线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowEror(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread
            (横向无法分配,即无法建立新的线程)。
            口Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。
            口JN1代码:如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。
            口虚拟机和GC:虚拟机、GC的代码执行也要消耗一定的内存。
            
        5.2.4 外部命令导致系统缓慢
            调用外部脚本,导致虚拟机内复制了一个和当前虚拟机一样的环境变量进程,再用这个进程执行了外部命令,这个消耗很大。解决办法:去掉调用shell脚本,改为使用Java的api 调用。
        
        5.2.5 服务器JVM进程崩溃
            调用其它系统,其它系统的接口未修复,会存在调用等待的线程和socket连接越来越多,导致虚拟机崩溃。
            解决:通知其它系统修复接口,改为生产者/消费者模式的消息队列实现,系统恢复正常。
        
        5.2.6 不恰当的数据结构导致内存占用过大
        5.2.7 由Windows虚拟内存导致的长时间停顿   
            在Java的GUI程序中要避免这种现象,可以加入参数“-Dsun.awt.keepWorkingSetOnMinimize=true”来解决。
            
    5.3 Eclipse运行速度调优
        5.3.1 调优前的程序运行状态
            客观地说,由于机器硬件还不错(请读者以2010年普通PC机的标准来衡量),15秒的启动时间其实还在可接受范围以内,但是从VisualGC中反映的数据来看,主要问题是非用户程序时间(图5-2中的Compile Time、Class Load Time、GCTime)非常之高,占了整个启动过程耗时的一半以上(这里存在少许夸张成分,因为如JIT编译等动作是在后台线程完成的,用户程序在此期间也正常执行,所以并没有占用了一半以上的绝对时间)。虚拟机后台占用太多时间也直接导致Eclipse在启动后的使用过程中经常有不时停顿的感觉,所以进行调优有较大的价值。
            
        5.3.2 升级JDK1.6的性能变化及兼容问题
            JDK1.5相对于JDK1.6运行时,多出来的一条正好就是设置永久代最大容量的-XX:MaxPermSize=256M
            当launcher——也就是Windows下的可执行程序eclipse.exe,检测到假如是Eclipse运行在Sun公司的虚拟机上的话,就会把参数值转化为-XX:MaxPermSize传递给虚拟机进程,因为三大商用虚拟机中只有Sun系列的虚拟机才有永久代的概念。
            在2009年4月20日,Oracle公司正式完成了对Sun公司的收购,此后无论是网页还是具体程序产品,提供商都从Sun变为了Oracle,而eclipse.exe就是根据程序提供商判断是否为Sun的虚拟机,当JDK1.6Update 21中java.exe、javaw.exe的“Company”属性从“Sun Microsystems Inc.”变为“Oracle Corporation”之后,Eclipse就完全不认识这个虚拟机了,因此没有把最大永久代的参数传递过去。
            了解原因之后,解决方法就简单了,launcher不认识就只好由人来告诉它,即在eclipse.ini中明确指定-XX:MaxPermSize=256M这个参数就可以了。
        5.3.3 编译时间和类加载时间的优化
            通过参数-Xverify:none禁止掉字节码验证过程也可作为一项优化措施。
            我们知道Java语言为了实现跨平台的特性,Java代码编译出来后形成的Class文件中存储的是字节码(ByteCode),虚拟机通过解释方式执行字节码命令,比起C/C++编译成本地二进制代码来说,速度要慢不少。为了解决程序解释执行的速度问题,JDK1.2以后,虚拟机内置了两个运行时编译器,如果一段Java方法被调用次数达到一定程度,就会被判定为热代码交给JIT编译器即时编译为本地代码,提高运行速度(这就是HotSpot虚拟机名字的由来)。
            所以Java程序只要代码没有问题(主要是泄漏问题,如内存泄漏、连接泄漏),随着代码被编译得越来越彻底,运行速度应当是越运行越快的。
            Java的运行期编译最大的缺点就是它进行编译需要消耗程序正常的运行时间,这也就是上面所说的“编译时间”。
        5.3.4 调整内存设置控制垃圾收集频率    
            新生代GC频繁发生,很明显是由于虚拟机分配给新生代的空间太小而导致的
        5.3.5 选择收集器降低延迟
            Eclipse应当算是与使用者交互非常频繁的应用程序,由于代码太多,笔者习惯在做全量编译或者清理动作的时候,使用“Runin Backgroup”功能一边编译一边继续工作回顾一下在第3章提到的几种收集器,很容易想到CMS是最符合这类场景的收集器。因此尝试在eclipse.in-中再加人这两个参数-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC-(ParNew收集器是使用CMS收集器后的默认新生代收集器,写上仅是为了配置更加清晰),要求虚拟机在新生代和老年代分别使用ParNew和CMS收集器进行垃圾回收。指定收集器之后,再次测试的结果如图5-13所示,与原来使用串行收集器对比,新生代停顿从每次65毫秒下降到了每次53毫秒,而老年代的停顿时间更是从725毫秒大幅下降到了36毫秒。
            
    5.4本章小结
        以上讲解了Java虚拟机的内存管理与垃圾收集是虚拟机结构体系中最重要的组成部分,对程序的性能和稳定性存非常大的影响,在本书的第2~5章中,主要讲解了从理论知识、异常现象、代码、工具、案例、实战等几个方面。
        关于虚拟机内存管理部分到此为止就结束了,后面将开始介绍Class文件与虚拟机执行子系统方面的知识。
上一篇下一篇

猜你喜欢

热点阅读