vlambda博客
学习文章列表

性能测试中的Java应用程序堆内存

JavaHeap可以使用几种不同的模型、方法、资源和提示来完全优化任何Java应用程序。


每个性能工程师都需要知道Java中的内存是如何工作的吗?要彻底调优Java性能瓶颈以获得高性能,回答是肯定的。对于每个性能工程师和Java开发人员来说,Java内存管理是一个重大的挑战,需要继续学习,以便对Java应用程序进行适当的调优。


它是正确分配新对象和删除未使用对象(垃圾收集器)的过程。Java有自动内存管理,这是一个垃圾收集器,它可以在后台清除未使用/未引用的对象,并释放一些内存。由于没有足够的Java知识和经验、JVM和垃圾收集器如何工作,以及Java内存是如何构建的,许多性能工程师在执行Java应用程序性能测试时无法找到和解决Java瓶颈。


在瓶颈分析方面,理解所有Java应用程序的Java内存模型一直是性能工程师的一项技术任务。通过我的理解、来自不同博客的研究以及我的经验,我开始了解JVM中的各种运行时数据区域是如何工作的。当开始性能测试时,我们大多数人都不知道什么是java堆或java中的堆空间,甚至不知道java中的对象是在哪里创建的,以及不同类型的GC如何从java堆内存的不同区域清除未引用的对象。


当我开始对Java应用程序进行专业的性能测试时,我遇到了几个与内存相关的错误,比如java.lang.OutOfMemoryError当我开始了解JVM堆和堆栈在Java性能测试中的作用时。当谈到具有挑战性机会的性能工程工作角色时,大多数公司/客户都会检查您在Java开发和Java性能调整方面的实际动手经验的专业知识,因此了解Java中内存分配的工作原理非常重要,这将使您有机会编写高性能和优化的应用程序,不会出现OutOfMemoryError和内存泄漏问题。


每个性能工程师都需要了解Java内存管理在JVM中是如何工作的,以微调JVM性能问题。我们以类、方法、对象、变量的形式创建的任何东西都与JVM中的内存有关。例如,如果我们要创建一个局部变量或全局变量或不同的类对象,则所有内容都存储在JVM堆内存中。


JVM中有大量的内存,它分为两部分,一部分是堆内存,另一部分是堆栈内存。首先,我们将从堆内存开始,解决与内存相关的所有性能潜在问题。Java中堆内存的用途和作用是什么,这是性能测试中比较普遍的问题。


对于所有的性能问题,有几种不同的模型、方法、资源和技巧可用于Java堆来完全优化任何Java应用程序。


堆内存


Java中有许多不同的图表解释了Java中堆内存的设计。每个性能工程师都必须从最近的JDK版本中了解PermGen和Metaspace之间的区别,我们可以参考下面的任何图表:



堆内存主要分为两代,年轻一代和老一代。年轻一代的第一部分称为EDEN空间,第二部分称为SURVIVOR0和SURVIVOR1空间组成的SURVIVOR空间。现在让我们来了解一下年轻一代每个空间的用途。每当我们创建新对象时,所有这些对象都首先存储在EDEN空间中。在JVM中,有一个称为垃圾收集的自动内存管理特性(这里不详细介绍GC的概念)。


如果应用程序很重并且创建了数千个对象,那么EDEN内存将充满对象,然后垃圾回收器将启动并删除所有未使用/未引用的对象,此过程称为Minor GC。这个小GC将把所有SURVIVOR对象移到SURVIVOR记忆空间。Minor GC已自动对年轻一代执行以释放内存。小GC在短时间内表现非常快。


为不同的类创建了这么多对象,所以当对象数量增加时,EDEN空间中的内存就会增加。让我们假设GC1、GC2、GC3等等在GCN之前被触发,当JVM自动调用许多垃圾收集器操作时,小GC会继续检查所有准备好生存的不同对象,JVM立即将所有这些对象转移到SURVIVOR内存中。


