JVM相关

Posted 数联架构师孵化器

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM相关相关的知识,希望对你有一定的参考价值。

写的不好仅供参考

JVM虚拟机生命周期


    1. 程序开始执行时开始运行,程序结束就停止运行

    2. 同一台机器运行三个程序,就会有三个运行中的JAVA虚拟机

    3. JAVA虚拟机开始于一个Main方法,这个方法是程序的起点,他执行的线程为程序的初始化线程,程序中的其他线程都由他来启动。

    4. JAVA中线程分为两类:守护线程和普通线程,main的初始化线程就是个普通线程,GC就是一个守护线程,只要JAVA虚拟机还有普通线程在执行,JAVA虚拟机就不会停止。如果权限够大,可以调用exit方法停止程序

类加载机制

         JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序看下图:

  1. Bootstrap ClassLoader/启动类加载器

加载 jre/lib/rt.jar里所有的class,由c++实现,不是ClassLoader子类

  1. Extension CLassLoader/扩展类加载器

负载加载java平台扩展功能的一些jar包,包括jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

  1. App ClassLoader/系统类加载器

负载加载ClassPath中指定的jar包及目录中的class

  1. Custom ClassLoader/用户自定义类加载器(java.lang.Class.Loader的子类)

属于应用程序根据自身需要自定义的ClassLoader,如tomcat,jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会检查类是否已被加载,检查顺序自底向上,从Custom ClassLoader 到BootStrap ClassLoader逐层检查,只要某个ClassLoader已加载就视为已经加载此类,保证此类只所有ClassLoader加载一次。而加载是自顶向下,也就是由上层来逐层尝试加载此类

双亲委派机制

如果一个类加载器收到了类加载请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传说到顶层的启动类加载器中,只有当父类不能加载请求时,子加载器才会尝试自己去加载

沙箱机制

基于双亲委派机制,采用一种JVM的自我保护机制,假设你要写一个java.lang.String的类,由于双亲委派的原理,此请求会先交给Bootstrap试图进行加载,但是BootStrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此保证了java的运行机制不会被破坏

GC回收机制

  1. 哪些内存需要回收

  1. 引用计数法

给对象添加一个引用计数器,每当一个地方引用这个对象时,计算器+1,引用失效计数器-1.任何时刻计数值为0的对象就是不可能再使用的。JAVA中没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况

  1. 可达性分析法

通过GC Roots的对象作为起始点,从这些节点向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链,说明这个对象不可用。


可以作为GC Roots对象包括以下几种

  1. 虚拟机栈 (栈帧中的局部变量区们也叫局部变量表)

  2. 方法区中的类静态属性引用的对象

  3. 方法区中常量引用的对象

  4. 本地方法栈中JNI(Native方法)引用的对象

  1. 方法区的垃圾回收

    1. 废弃常量

如果常量池中有个”abc”,但是当前程序中没有任何一个String对象引用这

个abc的常量,那么发送垃圾回收并且必要时,abc就会被移除常量池


    1. 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例

    2. 加载该类的ClassLoader已经被回收

    3. 该类对应的java.lang.Class对象没有再任何地方被引用,无法再任何地方通过反射访问该类的方法

    4. 无用的类

满足以上三个条件的类可以进行垃圾回收,但是并不是无用就被回收,JVM提供了一些参数供我们配置

垃圾搜集算法

  1. 标记-清除(Mark-Sweep)算法

首先标记所有需要回收的对象,标记完成后统一回收被标记的对象。

效率上:标记和清除效率不高

空间上:标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序分配较大对象时,无法找到足够的连续内存,而不得不提前触发一次垃圾收集动作。

JVM相关

JVM相关

  1. 复制清除法(Copying)

复制算法是为了解决效率问题而出现的,他将可用的内存分为两块,每次只用其中一块,当这块内存用完了,就将还存活的对象复制到另一款上面,然后再把已经使用过的内存空间一次性清理掉,内存分配爷不需要考虑碎片。

缺点:内存缩小了一半,代价高。

现在商用虚拟机都采用这种算法回收新生代,新生代的内存被划分了一块较大的Eden(伊甸园区)空间和两块较小Survivor(幸存区)空间,每次使用Eden和其中一块Survivor.每次回收将伊甸园区和幸存区中还存活的对象一次性复制到另一块幸存区,最好清除Eden和刚才用过的幸存区空间,如果幸存区的空间不够直接复制到老生代去

