性能测试分析调优必备的java虚拟机知识

Java虚拟机 

Java虚拟机(Java Virtual Machine,简称JVM)是一种用于执行Java字节码的虚拟计算机。它是Java平台的关键组成部分,负责将Java源代码编译为可在不同计算机体系结构上执行的字节码。

JVM起到了中间层的作用,使得Java程序可以在不同的操作系统和硬件上运行,实现了“一次编写,到处运行”的特性。JVM提供了内存管理、垃圾回收、安全机制、线程管理等功能,极大地简化了Java程序的开发和部署。

当你运行一个Java程序时,JVM将Java字节码加载到内存中,并逐行解释执行或即时编译为机器码执行。这种解释和编译的组合方式使得Java具有良好的跨平台性和高效的性能表现。

总之,Java虚拟机是Java程序的运行平台,它使得Java语言具备了跨平台、高效、安全等特性。

这个虚拟机的版本是sun公司的Hotspot。

参考:

测试工程师都能看懂的Jvm知识 上 (qq.com)

测试工程师都能看懂的Jvm知识 中 (qq.com)

测试工程师都能看懂的Jvm知识 下 (qq.com)

自动内存管理

 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域

堆Heap

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存 。

Java堆可以处于物理上不连续的内存空间中

按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

堆内存分类

Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等

Eden与Survivor空间比例为默认值8∶1

这样区分,是为了采用不同的回收算法来对内存进行回收。

jvm垃圾回收

堆内存资源宝贵且有限,如果堆积大量这种对象,势必会导致内存泄露、甚至溢出,那怎么办?垃圾回收啊,jvm自带一个垃圾回收线程,不断检查一些没有被局部变量、静态变量、或一些常量引用实例对象,标记这些对象为 可回收“垃圾”,定期清理掉,节省内存资源。

Young Gc就是发生在新时代的垃圾回收,Major Gc就是发生在老年代的垃圾回收,又称OldGc,Full Gc是发生在新时代、老年代、永久代等区域的垃圾回收,full就是全部的意思。不管是Young Gc、Full Gc还是其他Gc,都会造成 Stop the World 现象,都会导致系统卡顿,只是每个Gc造成的卡顿时间不同。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

(关于Full Gc到底是指old GC还是全部GC这里存疑。)

Full GC是Java中的全局垃圾回收,它会在整个堆空间中执行垃圾回收,清理所有不再被引用的对象所占用的内存空间。Full GC通常在以下情况下被触发:

  1. 调用System.gc时,系统建议执行Full GC,但并不必然执行。
  2. 老年代空间不足。
  3. 方法区空间不足。
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
  5. Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
  6. 永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。

需要注意的是,Full GC会暂停应用线程,对系统性能影响较大,所以应对策略通常是尽可能避免Full GC的发生,或者在必要时选择合适的时机进行Full GC。

GC导致程序停顿

有时候排查问题,需要注意GC会导致程序停顿。可以加日志查看。 

加入参数-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps-Xloggc:gclog.log

内存溢出OutOfMemoryError

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能

OutofMemortError

OutOfMemoryError是Java程序在试图分配更多内存时可能会遇到的一种错误。这通常发生在程序尝试创建大量对象或大型对象,而Java虚拟机(JVM)无法为它们分配足够的内存空间时。

有几种可能的原因可能导致OutOfMemoryError:

JVM的堆内存不足:这是最常见的原因。Java程序在运行时会在堆内存中创建对象。如果堆内存不足,JVM会抛出OutOfMemoryError。
永久代(PermGen)或元空间(Metaspace)空间不足:永久代是存储类的元数据和静态变量的地方,元空间是Java 8引入的替代永久代的概念。如果这两个区域的空间不足,也会抛出OutOfMemoryError。
栈深度过大:每个线程在JVM中都有自己的栈,用于存储局部变量和方法调用。如果栈的深度太大,超过了JVM的限制,也会抛出OutOfMemoryError。

解决OutOfMemoryError的方法: 

增加JVM的堆内存大小:可以通过调整启动JVM的参数来实现,例如使用-Xmx参数设置最大堆内存大小。
优化代码:避免创建大量的对象或大型对象,尽量复用对象,减少内存消耗。
检查是否存在内存泄漏:内存泄漏可能会导致程序在长时间运行后逐渐消耗越来越多的内存。使用Java的内存分析工具(如VisualVM、MAT等)可以帮助检测内存泄漏。
使用更适合的数据结构或算法:有些数据结构或算法可能会比其他的更消耗内存。尝试使用更节省内存的数据结构或算法可能有助于解决问题。
调整JVM的其他内存参数:例如,调整永久代或元空间的大小,或调整栈的大小等。

注意:在处理OutOfMemoryError时,最重要的是确定问题的原因。这可能需要使用内存分析工具,或者仔细审查代码以确定是否存在内存泄漏或其他问题。仅仅增加JVM的内存大小可能只是暂时的解决方案,如果问题的根本原因没有得到解决,问题可能会再次出现。 

Java堆溢出

Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。下面示例代码限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析

HeapDumpOnOutOfMemoryError是Java虚拟机(JVM)的一个参数,当JVM发生OutOfMemoryError时,它会自动生成一个堆转储文件(Heap dump file),这个文件通常用于后续的内存分析。

在JVM参数中加入-XX:+HeapDumpOnOutOfMemoryError,例如:java -XX:+HeapDumpOnOutOfMemoryError -jar yourApp.jar,当程序运行发生OutOfMemoryError时,会在项目目录下生成一个名为java_pid<pid>.hprof的堆转储文件(其中<pid>是Java进程的ID)。

需要注意的是,这种做法并不能解决OutOfMemoryError问题本身,只是提供了一种方便的途径进行后续的内存分析,以找出内存耗尽的原因。

import java.util.List;
import java.util.ArrayList;
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

 

在idea的edit configurations配置参数。

运行方法

可以看到发生了内存溢出。

Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。

要解决这个区域的异常,一般的手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

垃圾回收

垃圾收集(Garbage Collection,GC)

Java堆和方法区是垃圾回收器所关注的内存区域。

GC需要完成的3件事情

哪些内存需要回收?

什么时候回收?

如何回收?

可达性分析算法

可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的

垃圾回收算法

标记-清除算法

复制算法

标记-整理算法

分代收集算法

一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

垃圾收集器

基于JDK 1.7 Update 14之后的HotSpot虚拟机的收集器

如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器

serial收集器

在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。优点:简单高效,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

ParNew收集器

其实就是Serial收集器的多线程版本

它默认开启的收集线程数与CPU的数量相同

Parallel Scavenge收集器

目标则是达到一个可控制的吞吐量

Parallel Scavenge收集器也经常称为“吞吐量优先”收集器

CMS收集器

第一次实现了让垃圾收集线程与用户线程(基本上)同时工作

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿

 Serial Old收集器

同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一

G1收集器并行并发收集、分代收集、空间整理、可预测的停顿。

内存分配策略

对象优先在Eden分配大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象

长期存活的对象将进入老年代

内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为。因此,学习虚拟机内存知识,如果要到实践调优阶段,那么必须了解每个具体收集器的行为、优势和劣势、调节参数

内存占用

除了Java堆和永久代之外,我们注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。

Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory。

Direct Memory主要涉及的是Java的NIO库。它不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,而是在Java堆外的、直接向系统申请的内存区间,通常通过存在堆中的DirectByteBuffer来操作Native内存。

因为直接内存的读写性能通常高于Java堆,所以如果读写频繁的场合,可能会考虑使用直接内存。Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。

线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread (横向无法分配,即无法建立新的线程)。

Socket 缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。

JNI代码:如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。

虚拟机和GC:虚拟机、GC的代码执行也要消耗一定的内存。

本文内容摘自《深入理解java虚拟机:JVM高级特性与最佳实践》

猜你喜欢

转载自blog.csdn.net/seanyang_/article/details/133272123
今日推荐