zoukankan      html  css  js  c++  java
  • JVM运行原理及调优

    一。 JVM介绍

      1。java类加载机制

         java命令 启动java程序后 会启动java虚拟机加载类 类的.class文件中的二进制数据读入到内存中 ,其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象 
    站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

    • 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDKjrelib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
    • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDKjrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
    • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    • 自定义类加载器 (比如UrlClassLoader等)

      种类加载器的层次关系如下图所示:

    JVM类加载相关策略

    • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
    • 父类委托(双亲委派),先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类(为了避免不同的加载器 加载相同的类 出现重复字节)
    • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

    java代码测试

    public class ClassLoader {
    	public static void main(String[] args) throws Exception {
    		java.lang.ClassLoader classLoader = ClassLoader.class.getClassLoader();
    		System.out.println(classLoader);
    		System.out.println(classLoader.getParent());
    		//Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
    		System.out.println(classLoader.getParent().getParent());
    		
    		//随便创建一个类 编译出class后 扔到f盘 使用自定义加载器去加载 如果 
    		//当前项目类路径上还有该类  当前 URLClassLoader父加载器就是 使用AppClassLoader加载
    		//类路径没有 只有f盘有 就是URLClassLoader
    		String curBin="f:\";
    		URLClassLoader mycls=new URLClassLoader(new URL[]{new File(curBin).toURL()});
    		Class cls=mycls.loadClass("TT");
    		System.out.println(cls.getClassLoader());
    		
    	}
    }
    输出结果是 
    sun.misc.Launcher$AppClassLoader@1a28362
    sun.misc.Launcher$ExtClassLoader@5fcf29
    null
    java.net.URLClassLoader@3ef810
    

    注意 有继承关系的两个类 不能分别使用不同的自定义加载器互相加载 否则报错 对应jvm类加载策略第一条

     2。 jvm内存结构

     类加载器加载类后  类中的变量 方法 以及内的实例 如何存储 就看jvm内存结构 下图展示了jvm内存结构

    JVM内存结构主要有三大块:堆内存、方法区和栈。

    • 堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配(SurvivorRatio参数) 比如内存200M  eden占用 8/10 即160M 其他两个分别站 1/10即20M;
    • 方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);
    • 栈又分为java虚拟机栈和本地方法栈主要用于方法的执行,方法执行过程中 临时存放变量指针 调用方法的参数等(参考
      汇编函数调用原理文章https://blog.csdn.net/liaomin416100569/article/details/8154681,https://blog.csdn.net/liaomin416100569/article/details/52135500)。

    java命令配置虚拟 参数 -X 或者 -XX可以修改具体内存分配

    控制参数

    • -Xms设置堆的最小空间大小。
      堆中 年轻代和年老默认有个比如 是  NewRatio   = 2 (默认是 2:1)
      年亲代中eden和suvivor默认有个比例 8:1:1  (SurvivorRatio = 8)  jps查看进程 jmap -heap 进程编号 查看到改参数
    • -Xmx设置堆的最大空间大小。
    • -XX:NewSize设置新生代最小空间大小。
    • -XX:MaxNewSize设置新生代最大空间大小。
    • -XX:PermSize设置永久代最小空间大小。
    • -XX:MaxPermSize设置永久代最大空间大小。
    • -Xss设置每个线程的堆栈大小 (64位 默认是1M  -XX:ThreadStackSize默认是0)。

    帮助查看-X参数  (java -X)
           查看-XX参数 (java -XX:+PrintFlagsFinal 或者 -XX:+PrintFlagsInitial)

    线程除了独占各自虚拟机栈以及本地方法栈外 还独有一个 程序计数器(Program Counter Register) 用于线程切换过程 
    当前线程 运行代码的状态信息  比如 运行到第几行等

    堆和方法区出现异常 抛出 OutOfMemoryError错误
    栈出现异常  抛出 StackOverflowError错误

    假设有个类 :

    public class T1 {
    	static int i=0;
    	public static void main(String[] args) {
    		String str="hello";
    	}
    }

    堆:对象 "hello"  数组 args
    方法区:静态变量 i,类T1定义
    栈 : main线程对str引用 args引用

     3。 垃圾回收

           垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了。 jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的.

      》》对象存活判断

    •  引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。 
    • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

     》》 垃圾回收算法

     判断对象是否存活后  需要对这些对象进行标记 然后从对应区删除 常用算法 如下:

    • 标记 -清除算法(Mark-Sweep)
    算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。


    • 复制算法(Copying)
    复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果对象大部分都是存活的  移动效率低下 只适用于大部分需要被删除少部分存活 少部分移动的情况


    • 标记-压缩(整理)算法(Mark-Compact)
    标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,
    而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存



    • 分代收集算法
    GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
    “分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

     

     》垃圾收集器

     垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现 
    以下使用的-XX参数语法是
      1) 布尔型参数选项:-XX:+ 打开, -XX:- 关闭。(译者注:比如-XX:+PrintGCDetails)
      2) 数字和字符型型参数选项通过-XX:=设定。数字可以是 m/M(兆字节),k/K(千字节),g/G(G字节)。比如:32K表示32768字节。(比如-XX:HeapDumpPath=./java_pid.hprof)

    • Serial收集器
      串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停) 该收集器包括:
    1. 串行收集器(新老年代都是单线程): 参数控制:-XX:+UseSerialGC 
    2. ParNew收集器(新生代多线程 老年代单线程): ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
      参数控制:
          -XX:+UseParNewGC ParNew收集器
          -XX:ParallelGCThreads 限制线程数量 默认是cpu核数 通过以下命令查看 (java -XX:+PrintFlagsFinal -version | find "ParallelGCThreads" )
    C:Usersjiaozi>java -XX:+PrintFlagsFinal -version | find "ParallelGCThreads"
        uintx ParallelGCThreads                         = 4
              {product}

    • Parallel收集器
          Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩
      参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行 查看该参数 默认值 java -XX:+PrintFlagsFinal -version | find "UseParallelGC"   默认开启

    • Parallel Old 收集器
             Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
      这个收集器是在JDK 1.6中才开始提供
      参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行 同上查看 默认开启
    • CMS收集器
              CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的
          优点: 并发收集、低停顿 
          缺点: 产生大量空间碎片、并发阶段会降低吞吐量
      参数控制:
      -XX:+UseConcMarkSweepGC 使用CMS收集器
      -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
      -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
      -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
    • G1收集器
         G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:
    1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
    2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
    参数配置:
    -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启;
    -XX:MaxGCPauseMillis =50 #暂停时间目标;
    -XX:GCPauseIntervalMillis =200 #暂停间隔目标;
    -XX:+G1YoungGenSize=512m #年轻代大小;
    -XX:SurvivorRatio=6 #幸存区比例

     3。 jvm常用命令

      参考 https://blog.csdn.net/liaomin416100569/article/details/45688599

    三。调优方案

     gc调优 主要是从 减少fullgc的次数 可以设置相关的gc参数

    -server -Xms200m -Xmx200m -Xmn100m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4
    -verbose:gc -Xloggc:c:/logs/gc.log 
    -Djava.awt.headless=true 
    -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:/logs/ttt.hpro  
    -XX:+PrintGCTimeStamps -XX:+PrintGCDetails 
    -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
    -XX:+UseParallelGC -XX:MaxTenuringThreshold=15
    1. 出现fullgc钱将dump导出 查看堆信息 使用mat分析
      -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:/logs/ttt.hpro 
      web项目建议使用cms垃圾收集器  
      -XX:+UseConcMarkSweepGC
    2. 打印gc日志  将日志上传到 http://gceasy.io 生成统计报表
      -verbose:gc -Xloggc:c:/logs/gc.log 
    3. 使用 jstat 统计gc相关信息  (参考 https://blog.csdn.net/liaomin416100569/article/details/45688599)

         4 jvisualvm安装visualgc插件   github搜索  visualvm 找到 

             visualvm/visualvm.github.io

           访问网站 visualvm.github.io 下载visualgc插件 (必须github lantern翻墙) 下载过程参考下 gif图
         (目前地址:https://github-production-release-asset-2e65be.s3.amazonaws.com/68017978/bc1d31fc-a506-11e6-9ffc-c77450f7944b?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20180419%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20180419T085309Z&X-Amz-Expires=300&X-Amz-Signature=7408ee6977963d053ae1f3f2176238e7d66f6ec8b24590b611f15e503b8116e0&X-Amz-SignedHeaders=host&actor_id=11897728&response-content-disposition=attachment%3B%20filename%3Dcom-sun-tools-visualvm-modules-visualgc.nbm&response-content-type=application%2Foctet-stream)

         安装过程 进入jdk/bin目录打开visualvm 点击工具-插件 已下载 添加安装即可 


      打开监听的进程就可以看到gc相关内存

    实际优化可以对应用进行压力测试  观察 堆栈 gc等的情况 进行优化

    文章编写 参考博客 http://www.ityouknow.com/jvm.html

  • 相关阅读:
    初始化ArrayList的两种方法
    MySQL最大连接数设置
    页面按钮的语义和可访问性
    H5+App开发框架汇总
    JS使用模板快速填充HTML控件数据
    Meta标签中的format-detection属性及含义
    java中@Qualifier("string")是什么用法
    MySQL 当记录不存在时insert,当记录存在时update
    美国40岁以上的程序员在干啥
    老程序员都去哪了?
  • 原文地址:https://www.cnblogs.com/liaomin416100569/p/9331136.html
Copyright © 2011-2022 走看看