JVM相关

JVM相关

  1. 标记整理法

将所有存活的对象向一端移动,然后直接清理掉边界以外的内存

优点:解决内存碎片

缺点:由于需要大量移动存活对象,效率上比复制算法要低

  1. 分代收集算法

根据对象的生命周期的不同将内存划分几块,然后根据各块的特点采用最适当的算法,大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象成活率高、没有额外空间进行分配和担保的(老生代),采用标记清理或者标记-整理算法


常见垃圾收集器介绍

  1. Serial收集器是最基本、历史最悠久的垃圾收集器,它是一个单线程的收集器。进行垃圾回收时。必须暂停其他所有的工作线程,直到它收集结束。

优点是高效简单,适用于用户桌面的虚拟机

              既然会停止其他工作线程为什么它是高效简单?

              Serial收集器由于没有线程交互的开销一心只做垃圾收集自然可以获得最高单线程收集效率。用户桌面应用中分配给JVM管理的内存一般不会很大,收集个几十M甚至一两百M的新生代,停顿实际最多再100毫秒以内,只要不是频繁发生,这点停顿是没有问题的

缺点:优点也是它的缺点个人认为这个收集器,回收必须停止其他工作线程,高并发或者多线程中,就算是几十毫秒都是非常影响效率,这可能也是它适用CS应用的一种原因。BS中大量请求涌入,如果发生了GC,有断路器还好,没有的话可能会出现雪崩现象,或者服务器压力过大等一些问题。

  1. Parallel Scavenge收集器是一个新生代收集器,也是一个使用复制算法的收集器,又是并行的多线程的收集器。它的关注点与其他收集器不同,它关注的是吞吐量(吞吐量=代码运行时间/(代码运行时间+垃圾收集时间)),适用的是后台运算的程序

  2. CMS收集器(JDK7以前JVM GC默认算法,待查证),基于 标记-清除算法实现,运作过程:初始标记、并发标记、重新标记和并发清除。

初始标记标记和GC Roots直接关联的对象(也就是存活的对象),并发标记就是进行 GC Roots Tracing的过程,而重新标记则是对并发标记期间由于用户程序继续运行而导致标记产生变动的那一部分对象的标记记录的修正,并发清除就是进行垃圾回收了。最大的特点就是最短停顿回收时间

  1. G1收集器(JDK8默认GC算法),基于标记整理算法,不会产生内存碎片,而且能够精确的控制停顿(将java堆划分为很多大小固定的区域,并且跟踪这些区域的垃圾堆积成都,每次优先回收垃圾最多的区域)

JVM调优

对于调优一般就是三个过程:

  1. 性能监控:通过监控应用发现问题

  2. 性能分析:发现问题后通过经验和工具定位问题原因

  3. 性能调优:定位到原因后,通过代码、配置等手段进行优化

调优准备:

需要了解系统的总体架构,明确压力方向,那个接口调用频繁、处理高并发的压力如何。

需要构建测试环境来测试应用性能

对关键业务数据量进行分析,如数据库访问量压力,缓存数据有多大等

性能分析:

  1. CPU分析

当程序响应变慢,首页使用top、vmstat、ps等命令查看系统CPU使用率是否有异常,从而可以判断是否CPU繁忙造成的问题。其中主要通过us(用户进程所占的%)这个数据过高可以确定是CPU繁忙造成的响应缓慢。

CPU繁忙的原因:

  1. 线程中有无限空循环、无阻塞、正则匹配或者单纯的计算

  2. 发生频繁的GC

  3. 多线程的上下文切换。(大量线程抢占同一资源,就会出现这种情况)

确认CPU使用率高的进程后可以用jstack来打印异常进程的堆栈信息

Jstack[pid]

  1. 通过top –p [processId]

可以查看当前进程那个线程耗费了大量cpu

  1. 通过jstat –gcutil [pid]

可以查看对应进程的gc信息,判断是否是gc造成的cpu繁忙

  1. 通过vmstat

