Java应用性能测试之堆内存

每一个性能工程师都需要知道Java中内存是如何工作的吗?假如你想完全解决性能瓶颈的话,我的答案是“必须的”。Java的性能管理对每一个性能工程师以及Java开发者来说都是一个梦魇,但同时又是写好Java应用必不可少的一部分。

这是一个申请新的对象和清除不使用对象(垃圾回收)的过程。Java有自动的内存管理,在后台有自动运行的垃圾回收机制来回收不使用的对象并释放内存。假如没有足够的知识和经验来了解JVM和垃圾回收是如何工作的,不知道Java的内存是如何创建的,我们工程师在执行Java应用程序的时候就很难发现对应的瓶颈是在哪里。

当分析性能瓶颈的时候,理解Java内存模块的运行是一个技术活。在我查阅了很多博客,以及结合我自身的工作经验来看,趟过了很多工作上的坑之后,慢慢理解了JVM各个部分都是如何工作的。当我开始做性能测试的时候,根本不知道什么是Java的堆,我甚至不关注Java中对象都是如何创建的,更不用说GC是如何把不同类型的不使用的对象释放的。

在我开始做Java性能测试的时候,我遇到了好几个内存相关的错误,比如 java.lang.OutOfMemoryError,也就是在那时,我开始了解Java性能测试中JVM堆和栈所扮演的不同角色。当你想要获得一些性能相关的工作时,很多公司和客户都会检查你对Java开发和Java性能调试上面的专业度,所以理解Java中内存是如何申请的是非常重要的,它可以让你写出高性能的应用,再也不会出现诸如OutOfMemoryError或者Memory Leaks的错误。

每一个性能工程师在调试JVM性能问题时都需要理解Java的性能管理内部是如何工作的。我们所创建一切,比如类,方法,对象,变量其实在JVM中都是内存。比如,我们创建一个局部变量,一个全局变量或者不同的类对象,他们都是存储在JVM堆内存中。

JVM中有很大的内存,它被分成了两大部分,一个是堆内存另一个是栈内存。首先,我们从堆内存开始来分析可能的读写内存问题。堆内存在Java中的定位已经目标是什么,这大概是每一个性能测试工程师中心中的疑问。

对所有的性能问题来说,这是Java程序中非常关键的部分。其实有很多不同的模式,方法,资源以及技巧有关堆内存,我们可以用之来优化Java程序。

堆内存

关于Java中堆内存有很多图表都对之进行了描述。每一个性能工程师都需要理解PermGen和Metaspace之间的区别,我们可以从下面的图中粗略了解一二:

heap structure
JVM heap
JVM heap memory

堆内存被分为两代,一代是YOUNG,一代是OLD。YOUNG这一代的开始部分我们称之为EDEN空间,紧接着其后的第二部分称之为SURVIVOR,它有SURVIVOR0以及SURVIVOR1组成。现在我们来理解YOUNG每个部分的目的。无论何时,当我们创建一个新的对象时,他们都首先存放在EDEN空间。JVM中有一个自动的内存管理功能称之为垃圾回收(这里就不做详细介绍了)。

假如应用非常重创建了成百上千的对象,EDEN的内存就会被这些对象占满,这时候垃圾回收机制就会运行,把不使用或者没有引用的对象删除了,这个过程就称之为Minor GC。这个Minor GC会把所有的还存活的对象移动到SURVIVOR内存空间。Minor GC会自动在YOUNG中运行并释放内存空间。Minor GC运行的周期很短并且速度很快。

不同的类可能会创建很多对象,因此当对象的数目增大时,EDEN空间的内存也会增加。假设现在有GC1, GC2, GC3一直到GCN在运行(他们是由JVM自动调用垃圾回收操作创建的),他们会不停地检查不同的对象,并把他们立即转移到SURVIVOR内存中。

YOUNG中存活的对象会被转移到OLD中,当OLD也满的时候,Major GC就会启动。那什么时候Major GC会被启动呢?当OLD中内存完全满了的时候,Major GC就会启动。Major GC很长时候才会发生一次。当我们开发Java应用和Java自动框架的时候,假如程序员创建了很多的对象,那这时候就需要小心YOUNG和OLD这两个不同的概念了。

程序员不应该创建任何没有必要的对象,假如这种现象真的存在,那么垃圾回收应当在任务结束的时候即使销毁他们。Minor GC主要在YOUNG上运行,而Major GC则运行在OLD上。比如,你使用Amazon或者Walmart,我们知道将会有很多请求或者访问到这些页面,并且我们看到了超时的异常伴随着高的访问量。通常来说,在这种线上的商务网站,超时的异常一般是由于Major GC使用了大量的内存去销毁对象,从而使用相应的CPU以及内存的使用非常高。

这也表明了一些特定的类创建了太多了的对象。这种情况下,Major GC将会不停试图销毁不使用的对象。Major GC相比于Minor GC使用的时间会更长。

PermGen(Permanent Gneration) — JDK7之前支持

每个性能工程师都会好奇Permgen究竟是什么?

  1. Permgen包含什么?
  2. PermGen究竟在做什么?
  3. 什么样的数据被存储在这里?
  4. 什么样的属性会被存储在这里?

