Java虚拟机(JVM)调优

我们常用的虚拟机是HotSpot,除此之外,还有比如OpenJDK、IBM等

一、Java内存结构

  ★ 栈[本地方法栈和虚拟机栈]:本地方法栈(Native Method Stack) 与虚拟机栈所发挥的作用非常相似,它们的区别不过是虚拟机栈
     为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
     HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一。 

        ①、每个线程有一个私有的栈,随着线程的创建而创建。在jvm中栈用来存储一些对象的引用、局部变量以及方法中的形参或计算过程的中间数据,
           在方法退出后那么这些变量也会被销毁。它的存储比堆快得多,只比CPU里的寄存器慢。
        ②、栈里面存着的是一种叫“栈帧”的东西,每一个方法对应一个栈帧,方法中的形参,局部变量都放在栈帧中。栈的大小可以固定也可以动态扩展。
           当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误。当申请不到空间时,会抛出 OutOfMemoryError。
           这里有一个小细节需要注意,catch 捕获的是 Throwable,而不是 Exception。因为StackOverflowError 和 OutOfMemoryError 
           都不属于 Exception 的子类。
        ③、如果JVM开启了逃逸分析和标量替换,小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上,直接分配在栈上,可以自动
           回收,减轻GC压力,但大对象或者逃逸对象无法栈上分配。逃逸分析和标量替换是JVM的一种优化,默认时开启的,Java没有寄存器,所有参数
           传递都是使用操作数栈。
      
          java以栈帧为单位保存线程的运行状态。虚拟机只会对java栈执行两种操作:以栈帧为单位的压栈或者出栈。栈是私有的,是线程安全的
          栈帧由三部分组成:局部变量区,操作数栈和帧数据区。
         ● 栈内存在JVM中默认是1M,可以通过 -Xss 参数进行设置 

  ★ 堆:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配,这部分空间可通过 GC 进行回收。
        当申请不到空间时会抛出 OutOfMemoryError。

         ● 最小堆内存在JVM中默认物理内存的64分之1,用参数 -Xms 设置,最大堆内存在JVM中默认物理内存4分之一,用参数 -Xmx 设置,且建议
           最大堆内存不大于4G,并且设置-Xms=-Xmx,避免每次GC后,调整堆的大小,减少系统内存分配开销

  ★ 方法区(元空间):方法区也是所有线程共享。主要用于存储类的信息、常量池等。 方法区逻辑上属于堆的一部分,
                   但是为了与堆进行区分,通常又叫“非堆”。
            【java 虚拟机规范只是规定了方法区这么个概念和它的作用,并没有规定如何实现它,在其它JVM中不存在永久代,
            永久代存在在HOtsPOt虚拟机中,hotspot虚拟机在jdk1.7的时候把方法区叫做永久代,但是由于永久代内存经常不够用或发生内存泄漏,
            出现OOM error, 所以jdk1.8把移除永久代,改为元空间,元空间和永久代的本质类似,都是对JVM规范中方法区的实现,
            不过元空间和永久代之间最大的区别在于:元空间不在虚拟机中,而是使用本地内存中】

         ●  在1.7之前,可以使用如下参数来调节方法区的大小

                -XX:PermSize   方法区初始大小
      
                -XX:MaxPermSize  方法区最大大小

            超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

         ●  在jdk8中已经将永久带移除了。就是说-XX:PermSize这些参数在jdk8中将是无效的。新出现的元空间(Metaspace)来代替原来的永久带。
                  在1.8中,可以使用如下参数来调节方法区的大小

                    -XX:MetaspaceSize   元空间初始大小

                    -XX:MaxMetaspaceSize 元空间最大大小

              超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: Metadata space
              在jdk1.7中抛出的异常是这样:java.lang.OutOfMemoryError: PermGen

  ★ 程序计数器:是Java运行时数据区中的一小块内存区域
         ●  程序计数器是线程私有的,也就是说,每一个线程都拥有仅属于自己的程序计数器。
         ●  当虚拟机执行的方法不是native的时,程序计数器指向虚拟机正在执行字节码指令的地址;
         ●  当虚拟机执行的方法是native的时,程序计数器中的值是空。因为计数器记录的字节码指令地址,
            但是native 本地(如:System.currentTimeMillis())方法是大多是通过C实现并未编译成需要执行的字节码指令,
            所以在计数器中当然是空(undefined).
         ●  此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
Java内存结构.png

二、堆内存的构成

  1.年轻代:用于存放新产生的对象。
       可以使用参数 -Xmn 配置年轻代的大小,jvm中默认Eden区的内存比上survivor的内存等于8。
       即:如果年轻代的Xmn配置的100M,那么Eden就会被分配80M内存,每个survivor分配10M内存

  2.老年代:用于存放被长期引用的对象。年轻代在垃圾回收多次都没有被GC回收的时候就会被放到老年代,以及一些大的对象
          (比如缓存,这里的缓存是弱引用),这些大对象可以不进入年轻代就直接进入老年代

  3.持久带:用于存放Class,method元信息。

       ● survivor是幸存者,堆内存中两个survivor大小是一样的,两个survivor是彼此相互复制的
       ● 当new出一个对象,如果对象比较大的时候,直接放到tenured(老年代)中,否则放到eden(新生代)中,如果GC垃圾收集器回收了一次,
         没有被垃圾收集器回收的幸存者会放到一个survivor中,如果GC再回收一次,把幸存者放到另一个survivor中,
         然后把之前survivor中的幸存者复制到这个survivor中.......