观察内核状态的上下文切换次数,判断是不是上下文切换造成的繁忙

  1. 内存分析

  1. 堆内存为java应用主要的内存区域,这部分内存相关有:

  1. 创建的对象:这个是存储在堆中,需要控制好对象的数量和大小尤其是大的对象很容易进入老生代

  2. 全局集合:全局集合通常是生命周期较长,因此需要特别注意全局集合的使用

  3. 缓存:缓存选用的数据结构不同,会很大程序影响内存的大小和gc

  4. 多线程:线程分配会占用本地内存,过多线程也会造成内存不足

以上使用不当很容易造成:

  1. 频繁GC会使应用响应变慢

  2. OOM(内存不足),直接造成内存泄漏错误使用

Heap space:堆内存不足

PermGen space:永久代内存不足

Native Thread:本地线程没有足够的内存可以分配

  1. 排查内存问题的常用空间是,是jdk自带的

  1. 查看jvm内存使用情况:jmap –heap

  2. 查看jvm内存存活的对象: jmap –histo:live

  3. 把heap里所有对象dump下来,无论对象是死是活:jmap –dump:format=b,file=xxx.hprof

  4. 先做一次full GC,再dump,只包含仍然存活的对象信息:jmap –dumo:format=b,live,file=xxx.hprof

  1. IO分析

通常与应用性能相关的包括:文件IO和网络IO

  1. 文件IO

使用系统工具pidstat、iostat、vmstat来查看IO状况。这里注意bi和bo这两个值,分别表示块设备每秒接受的块数量和块设备每秒发送的块数量,由此可以看出IO繁忙状况。

造成IO性能差的原因:

  1. 大量的随机读写

  2. 设备慢

  3. 文件太大

  1. 网络IO查看网络IO状况,一般使用netstat工具。可以查看所有连接的状况、数目、端口,当time_wait和close_wait连接过多时,会影响应用的相应速度。

性能调优

  1. CPU调优

  1. 不要存在一直运行的线程,可以使用sleep休眠一段时间。这种情况普遍存在pull方式消费的场景下,当一次pull没有拿到数据的时候建议sleep一下

  2. 轮询的时候可以使用wait/notify机制

  3. 避免循环、正则匹配、计算过多,包括使用String的fromatsplit eplace(可以使用apache commons-lang里的StringUtils对应的方法),使用正则去判断邮箱格式(有时候会造成死循环)、序列/反序列化等

  4. 结合JVM和代码,避免产生频繁的gc,尤其是full gc

  5. 使用线程池,减少线程数以及线程切换

  1. 内存调优

  1. 合理设置各个代的大小。避免新生代设置过小(内存不够时会频繁扫描新生代,或者直接创建对象到老生代)以及过大(产生碎片,浪费资源)同样也要避免Survivor设置过大和过小

  2. 选择合适的GC策略。需要根据不同的场景选择合适的GC策略。

  3. JVM启动参数配置-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:[log_path],以记录gc日志,便于排查问题。

  4. 避免保存重复的String对象,同时也需要小心String.subString()与String.intern的使用

  5. 使用对象池无节制创建对象,造成频繁GC。不要随便使用对象池,除非像连接池、线程池这种初始化/创建资源消耗较大的场景

  6. 谨慎热部署/加载的使用,尤其是动态加载类

  7. 不要用Log4j输出文件名、行号、因为Log4j通过打印线程堆栈实现,生成大量String.此外用log4j时建议使用log.isInfoEnabled()判断对应级别的日志是否打开,在做操作,否在也会产生大量String

  1. IO调优

  1. 文件IO:

  1. 使用异步写入代替同步写入,可以借鉴redis的aof机制

  2. 利用缓存、减少随机读

  3. 尽量批量写入,减少IO次数和寻址

  4. 使用数据库代替文件存储

  1. 网络IO

  1. 和文件IO类似,使用异步IO,多路复用IO/事件驱动IO代替同步阻塞IO

  2. 批量进行网络IO,减少IO次数和寻址

  3. 使用缓存,减少对网络数据的读取


以上是关于JVM相关的主要内容,如果未能解决你的问题,请参考以下文章

JVM的相关概念

jvm常用相关参数

JVM 相关知识

jvm GC日志 相关参数

JVM相关参数介绍

面试相关之 JVM &设计模式