深入理解JVM内存管理:永久保存区域、堆区域与Java栈的全面解析

在解决 java 内存溢出问题之前,需要对 java 虚拟机(jvm)的内存管理具备一定的认知。jvm 所管理的内存大致包含以下三种不同类型的内存区域:

深入理解JVM内存管理:永久保存区域、堆区域与Java栈的全面解析

永久保存区域(PermanentGeneration space)、堆区域(Heap space)、Java 栈(JavaStacks)。永久保存区域主要是存放 Class(类)以及 Meta 的信息。Class 在第一次被 Load 时,会被放入 PermGenspace 区域。Class 需要存储的内容主要包含方法和静态属性。

堆区域用于存放 Class 的实例,也就是对象。对象需要存储的内容主要是非静态属性。每当使用 new 创建一个对象实例后,该对象实例就会存储在堆区域中,并且这部分空间会被 JVM 的垃圾回收机制所管理。Java 栈的功能与大多数编程语言(包括汇编语言)的栈相似,主要用于存储基本类型变量以及方法的输入输出参数。

Java 程序的每个线程都拥有一个单独的堆栈。内存空间中容易出现内存溢出问题的有:PermanentGeneration space 以及 Heap space。

第一种 OutOfMemoryError 是 PermGenspace 出现的问题。其原意是程序中使用了大量的 jar 或 class,导致 java 虚拟机装载类的空间不足,与 PermanentGeneration space 相关。

解决这类问题有以下两种办法:

增加 java 虚拟机中 XX:PermSize 的大小,XX:PermSize 为初始永久保存区域大小;同时增加 XX:MaxPermSize 的大小,XX:MaxPermSize 是最大永久保存区域大小。

针对 tomcat6.0 ,在 catalina.sh 或 catalina.bat 文件中,找到一系列环境变量名说明结束的位置(大概在 70 行左右),然后增加一行,内容为 JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m" 。若为 windows 服务器,还可在系统环境变量中进行设置。

感觉在使用 tomcat 发布 sprint + struts + hibernate 架构的程序时,这种内存溢出错误很容易出现。通过使用上述方法,我成功地解决了部署 ssh 项目的 tomcat 服务器经常出现宕机的问题。

清理应用程序中 web-inf/lib 下的 jar 。若 tomcat 部署了多个应用,且很多应用都使用了相同的 jar ,那么可以将共同的 jar 移到 tomcat 共同的 lib 下,以此减少类的重复加载。网上有部分人推荐这种方法,我未曾试过,但感觉无法减少太大的空间,最靠谱的还是第一种方法。

第二种 OutOfMemoryError 是 Java heap space。这种问题出现的原因在于,java 虚拟机创建的对象数量过多,在进行垃圾回收之前,虚拟机分配给堆内存的空间就已经被用完了,并且与 Heapspace 相关。

解决这类问题有两种思路:

检查程序,查看是否存在死循环的情况,以及是否存在不必要地重复创建大量对象的现象。找到原因之后,对程序和算法进行修改。

我以前在写一个使用 K-Means 文本聚类算法对几万条文本记录进行文本聚类的过程中,这些文本记录每条的特征向量大约有 10 来个。因为程序在细节方面存在问题,所以引发了 Javaheap space 的内存溢出问题。后来,通过对程序进行修改,这个问题得到了解决。

增加 Java 虚拟机中初始堆大小(Xms)的参数大小,同时增加 Java 虚拟机中最大堆大小(Xmx)的参数大小。

设置 JAVA_OPTS 为 -Xms256m 以及 -Xmx1024m

一、内存溢出类型

Java 语言中出现了“java.lang.OutOfMemoryError: PermGen space”的错误,即永久代空间溢出错误。这种错误通常发生在 Java 应用程序运行过程中,当 JVM(Java 虚拟机)试图分配更多的内存到永久代时,由于永久代的空间已经耗尽,就会抛出这个异常。永久代主要用于存储类的元数据、常量池等信息,当应用程序加载大量的类或者使用大量的常量时,就可能会导致永久代空间不足的问题。

JVM 管理两种类型的内存,分别是堆和非堆。堆是供开发人员使用的,如前文所述,它在 JVM 启动时被创建;非堆则是留给 JVM 自身使用的,用于存放类的信息。它与堆不同,在运行期间,垃圾回收器(GC)不会释放其空间。

如果 web app 使用了大量的第三方 jar,或者应用有太多的 class 文件,而恰好 MaxPermSize 设置较小,超出后会导致这块内存占用过多而造成溢出。另外,在 tomcat 热部署时,不会清理前面加载的环境,只会将 context 更改为新部署的,这样非堆存的内容就会越来越多。

PermGen space 的全称是 Permanent Generation space。它指的是内存的永久保存区域。这块内存主要被 JVM 用来存放 Class 和 Meta 信息。Class 在被 Loader 时就会被放到 PermGen space 中。它和存放类实例(Instance)的 Heap 区域不同。GC(Garbage Collection)不会在主程序运行期对 PermGen space 进行清理。所以如果你的应用中有很多 CLASS,就很可能出现 PermGen space 错误。这种错误常见于 web 服务器对 JSP 进行 pre compile 的时候。如果你的 WEB APP 使用了大量的第三方 jar。这些第三方 jar 的大小超过了 jvm 默认的大小(4M)。那么就会产生此错误信息。

一个最佳的配置例子,本人经过验证,自从使用此配置后,就再也没有出现过 tomcat 死掉的情况。

设置 JAVA_OPTS 为 -Xms800m ;设置 JAVA_OPTS 为 -Xmx800m ;设置 JAVA_OPTS 为 -XX:PermSize=128M ;设置 JAVA_OPTS 为 -XX:MaxNewSize=256m ;设置 JAVA_OPTS 为 -XX:MaxPermSize=256m

Java 语言中出现了“java.lang.OutOfMemoryError: Java heap space”错误,即 Java 堆空间不足的错误。这意味着在 Java 程序运行过程中,用于存储对象的数据结构(即 Java 堆)已经没有足够的空间来容纳新创建的对象。这种情况可能会导致程序崩溃或出现其他异常行为。

第一种情况是一个补充。主要存在的问题出现在这个情况里。它的默认空间(也就是-Xms)是物理内存的 1/64,最大空间(即-Xmx)是物理内存的 1/4。

如果内存剩余的量小于 40%,那么 JVM 会将堆增大到 Xmx 设置的值。如果内存剩余的量超过 70%,JVM 就会把堆减小到 Xms 设置的值。因此,服务器的 Xmx 和 Xms 设置通常应该设置为相同的,这样可以避免在每次进行 GC 之后都需要去调整虚拟机堆的大小。

如果物理内存是无限大的。那么 JVM 内存的最大值与操作系统相关。通常 32 位机的 JVM 内存最大值在 1.5g 到 3g 之间。64 位机的 JVM 内存最大值则没有限制。

如果 Xms 超过了 Xmx 的值,那么会引起服务器启动不起来;如果堆最大值和非堆最大值的总和超过了物理内存,会引起服务器启动不起来;如果堆最大值和非堆最大值的总和超过了操作系统的最大限制,也会引起服务器启动不起来。

垃圾回收GC的角色

JVM 调用 GC 的频率是比较高的。主要在以下两种情况下会进行垃圾回收:

当应用程序的线程处于空闲状态时;另外,当 Java 内存堆不足时,就会持续调用 GC。如果连续进行回收仍然无法解决内存堆不足的问题,那么就会报出 out of memory 错误。由于这个异常是由系统运行环境所决定的,所以无法预先得知它会在何时出现。

程序的运行依据 GC 的机制会引发系统运行环境的改变,从而增加了 GC 的触发几率。

为避免这些问题,在程序的设计和编写过程中,应避免垃圾对象占用内存以及产生 GC 的开销。显示调用 System.GC(),这仅仅是建议 JVM 需要在内存中对垃圾对象进行回收,并非必须立即进行回收。

另外还会使 GC 的消耗有所增加。

二、JVM内存区域组成

简单的说java中的堆和栈

java把内存分两种:一种是栈内存,另一种是堆内存

函数中定义的基本类型变量在函数的栈内存中分配;函数中定义的对象的引用变量也在函数的栈内存中分配。

2。堆内存用来存放由new创建的对象和数组

在函数(代码块)里定义一个变量时,java 会在栈中给这个变量分配内存空间。当变量的作用域结束后,java 会自动把为该变量分配的内存空间释放掉。而在堆中分配的内存,是由 java 虚拟机的自动垃圾回收器来进行管理的。

堆的优势在于能够动态地分配内存大小,并且其生存期无需事先告知编译器,因为它是在运行时进行动态内存分配的。而其缺点是需要在运行时进行动态内存分配,这就导致存取速度相对较慢。

栈的一个优势在于它的存取速度比堆要快。栈的另一个缺点是,存放在栈中的数据,其大小以及生存期都必须是确定的,没有灵活性。

java堆分为三个区:New、Old和Permanent

GC有两个线程:

新创建的对象被分配至 New 区。当 New 区被填满时,GC 辅助线程会将其移到 Old 区。当 Old 区也被填满时,会触发 GC 主线程去遍历堆内存中的所有对象。Old 区的大小等于 Xmx 减去 -Xmn。

java栈存放

栈调整时,参数包含+UseDefaultStackSize 和 -Xss256K,这意味着每个线程能够申请 256k 的栈空间。并且每个线程都拥有它自己的 Stack。

三、JVM如何设置虚拟内存

在 JVM 中,当 98%的时间都用于 GC ,并且可用的 Heap size 不足 2%时,就会抛出此异常信息。

Heap Size 最大不应超过可用物理内存的 80%。通常情况下,要把 -Xms 和 -Xmx 选项设置为相同的数值,并且 -Xmn 应为 -Xmx 值的 1/4。

JVM 初始分配的内存是由 -Xms 来指定的,其默认值为物理内存的 1/64;JVM 最大分配的内存是由 -Xmx 来指定的,其默认值为物理内存的 1/4。

当默认的空余堆内存小于 40%时,JVM 会增大堆直至达到 -Xmx 的最大限制;当空余堆内存大于 70%时,JVM 会减少堆直到达到 -Xms 的最小限制。所以服务器通常会将 -Xms 和 -Xmx 设置为相等的值,以此来避免在每次进行 GC 后都调整堆的大小。

如果物理内存是无限大的,那么 JVM 内存的最大值与操作系统有着很大的关联。

简单来说,32 位处理器的可控内存空间为 4GB。不过,具体的操作系统会对其进行限制。

这个限制通常在 2GB 到 3GB 之间,一般而言,在 Windows 系统下是 1.5G 到 2G,在 Linux 系统下是 2G 到 3G。而对于 64bit 以上的处理器,就不存在这样的限制了。

如果 Xms 超过了 Xmx 的值,那么会引起服务器启动不起来;如果堆最大值和非堆最大值的总和超过了物理内存,也会引起服务器启动不起来;如果堆最大值和非堆最大值的总和超过了操作系统的最大限制,同样会引起服务器启动不起来。

设置 NewSize 和 MaxNewSize 相等,“new”的大小最好不超过“old”的一半,因为如果 old 区不够大,就会频繁触发“主”GC,从而大大降低性能。

JVM 会使用 -XX:PermSize 来设置非堆内存的初始值。其默认值为物理内存的 1/64。

XX 可以通过设置 MaxPermSize 来确定最大非堆内存的大小,其默认值为物理内存的 1/4。

解决方法:手动设置Heap size

对 TOMCAT_HOME/bin/catalina.bat 进行修改。

在“echo "Using CATALINA_BASE: "之后加入以下行,接着加入“$CATALINA_BASE”。

JAVA_OPTS 为 -XX:MaxNewSize=256m 。

四、性能检查工具使用