PermGen是一个特殊的对空间,和主内存堆是分开的。事实上Permgen不是堆内存的一部分,他们是非堆内存。所有的静态变量和常量会存储在方法的空间,而方法空间是Permgen的一部分。Permgen唯一的缺点是他的存储空间有限,所以经常会产生OutOfMemoryError。

Perm Gen中的类加载器是不会被垃圾回收的,所以会经常产生内存泄漏。32bit的JVM的默认最大内存是64MB,64bit是84MB。JVM中的-XX:PermSize 和 -XX:MaxPermSize在JDK8中已经不再支持。

MetaSpace — JDK8开始支持

MetaSpace是JDK8引入的用来取代之前的PermGen内存。两者最大的区别就在于他们对于内存申请的处理。尤其特别的是,这块内存空间会自动增加。metaspace的优点就是垃圾回收会在内存使用到达最大大小时自动运行。

有了这样的改进,在MetaSpace中OutOfMemoryError就离我们渐渐远去了。我们也有类似的参数来调整相关的设置 XX:MetaspaceSize以及XX:MaxMetaspaceSize

栈内存 – 简单介绍一下栈

Java的栈内存是用来为进程执行服务的,它包含了特定的方法值。使用的数据结构是LIFO(后进先出)。栈在java是一块包含方法,局部变量以及引用变量的内存。存储在堆中的变量是可以全局访问的,而在栈中的变量别的进程是不能访问的。当一个方法被调用的时候,会在栈中为这个方法创建一个新的块。

这个新的块包含了所有的局部变量,以及这个方法使用的别的对象的引用。当这个方法结束的时候,这个块就会被擦除,然后它就可以被后面的别的方法所使用。这里的对象只会在那个特定的函数生命周期中存在。栈的大小和堆比起来小很多。

当栈的空间不足时,会报Java.lang.StackOverFlowError,我们可以使用-Xss来定义栈的大小。

内存相关的错误

你需要理解的是这些输出其实只是表达JVM所受到的影响,而不是说真正的错误。真正的错误或者根源可能来自于你代码的某一个地方,比如内存泄漏,GC的问题,同步的问题,资源的申请或者甚至是硬件的设置。解决这些问题的最简单的方法就是增大受影响的内存大小。

我们需要监测资源的使用情况,分析一个类,得到多个堆的dump,分析这些dump,检查分析、优化我们的代码。假如这些方法都没有作用,那么可能需要分配更大的空间。

java.lang.StackOverFlowError — 这个错误表明栈内存满了

java.lang.OutOfMemoryError — 这个错误表明堆内存满了

java.lang.OutOfMemoryError: GC Overhead limit exceeded — 这个错误表明GC超过了限制

java.lang.OutOfMemoryError: Permgen space — 这个错误表明Permanent Generation满了

java.lang.OutOfMemoryError: Metaspace — 这个错误表明 Metaspace 满了

java.lang.OutOfMemoryError: Unable to create new native thread — 这个错误表明JVM本地代码不能在创建进程了,因为已经创建了很多进程,所有可用的资源都被消耗了。

java.lang.OutOfMemoryError: request size bytes for reason —这个错误表明交换空间满了java.lang.OutOfMemoryError: Requested array size exceeds VM limit — 这个错误表明数组的大小超了当前平台的限制

怎样设置初始和最大的堆

初始堆大小 — XMS

比1/64的物理内存大,或者别的合理的最小值。在J2SE 5.0之前,一个合理的最小值就是默认的初始堆大小。可以使用命令行中的-Xms来设置。

最大的堆大小 — XMX

比1/4的物理内存小或者1GB。在J2SE 5.0之前,默认的最大堆大小是64MB。可以使用命令行的-Mxm来设置。当Jenkins运在vm或者Docker Container中时这个阈值需要延长。把最小的堆大小和最大的值设置爱成一样。

推荐的JVM 设置

下面是一些推荐的值:

-server -Xms24G -Xmx24G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

对一些副本服务器,可以设置成这样:

-server -Xms4G -Xmx4G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

对独立的安装,使用这个值:

-server -Xms32G -Xmx32G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70

上述JVM设置的详解:

-Xms, -Xmx: 用来限制堆的大小。堆大小在副本服务器上比较小是因为即使全GC也不会导致SIP的转播。

-XX:+UseG1GC: 使用G1回收

-XX:MaxGCPauseMillis: 设置最大的GC暂停时间,这个只是一个软件的设置,JVM会尽力达到这一点。

-XX:ParallelGCThreads: 设置并行垃圾回收的进程数目。默认值在不同的系统上不同。

-XX:ConcGCThreads: 垃圾回收使用的并发进程数据。默认值在不同的系统上不同。

-XX:InitiatingHeapOccupancyPercent: 开始并发GC的堆使用百分比。默认值是45。

总结

有超过600参数可以传递给JVM去调整相应的垃圾回收和内存。假如在加上别的方面,这个数据很容易就到达1000+。我们只是简单介绍了一下性能调试中经常会遇到的参数。感觉阅读这篇文章。

原文地址:

https://dzone.com/articles/heap-memory-in-java-performance-testing

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *