JVM Optimization

  • 架构图

  • 基本概念说明
    • 堆(heap):数据存储,对象实例;空间往上增长,线程共享区;大小可通过-Xmx和-Xms配置
      • 新生代(Young Generation):划分为Eden Space和两个Survivor(From Space 和To Space),new object首先存放在Eden区,Eden空间不足,进行GC,将存活的对象放到From Space;如果From Space已满,再进行GC,将存活的对象放到To Space;如果To Space已满,则再次GC,将存活的对象放入旧生代(Old Generation);可用-XX:survivorRatio控制Eden和Survivor的比例(默认值为8,即Eden占8/10, 另两个survivor各占1/10),或-Xmn配置新生代;-XX:NewSize和-XX:MaxNewSize分别代表新生代的初始值和最大值;
      • 旧生代(Old Generation):存放新生代经过三次垃圾会后仍存活的对象,如果旧生代已满,进行FullGCC,如果FullGCC后仍然无法存储对象,则报OutOfMemoryError(原因:要么heap设置大小不够;要么应用创建太多对象,且由于被引用而长时间不能被回收);
      • 持久代(Permanent Generation):存放类的元数据(方法、字段、类和包上描述数据,如注解Annotation,注释等,元数据可用于创建文档,跟踪代码中的依赖性如重构或重载,执行编译时检查,代码分析)、常量池、静态变量;如果出现OutOfMemoryError: PermGen space,原因有二:加载jar太多;或大量动态反射的类生成;可通过-XX:PermSize和-XX:MaxPermSize配置初始值和最大值;JDK1.8后将元数据移植到本地内存(好处:如多个项目的共享jar在本地内存只有一份),HotSopt VM为其分配或释放内存;可通过-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize配置大小;
      • JDK1.8后将元数据存放在Metaspace区域(本地内存,取代method area区域),常量池和静态变量仍在Heap的持久代中;

 

    • 栈(stack):程序执行,处理数据;空间动态增长(对象可垃圾回收),一个线程对应一个栈,每个方法对应一个frame,其中存储局部变量(局部变量区),方法返回值和程序运行产生的中间值(操作数栈)。基本类型长度固定(不会动态增长),需要空间较小,存放栈中;也存放对象引用;Object ob = new Object()一个最基本的对象,占用空间:占的空间为:4byte(栈)+8byte(堆);如Class A{
          int i;
          boolean b;
          Object obj;
      },其大小为:A(8byte)+int(4byte)+boolean(1byte)+引用(4byte)=17byte,但分配时为8的倍数,所以空间为24byte。
    • PC Register:存储了下一条将要执行的指令;
    • class load 机制

 

      • Custom ClassLoader: 应用程序自定义的ClassLoader, 如Tomcat会根据J2EE规范自行实现ClassLoader加载过程中,会检查类是否已被加载(自下而上),只要某个ClassLoader已加载即可,否则会自上而下加载该类。
      • Link:二进制字节码格式校验;静态变量默认值赋值;检验类、接口中属性和方法的存在,否则抛出NoSuchMethodError、NoSuchFieldError。
      • Initialize: 静态初始化代码、构造器代码以及静态属性的初始化;new对象;反射调用类中的方法;子类调用了初始化;JVM启动过程中指定的初始化类。
    •  JVM垃圾回收过程:垃圾回收的频率和时间是检验程序运行好坏的标准(因为回收时要停顿应用);
      • 清理Eden和Survior的GC叫Minor GC;清理old区域的叫Major GC;清理整个Heap的叫Full GC;
    • 哪些对象可被回收:
      • 引用计数算法:每创建一个对象,给其分配一个引用计数变量,并置值为1;这个对象每被引用一次,引用计数加1;当引用无效(即该对象的引用者生命周期结束或被修改时),该实例的引用计数减1;一个引用计数为零的对象将被垃圾回收,同时该对象引用其它对象的引用计数器减1。
        • 缺点:无法检测出循环引用,如父对象有对子对象的引用,子对象也有对父对象的引用;
        • 优点:执行效率高(可程序执行中交织进行)
      • 可达分析算法:通过从对象GC ROOT的引用关系开始分析,逐层画出一个引用关系图节点图,最后剩余节点被认为没有引用到,可垃圾回收(还会进行两次标记,最后确定是否回收);GC ROOT对象包括:栈中引用对象;方法区中静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象;
      • 引用分类:强引用(如Object obj = new object()),软引用,弱引用,虚引用,引用计数和可达分析算法都是基于强引用。
      • 方法区中常量和类的回收:前者可通过可达性分析进行回收;后者需同时满足:该类所有实例是否都已回收?加载该类的ClassLoader是否已经回收?该类对应的java.lang.Class对象是否在别的地方有被引用(确定无反射访问)?
    • 垃圾回收算法
      • 标记-清除算法:使用从根集合(GC Roots)进行扫描,对存活的对象进行标记,完成标记后,再扫描整个空间,对没有标记的对象进行垃圾回收;该算法不需要移动对象,当存活对象较多时,该算法较为高效,但会产生内存碎片;
      • 复制算法:为克服句柄开销和解决内存开销,将堆分为对象区和空闲区,当对象区已满,将对象区的存活对象复制到空闲区,这样将原来的对象区转变为空闲区,又可存放新的对象;
      • 标记-整理算法:与标记-清除算法不同的是,在回收没有引用的对象后,将存活对象往左端空闲空间移动,同时更新对应指针;该算法成本较高,但解决了碎片问题;
      • 分代收集算法(目前JVM常用算法):根据对象存活的生命周期,将对象划分为新生代(有大量对象回收)、老生代(较少对象回收)和永久代(不回收),不同代采用不同回收算法;
        • 新生代分为eden+survior0+survior1(比例8:1:1),新生对象放在eden中,垃圾回收时(不一定eden满),将eden中存活的对象复制到survior0,然后清空eden,如果survior0满,则将eden和survior0中存活对象复制到survior1,然后清空eden和survior0,再交换survior1和survior0(此时survior0为空),保持survior1为空,再循环以上过程;如果survior1不能存放eden和survior0中存活对象时,将存活对象复制到老年代;如果老年代也满,即触发Major GC(Full GC),新生代和老年代都进行垃圾回收(新生代叫Minor GC,频率较高)。
        • 老年代存放生命周期较长的对象(在新生代经过多次垃圾回收仍然存活的对象),内存空间大概是新生代2倍或以上。
        • 持久代存放静态文件,如java类,垃圾回收对该区没有多大影响;如果应用动态生成有大量类或调用累,需要将该区设置一个较大空间。
    • 垃圾收集器
      • 串行收集器(Serial Collector,复制算法):新生代单线程收集器(标记和清除都是单线程),是client模式默认的GC,简单高效,也可通过-XX:+UseSerialGC强制指定别的GC方式;
      • 串行老年代收集器(Serial Old Collector,清除-标记算法):老年代单线程收集器;
      • 并行新生代收集器(ParNew Collector,停止-复制算法):新生代并行收集器,在多核CPU环境下,比串行收集器效率更高;
      • Parallel Scavenge收集器(停止-复制算法):并行收集器,高效利用CPU以获得高吞吐量(用户线程时间/(用户线程时间+GC线程时间)),server模式默认采用的GC方式;可用-XX:+UseParallelGC和-XX:ParallelGCThreads指定GC方式和GC线程数;
      • Parallel Old Scavenge收集器(停止-复制算法):老年代并行收集器(吞吐量优先);
      • CMS收集器(Concurrent Mark Sweep,标记清理算法):高并发,低停顿(GC回收时暂停应用),响应快,尤适合多CPU。
    • Scanvenge GC和Full GC
      • 当Eden申请新生成对象空间失败时会出发Scanvenge GC;
      • 4种情况触发Full GC:年老带已满(Tenured);持久代已写满(Perm);System.gc()被显示调用;上一次GC后Heap各域分配策略动态变化;
    • 执行引擎(Execution Engine):字节码由一个个指令单元组成,每个指令有一个字节的操作码和操作数组成,执行引擎逐一读取这些指令;但字节码语言是人类可以理解的语言,需要再将bytecode转化成JVM machine能执行的语言;
      • 解释器(Interpreter):逐一读取、解释、执行字节码指令;可迅速解释一条指令,但执行指令较慢;
      • JIT (Just-In-Time) compiler:作为补偿Interpreter的一种手段,首先运行Interpreter, 然后在合适的时机,将所有的bytecode转化为native code直接执行,而且native code保存在缓存中,所以执行效率很高。
      • HotSpot采用编译器和解释器混合使用,HotSpot会将一些频繁调用的方法和代码(也叫热点代码Hot Spot Code)在运行时编译成native code;HotSpot内置两个编译器:client compiler(C1)和server compiler(C2), 程序采用哪个取决于JVM的运行模式设置;
      • 为了平衡启动响应速度和运行效率,HotSpot会采取逐层编译机制(Tiered compilation),JDK1.7的server complier默认开启该机制:

第0层:程序解释执行,解释器不开启性能监控,可触发第一层编译。 
第1层:也称为C1编译(更好的编译速度),将字节码编译为本地代码,进行简单、可靠的优化。 
第2层:也称为C2编译(更好的编译质量),也是将字节码编译为本地代码,但会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

    • 补充说明1:基本类型byte,boolean(1 byte),short,char(2 bytes),int,float(4 bytes),long,double(8 bytes);
    • 总结比较:
      • 堆中存放对象(大小一般不可估计,可动态增长),栈存放基本数据类型(长度一般1-8个字节,占用空间比较少)和堆中的对象引用(即4个byte的地址);
      • 由于程序是在栈中执行的,所以参数传递时,只存在传递基本类型和对象引用的问题(程序进入方法后,按引用地址从堆中找到对应的对象,进行数据读取或修改);
      • 可以没有堆,但不能没有栈;栈通过-Xss来设置大小(如出现异常java.lang.StackOverflowError)
      • 一个空对象的占用8个byte;Object ob = new Object();占用4+8个byte;下面这个对象需要17个字节空间,但因为占用空间是8的倍数,所以为24个字节;

        Class NewObject {

            int count;

            boolean flag;

            Object ob;

        }

      • Java 虚拟机在执行字节码时,实际上最终还是把字节码解释成机器指令;
      • 强引用:平时生产的对象在虚拟机下创建的引用,如果为强引用,不会被垃圾回收;
      • 软引用:一般是缓存引用,在内存空间紧张时,会被垃圾回收(所有出现OutOfMemory时不会有软引用);
      • 弱引用:也指缓存引用,但弱引用不管内存是否紧张,一定会在垃圾回收周期内被回收;
      • 尽可能避免使用短命的大对象;可以使用参数-XX:PetenureSizeThreshold (只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数)设置大对象直接进入年老代的阈值,即当对象的大小超过这个值时,将直接分配在年老代;也可通过参数-XX:MaxTenuringThreshold 来设置对象年龄(默认值是 15),当对象达到这个年龄阈值时,就挪到年老代;
      • 为什么一般不将大对象存放在Eden区,因为大对象占用空间大,如果Eden空间紧张,JVM 不得不将Eden中的大量小的对象挪到Old区,这对垃圾回收很不利;
      • 一般来说,稳定的堆对垃圾回收有利,因为稳定的堆可以减少 GC 的次数(但增加了GC的时间);可通过设置Xms 和-Xmx 同样的值,即最大堆和最小堆 (初始堆) 相等,
      • 增加吞吐量:尽可能减少系统执行垃圾回收的总时间,所以可以考虑使用并行回收收集器,比如
        java –Xmx4800m –Xms4800m –Xmn2G –Xss128k –XX:+UseParallelGC
           –XX:ParallelGC-Threads=20 –XX:+UseParallelOldGC
        • –Xmx4800m –Xms4800m:如果将堆的最小值设为1000m,那 JVM 会尽可能在 1000MB 堆空间中运行,这样发生 GC 的可能性就会比较高;
        • -Xss128k:减少线程栈的大小,这样可以使剩余的系统内存支持更多的线程;
        • -Xmn2g:设置年轻代区域大小为 2GB;
        • –XX:+UseParallelGC:Eden使用并行垃圾回收收集器,可尽可能地减少 GC 时间,增加系统吞吐量;
        • –XX:ParallelGC-Threads:设置垃圾回收的线程数,一般和 CPU 数量相等;
        • –XX:+UseParallelOldGC:设置Old使用并行回收收集器;
      • 为了减少 Full GC 次数,应尽可能将对象预留在Eden,因为Eden Minor GC 的成本远远小于年Old的 Full GC;
      • 为降低应用的垃圾回收时的停顿,首先考虑的使用CMS 回收器;比如–XX:+UseConcMarkSweepGC:年老代使用 CMS 收集器降低停顿:
        java –Xmx3800m –Xms3800m –Xmn2g –Xss128k –XX:ParallelGCThreads=20
         –XX:+UseConcMarkSweepGC –XX:+UseParNewGC –XX:+SurvivorRatio=8 –XX:TargetSurvivorRatio=90
         –XX:MaxTenuringThreshold=31
posted @ 2018-08-17 06:38  岩文01  阅读(156)  评论(0编辑  收藏  举报