年轻一代中幸存下来的对象被移到老一代,当老一代被对象填满时,主GC被触发。何时执行主要GC?当老一代内存中的对象已满时,将执行主GC。大型GC需要长时间来完成。当团队开发任何Java应用程序或Java自动化框架时,如果开发人员要创建这么多对象,他们就必须小心年轻一代和老一代的概念。


开发人员不应该创建任何不必要的对象,如果他们创建了任何对象,垃圾回收器应该在他们完成任务后销毁它们。小GC将在年轻一代上执行,而老一代将在主要GC上执行。例如,如果我们以亚马逊或沃尔玛为例,我们意识到会有太多的请求和点击,我们会看到超时异常,高流量,在任何在线电子商务销售中,大多数时间都会出现超时异常,因为主要的GC会忙于占用大量内存来销毁对象,而且很明显,作为副作用,服务器上的CPU和内存利用率很高。


这表明他们在内部创建了许多特定类的对象。在这个场景中,主要GC将尝试连续大量销毁所有未使用的对象。与次要GC相比,主要GC需要更长的时间。


PermGen(Permanent Generation)-在JDK7之前可用


也许每个性能工程师似乎都有不同的问题,关于这个Permgen是干什么的?


  • Permgen会包含什么?
  • 在PermGen发生了什么?
  • 这里存储什么样的数据?
  • 什么样的属性将存储在 Permanent Generation 中?


PermGen是一个特殊的堆空间,与主内存堆分离。这个Permgen不是堆内存的一部分,它被称为非堆内存。您定义的所有静态变量和常量变量都将存储在方法区域中,而记住方法区域是Perm生成的一部分。这种Permgen的唯一缺点是其有限的内存大小,这将产生OutOfMemoryError。


PermGen中的类装入器没有得到充分的垃圾回收,因此会产生内存泄漏。32位JVM的默认最大内存大小为64 MB,64位版本为82 MB。jdk8已经删除了对Permgen的JVM参数-XX:PermSize和-XX:MaxPermSize的支持


Metaspace-jdk8


Metaspace是从javajdk8开始的一个新的内存空间,它取代了javajdk7中旧的PermGen内存空间。最显著的区别在于它如何处理内存分配。具体地说,这个本机内存区域在默认情况下会自动增长。jdk8的metaspace的优点是,一旦类元数据使用量达到其最大metaspace大小,垃圾收集器会自动触发对死类的删除。


通过新的改进和更改,JVM减少了从metaspace获取OutOfMemoryError的机会。我们有一些JVM参数来调整metaspacexx:MetaspaceSize和XX:MaxMetaspaceSize的内存


堆栈内存


Java堆栈内存用于线程执行,它包含特定的方法值。使用后进先出的数据结构。java中的Stack是一个包含方法、局部变量和引用变量的内存部分。存储在堆中的对象可以全局访问,而其他线程不能访问堆栈内存。当一个方法被调用时,它会在堆栈中为该特定方法创建一个新的块。


新块将具有所有的局部值,以及对该方法正在使用的其他对象的引用。当方法结束时,新块将被删除,并可供下一个方法使用。您在这里找到的对象只能访问该特定函数,并且不会超出该函数。与堆内存相比,堆栈内存的大小非常小。


如果堆栈中没有存储函数调用或局部变量的内存,JVM将抛出java.lang.StackOverflower我们可以使用-Xss来定义堆栈内存大小。


内存相关错误


您需要完全理解的是,这些输出只能说明对JVM的影响,而不是实际的错误。实际错误及其根本原因可能发生在代码中的某个地方,例如内存泄漏、GC问题、同步问题、资源分配,甚至可能是硬件设置。要解决这个问题,解决所有这些错误的简单方法是增加受影响的资源大小。


我们将需要从性能测试和工程环境中监控资源使用情况,分析每个类别,进行多个堆转储,遍历堆转储,检查并调试/优化您的代码,等等,如果您的修复看起来都不起作用,需要换一个思路。


