jvm优化弊端,互联网微服务性能优化
jvm优化弊端,互联网微服务性能优化程序计数器、Java栈、本地方法栈、Java堆、方法区、运行时常量池、直接内存 JVM运行时数据区 Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码) 就可以在多种平台上不加修改地运行。 Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。
“世界上并没有完美的程序,但我们并不因此而沮丧,因为写程序本来就是一个不断追求完美的过程。” ——《深入理解java虚拟机》
看过我的头条文章都知道,在之前有分享过互联网WEB容器性能优化,本文将从以下四个方面来阐述JVM及其在线上服务出问题时如何调优。
1 JVM 模型Java技术体系、JVM内存模型、参数介绍和回收算法
1.1 简介 JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码) 就可以在多种平台上不加修改地运行。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。
JVM运行时数据区
程序计数器、Java栈、本地方法栈、Java堆、方法区、运行时常量池、直接内存
程序计数器
- 较小的内存空间
- 当前线程所执行的字节码的行号指示器
- 字节码解释器是通过改变计数器的值来选取下一条需要执行的字节码指令
Java栈
- 虚拟机栈描述java方法执行的内存模型
- 每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息
- 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程
本地方法栈
- 虚拟机栈与虚拟机栈基本类似,区别在于Java栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
Java堆
- 存储对象实例、数组的区域
- 垃圾收集器管理的主要区域
- 分为新生代和老年代
- 内存最大的一块
- 所有线程共享
方法区(非堆区)
- 所有线程共享
- 存储已被虚拟机加载的类信息
- 运行时常量池也在方法区
参数控制
- -Xms、–Xmx 堆区初始值、最大值
- –Xmn 设置年轻代大小
- -XX:PermSize 设置非堆内存初始值
直接内存
- 直接内存并不是虚拟机运行时数据区的一部分,但这部分内存也被频繁使用
- 在JDK1.4中新加入了NIO类,它可以使用Native函数库直接分配堆外内存
- 内存管理的时候经常忽略直接内存,是的内存总和大于物理内存限制,从而导致OutOfMemoryError异常
溢出泄露原理、垃圾收集原理
2.1 Java堆溢出堆(Heap)是Java存放对象实例的地方。堆溢出可以分为以下两种情况,这两种情况都会抛出
OutOfMemoryError:Java heap space。
内存泄漏
- 指对象实例在新建和使用完毕后,仍然被引用,没能被垃圾回收释放,一直积累,直到没有剩余内存可用。
- 指当我们新建一个实例对象时,实例对象所需占用的内存空间大于堆的可用空间。
垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。人们当时就在思考GC需要完成的3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
为什么我们要了解GC呢?
当需要排查各种内存溢出、内存泄露问题时;
当垃圾收集成为系统达到更高并发量的瓶颈时。
在堆里面存放着java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还"存活"着,哪些已经"死去"。
引用计算算法
- 给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
可达性分析算法
- 从字面理解此算法,即判断某个对象是否可达。
- 这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
标记 – 清除算法
- 首先标记出所有需要回收的对象。
- 标记完成后统一回收所有被标记的对象。
复制算法
- 将可用内存按容量划分为大小相等的两块,每次只使用其中一块。
- 当这一块的内存用完了,就将还存活的对象复制到另外一快上面,然后将已用过的内存空间一次清理。
标记 – 整理算法
- 不直接清理,而是让所有存活的对象都向一端移动,然后清理边界以外的内存。
分代收集算法
- 根据对象存活周期不同将内存划分为几块,然后按照不同算法回收。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体体现。
常用收集器
Serial 收集器、Parallel Scavenge 收集器、Serial Old 收集器、Parallel Old 收集器、ParNew收集器、CMS收集器、 G1 收集器
Parnew收集器是多线程的年轻代收集器,默认开启垃圾回收线程数和CPU核数相同,目前只能与CMS(老年代收集器)配合工作。
CMS收集器是一种以获取最短回收停顿时间为目标的老年代收集器。适合集中在互联网站
或者B/S系统的服务端上,系统停顿时间最短,可以给用户带来较好的体验。
收集算法是基于标记-清除"算法实现的,收集步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
G1收集器是当今收集器技术发展的最前沿成果之一,优势是不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。
工欲善其事必先利其器:GUI、命令行
3.1 GUI分析工具 为什么
- JDK提供的图形用户界面,可以详细地查看当前应用服务的内存情况,从而在解决异常、优化代码、优化服务等方面提供一些建议。
工具类型
- JConsole:综合资源消耗监控
- JMC:线程消耗CPU资源分析
- Jprofiler:内存资源分析
简介
- Jconsole 的图形用户界面是一个符合java管理扩展(JMX)规范的监测工具,Jconsole 使用java虚拟机,提供在Java平台上运行的应用程序的性能和资源消耗的信息。
简介
- Java Mission Control 飞行记录器,可查看记录数据,通过这些数据,可以清楚地了解到这一分钟时间内,整个操作系统以及JVM的所有数据情况。
简介
JProfiler是一个性能检测工具,通过评估CPU、内存以及线程来避免内存泄漏。支持较多的检测模式,使用较为灵活;
本地会话实时分析模式、远程会话实时分析模式、离线分析模式、快照比较和查看HPROF快照。
为什么
- 快捷,足够原始、粗暴。
工具类型
- Jps:简单查看当前java进程基本情况
- Jstat:JVM统计监测工具
- Jstatck:生成虚拟机当前时刻的线程快照
- Jmap:查看虚拟机内存使用状况
用法
- Jps 主要用来输出JVM中运行的进程状态信息。语法格式如下:
如果不指定hostid就默认为当前主机或服务器。
- 命令行参数选项说明如下:
- 实例
用法
- Jstat 是JVM统计监测工具,语法格式如下:
vmid是Java虚拟机ID,在Linux/Unix系统上一般就是进程ID;
interval是采样时间间隔;
count是采样数目。比如下面输出的是GC信息,采样时间间隔为250ms,采样数为4。
- 实例
用法
- jstack主要用来查看某个Java进程内的线程堆栈信息。语法格式如下:
jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码,所以它在JVM性能调优中使用得非常多。
- 实例
用法
- jmap用来查看堆内存使用状况,可以结合jhat使用,语法格式如下:
- 实例
案例分析、调优实战
4.1 案例分析之堆溢出 抛错信息
- OutOfMemoryError: Java heap space
原因
- 内存溢出主要存在问题就是出现在这个情况中。当在JVM中如果98%的时间是用于GC且可用的 Heap size 不足2%的时候将抛出此异常信息。
解决方式
- 调整参数,如调大JVM堆区内存、设置合适的垃圾回收方式;
- 观察GC回收是否正常;
- 查看抛出堆栈,排查代码是否存在内存泄露。
抛错信息
- OutOfMemoryError: PermGen space
原因
- 项目引用了大量的第三方jar,或者应用有太多的class文件而恰好MaxPermSize设置较小,超出了也会导致这块内存的占用过多造成溢出,或者Tomcat热部署时候不会清理前面加载的环境,只会将context更改为新部署的,非堆存的内容就会越来越多。
解决方式
- 调整参数,如调大JVM非堆区内存;
- 检查Tomcat热部署时非堆区是否会重置。
抛错信息
- OutOfMemoryError: unable to create new native thread.
原因
- 应用创建了太多的线程,而能创建的线程数是有限制的,导致了异常的发生。
- 能创建的线程数的具体计算公式如下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
解决方式
- 通过jstack工具定位程序是否存在bug;
- 如果程序确实需要大量的线程,改变MaxProcessMemory,JVMMemory,ThreadStackSize这三个因素,来增加能创建的线程数。