堆内存.png

三、堆内存参数的调整

       ●  用 VisualVM 观察虚拟机堆信息

四、GC如何确定垃圾

   ▲ 什么是垃圾?
        ● 没有任何引用指向的对象(除去循环引用)

   ▲ 如何确定垃圾【JVM内存垃圾收集算法】?
        ●  引用计数:会有循环引用的问题      
        ●  正向可达:从roots对象计算可以到达的对象【main方法中创建的对象,classLoader等都可以是roots对象】

五、垃圾收集算法

       1、Mark-Sweep : 标记清除
             ★ 标记清除最大的问题就是内存的碎片化严重(可用内存不连续),后续可能发生大对象不能找到可利用空间的问题。
标记清除.png
       2、Copying : 复制算法 (用于新生代)
             ★ 效率高,解决了内存碎片化的问题,同时可以压缩内存,但是浪费内存(所有内存分为两部分,只能使用其中一半的内存)
复制.png
       3、Mark-Compact:标记压缩
             ★ 效率比Cpoy略低,一般用于老年代中
标记压缩.png

六、垃圾收集器选择

      JVM采用分代算法

    1、new(新生代):
       ● 存活对象少
       ● 使用copying算法,占用的内存空间也不大,效率也高

    2、old(老年代):
       ● 垃圾少
       ● 一般使用 Mark-Compact算法

七、JVM参数

       ●  - :标准参数,所有JVM都应该支持
       ● -X:非标准参数,每个JVM实现都不同
       ● -XX:不稳定参数,下一个版本可能会取消

八、JVM中的垃圾收集器

       ●  Serial Collector
            XX:+UseSerialGC
            单线程

       ●  Parallel Collector
            并发量大,不过每次垃圾收集,JVM都会停下来等垃圾收集完再运行执行

       ●  CMS Collector
            停顿时间短,JVM会在垃圾收集的同时执行

       ●  G1
            不仅停顿时间短,同时并发大

九、Java对象的分配

    如果JVM开启了逃逸分析和标量替换,默认是开启的,则会先将对象分配到栈上

   ▲ 栈上分配
       ●  线程私有小对象
       ●  无逃逸
       ●  支持标量替换
       ●  无需调整

   ▲ 线程本地分配TLAB(Thread Local Allacation Buffer)
       ●  占用eden,默认1%
       ●  多线程的时候不用竞争eden就可以申请空间,提高效率
       ●  小对象
       ●  无需调整

   ▲ old(老年代)
       ●  大对象

   ▲ aden

十、常用参数设置

   ▲ 堆设置
       ●  -Xms:初始堆大小
       ●  -Xmx:最大堆大小
       ●  -Xss:线程堆大小
       ●  -XX:NewSize=n:设置年轻代大小       
       ●  -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。
                               如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
       ●  -XX:MaxPermSize=n:设置持久代大小

   ▲ 收集器设置
       ●  -XX:+UseSerialGC:设置串行收集器
       ●  -XX:+UseParallelGC:设置并行收集器
       ●  -XX:+UseConcMarkSweepGC:设置并发收集器

   ▲ 垃圾回收统计信息    
       ●  -XX:+PrintGC
       ●  -XX:+PrintGCDetails
       ●  -Xloggc:filename

十一、调优Tomcat并使用JMeter进行评测

       ● tomcat 的bin目录下,catalina.bat中进行设置
            set JAVA_OPTS=
                   -Xms4g
                   -Xmx4g
                   -Xss512k
                   -XX:+AggressiveOpts  //虚拟机中能用到的优化项都设置上,包括升级后的优化项
                   -XX:+UseBiasedLocking   //优化锁的
                   -XX:PermSize=64M (Java1.8没有了) //永久区的大小
                   -XX:MaxPermSize=300M         //永久区最大大小
                   -XX:+DisableExplicitGC          //关掉显示调用GC  【System.gc();】
                   -XX:+UseConcMarkSweepGC   //使用CMS缩短响应时间,并发收集,低停顿
                   -XX:+UseParNewGC   //并发收集新生代的垃圾
                   -XX:+CMSParallelRemarkEnabled  //在使用UseParNewGC的情况看下,尽量减少mark的时间
                   -XX:+UseCMSCompactAtFullCollection //使用并发收集器时,开启对年老代的压缩,使碎片减少
                   -XX:LargePageSizeInBytes=128m  //内存分页大小对性能的提升
                   -XX:+UseFastAccessorMethods    //get/set方法转成本地代码
                   -Djava.awt.headless=true  //修复linux下的tomcat处理图表时可能产生的一个bug

十二、ThreadLocal(ThreadLocal采用了“以空间换时间”的方式)

  1、ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

  2、顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,
     是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。

你可能感兴趣的:(Java虚拟机(JVM)调优)