定位内存泄漏:

JProfiler 工具的主要作用是检查和跟踪系统(仅限于 Java 开发的系统)的性能。它能够通过实时监控系统的内存使用情况,以及随时监视垃圾回收和线程运行状况等方式,很好地对 JVM 的运行情况及其性能进行监视。

应用服务器的内存长期被不合理地占用着,内存时常处于高位的占用状态,并且很难将其回收到低位。

应用服务器很不稳定,差不多每两天就得重新启动一次,有的时候甚至每天都要重新启动一次。

应用服务器时常会进行 Full GC(垃圾回收),其持续时间较长,大概在 30 到 40 秒左右。在应用服务器进行 Full GC 期间,它不会响应客户的交易请求,这对系统性能产生了很大的影响。

开发环境与产品环境存在不同,所以该问题有时会在产品环境中出现。通常可以借助工具来跟踪系统的内存使用状况。在某些个别情况下,或许在某个时刻确实因使用了大量内存而出现 out of memory 的情况,此时应继续跟踪,查看后续是否会有所下降。

如果一直居高不下这肯定就因为程序的原因导致内存泄漏。

五、不健壮代码的特征及解决办法

尽早释放无用对象的引用。当使用临时变量时,可让引用变量在退出活动域后自动设置为 null,以此暗示垃圾收集器来收集该对象,从而防止发生内存泄露。

如果实例仍然有指针指向,那么 jvm 就不会回收该资源。因为垃圾回收会把值为 null 的对象当作垃圾,这样做可以提高 GC 回收机制的效率。

我们的程序在处理过程中不可避免地要大量运用字符串处理。在这种情况下,应避免使用 String ,而应大量使用 StringBuffer 。因为每一个 String 对象都需要独立占用内存中的一块区域。

String str = "aaa";

String str2 = "bbb";

String str3 = str + str2;//

假如执行此次之后的 str 和 str2 以后不再被调用,那么它们就会被放在内存中,等待 Java 的 gc 去回收。在程序中如果过多地出现这种情况,就会报上面那个错误。建议在使用字符串时,如果能够使用 StringBuffer 就不要使用 String,这样可以节省不少开销。

尽量少使用静态变量,原因在于静态变量是全局性质的,并且垃圾回收器不会对其进行回收。

避免集中创建对象,尤其是大对象。因为这样的话,JVM 会突然需要大量内存,在这种情况下,必然会触发 GC 来优化系统内存环境。同时,要显示地声明数组空间,并且申请的数量还极大。

这是一个案例想定供大家警戒

使用 jspsmartUpload 来进行文件上传,在运行过程中时常会出现 java.outofMemoryError 这样的错误。经过检查,发现了问题所在,即组件中的代码:

m_totalBytes 等于 m_request 获取到的内容长度;

创建了一个名为 m_binArray 的数组,该数组的元素类型为 byte,其长度为 m_totalBytes 。

问题原因在于 totalBytes 这个变量所得到的数非常大,从而使得该数组分配了众多的内存空间,并且该数组无法及时进行释放。解决办法仅仅只能更换一种更为合适的方式,至少是那种不会引发 outofMemoryError 的方式来予以解决。

尽量利用对象池技术来提升系统性能;当生命周期长的对象拥有生命周期短的对象时,往往会导致内存泄漏,比如大集合对象拥有大数据量的业务对象时,可考虑分块进行处理,接着采用解决一块就释放一块的策略。

不要在经常被调用的方法里创建对象,尤其忌讳在循环过程中创建对象。可以适当地运用 hashtable 和 vector 来创建一组对象容器,接着从容器中获取那些对象,而无需每次创建完 new 后又将其丢弃。

一般是在开启大型文件时,或者与数据库一次获取了过多数据时,会出现 Out Of Memory Error 的状况。在这种情况下,大概需要计算一下数据量的最大值是多少,并且设定所需的最小内存空间值和最大内存空间值。

添加小唐老师微信免费领取

【jmeter课程】

目前100000+人已关注加入我们

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读