java.lang.StackOverflower-此错误表示堆栈内存已满。

java.lang.OutOfMemoryError-此错误表示堆内存已满。

java.lang.OutOfMemoryError: GC Overhead limit exceeded -此错误表示GC已达到其开销限制

java.lang.OutOfMemoryError:Permgen space-此错误表示永久生成空间已满

java.lang.OutOfMemoryError:Metaspace-此错误表示Metaspace已满(因为javajdk8)

java.lang.OutOfMemoryError:Unable to create new native thread-此错误表示JVM本机代码无法再从底层操作系统创建新的本机线程,因为已经创建了太多线程,它们占用了JVM的所有可用内存

java.lang.OutOfMemoryError:request size bytes for reason-此错误表示应用程序已完全占用交换内存空间

java.lang.OutOfMemoryError: Requested array size exceeds VM limit -此错误表示我们的应用程序使用的数组大小大于基础平台允许的大小


如何设置初始堆和最大堆


初始堆大小-XMS


大于机器上机器物理内存的1/64,或某个合理的最小值。在J2SE5.0之前,一个合理的最小值是默认的初始堆大小,它因平台而异。可以使用命令行选项-Xms覆盖此默认值。


最大堆大小-XMX


小于物理内存的四分之一或1GB。在J2SE5.0之前,默认的最大堆大小是64MB。可以使用-Xmx命令行选项覆盖此默认值。根据oraclejvm人类工程学页面,最大堆大小应该是物理内存的1/4。当在VM和/或Docker容器中运行时,这个阈值也会被扩展(当在同一主机上运行多个Java应用程序时,最大堆大小限制将被聚合)。


建议的JVM设置


建议对大多数生产引擎层服务器使用以下JVM设置示例:


-server-Xms24G-Xmx24G-XX:PermSize=512m-XX:+UseG1GC-XX:maxgcpausemilis=200-XX:ParallelGCThreads=20-XX:congcthreads=5-XX:initiatingeappocationncypercent=70


对于生产副本服务器,请使用示例设置:


-server-Xms4G-Xmx4G-XX:PermSize=512m-XX:+UseG1GC-XX:maxgcpausemilis=200-XX:ParallelGCThreads=20-XX:congcthreads=5-XX:initiatingheapoccurrencypercent=70


对于独立安装,请使用示例设置:


-server-Xms32G-Xmx32G-XX:PermSize=512m-XX:+UseG1GC-XX:maxgcpausemilis=200-XX:ParallelGCThreads=20-XX:congcthreads=5-XX:initialingeapOccupmentcypercent=70


上面的JVM选项具有以下效果


-Xms,-Xmx:为堆大小设置边界,以提高垃圾收集的可预测性。副本服务器中的堆大小是有限的,因此即使是完整的gc也不会触发SIP重传。-Xms设置起始大小以防止由堆扩展引起的暂停。


-XX:+UseG1GC:使用垃圾优先(G1)收集器。

-XX:maxgcpausemilis:设置最大GC暂停时间的目标。这是一个软目标,JVM将尽力实现它。

-XX:ParallelGCThreads:设置垃圾收集器并行阶段使用的线程数。默认值随运行JVM的平台而异。

-XX:congcthreads:并发垃圾回收器将使用的线程数。默认值随运行JVM的平台而异。

-XX:initiatingheapoccurrencypercent:启动并发GC循环的(整个)堆占用率百分比。GC根据整个堆的占用率而不仅仅是一代(包括G1)触发并发GC循环,则使用此选项。值为0表示“do constant GC cycles”。默认值为45。


结论


有600多个参数可以传递给JVM来微调垃圾收集和内存。如果包含其他方面,JVM参数的数量将超过1000+。我们只解释了几个JVM参数,这些参数在Java应用程序的性能测试中是最有用的。