堆内存结构
堆( Heap ):这是 JVM 中最大的一块内存区域,主要功能是存放 Java 对象实例和数组。堆区是线程共享的,所有的对象实例以及数组都要在堆上分配。堆内存还可以按照垃圾分代收集的角度划分为年轻代和老年代。年轻代又进一步细分为 Eden 区、From Survivor 区和 To Survivor 区。
年轻代( Young Generation )
Eden 区:存放新建对象。当 Eden 区空间填满时,会触发 Minor GC ,回收不再被引用的对象,并将存活的对象移动到 Survivor 区。
Survivor 区:Survivor 区分为两个大小相等的空间,通常称为 From Space(或 Survivor0 区/ S0)和 To Space(或 Survivor1 区/ S1)。在 Minor GC 过程中,Eden 区和 From Space 中的存活对象会被复制到 To Space ,同时清空 Eden 区和 From Space 。然后,From Space 和 To Space 的角色会互换。这样,保证了在任意时刻,总有一个 Survivor 区是空的,有助于减少内存碎片。
Eden 区与 Survivor 区的比例:默认情况下,Eden 区与 Survivor 区的比例是8:1:1。这个比例可以通过 JVM 启动参数 -XX:SurvivorRatio= 进行调整,其中 表示 Eden 区与一个 Survivor 区的比例。老年代( Old Generation ):存放经过多次 Minor GC 后仍然存活的对象(即生命周期较长的对象)。
GC 类型:当老年代空间填满时,会触发 Full GC 或 Major GC 。Full GC 是回收老年代和年轻代的垃圾对象,触发 Full GC 会出现 STW ( stop the world )现象,即挂起所有进程等待清理垃圾。Full GC 的速度通常比 Minor GC 慢,因为涉及的对象更多,且可能涉及整个堆内存的扫描。Major GC 是回收老年代的垃圾对象。-
注: 永久代( PermGen Space ):在 JDK 7 及以前版本中,永久代用于存储类的元数据、常量、静态变量等信息。但在 JDK 8 及更高版本中,永久代被元空间( Metaspace )所取代。
对象的生命周期
堆内存中对象的生命周期是指从对象被创建到对象被垃圾回收器(GC)回收所经历的一系列阶段。
- 新建对象会存放在堆内存中年轻代的 Eden 区。当 Eden 区空间被对象占满时,会触发普通的回收器 Minor GC 。而回收器会判断且标记年轻代的对象( Unreferenced——垃圾的对象、 Referenced——可用的对象),然后清理垃圾对象,并将幸存对象(没有被清除对象)必须放入 S0 或 S1 中(即每次所有幸存对象只放入一个 Survivor Space 中,另一个 Survivor Space 为空)。对象上的数字代表幸存次数(年龄),每次幸存后都会换 Survivor Space ,且年龄加1。
- 当 Eden 区空间空余后则会继续存放新建对象,其被对象占满时又会触发 Minor GC …重复步骤1过程,而幸存对象则会在 S0 或 S1 区域中反复移动,年龄随着移动一次就增加一次。当幸存对象的年龄达到了设置的某个值后,便会从年轻代进入老年代。当老年代空间填满时,则会触发 Major GC 或 Full GC 。
常用的内存配置参数
在 Java 的 JVM 调优中,内存的配置和管理是至关重要的一环。内存的配置参数直接影响到 Java 应用程序的性能、稳定性和资源利用率。
堆内存( Heap Memory )
-Xms:设置 JVM 启动时堆的初始大小。这个值可以根据应用程序的内存需求来设置,以避免 JVM 在启动后频繁地调整堆大小,从而提高性能。
-Xmx:设置 JVM 可以使用的堆的最大值。这个值可以根据系统的物理内存大小和应用程序的内存需求来设置,以防止内存溢出。
注:Server 端 JVM 最好将 -Xms 和 -Xmx 设为相同值,避免每次垃圾回收完成后 JVM 重新分配内存,也可以减少垃圾回收次数。
-XX:NewSize:设置年轻代的初始大小。
-XX:MaxNewSize:设置年轻代的最大大小。若未设置,则默认值为老年代的大小。
-XX:NewRatio:设置老年代与年轻代的比例。如 -XX:NewRatio=3 表示老年代是年轻代的3倍。非堆内存( Non-Heap Memory )
-XX:MaxMetaspaceSize:设置元空间的最大值。在 Java 8 及以后版本中,元空间用于存储类的元数据,这个参数用于限制元空间的增长,以防止过多的内存占用。
-XX:MaxPermSize:在 Java 8 之前的版本中,用于设置永久代的最大值。由于永久代在Java 8中已被元空间取代,因此这个参数在Java 8及以后版本中不再使用。
-XX:PermSize:设置永久代的初始内存大小。其他内存相关参数
-Xss:设置每个线程的栈空间大小。栈空间用于存储线程执行时的局部变量和方法调用等信息。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右(小系统:-Xss126K ,大系统:-Xss256K )。
JVM 的内存配置参数是 JVM 调优的重要组成部分。通过合理配置堆内存、非堆内存以及选择合适的垃圾回收器及其参数,可以显著提高 Java 应用程序的性能、稳定性和资源利用率。然而,需要注意的是,JVM 调优是一项复杂的任务,需要根据具体的应用程序特性和工作负载来进行评估和测试。因此,在进行 JVM 调优时,建议首先进行性能分析和监控,找出潜在的瓶颈和问题点,然后针对性地调整参数,并进行实际测试和验证。
内存泄漏
内存泄漏( Memory Leak ),指程序中已不再需要的内存却未被释放的情况。一次内存泄露的危害可以忽略,但内存泄露堆积的后果是内存溢出。
根据发生方式的不同,内存泄漏可以分为四类:
常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。常发性内存泄漏危害性较大,因为其会随着时间的推移而不断累积,最终可能导致系统资源耗尽。
偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的,对于特定的环境,偶发性的内存泄漏也可能变成常发性的。因此,测试环境和测试方法对检测这类内存泄漏至关重要。
一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅一块内存发生泄漏。一次性内存泄漏不会重复发生,但仍会导致内存资源的浪费和系统性能的下降。
隐式内存泄漏:程序在运行过程中不停地分配内存,但是直到结束的时候才释放内存。严格地说,这里并没有发生真正的内存泄漏,因为最终程序释放了所有申请的内存。然而,对于一个需要长时间运行的服务器程序来说,如果不及时释放内存,也可能导致最终耗尽系统的所有内存。隐式内存泄漏危害性较大,因为其较难被检测到,且随着时间的推移,会对系统性能和稳定性产生负面影响。
常见原因:
1.对象未被释放:程序中的对象已经不再被访问,但没有手动释放。
2.静态引用:静态变量一直占用内存空间,如果静态变量引用了一个对象,那么这个对象将无法被垃圾回收器回收。
3.长生命周期对象持有短生命周期对象的引用:长生命周期对象持有一个短生命周期对象的引用,导致短生命周期对象无法被垃圾回收。
4.线程资源未被释放:线程资源未被正确释放,导致内存泄漏。
解决方法:
1.使用内存分析工具:如 JProfiler 、VisualVM 、MAT 等工具,分析程序中的对象占用的内存空间,找出哪些对象无法被释放。
2.分析堆栈信息:通过堆栈信息找出哪些对象被持有了过长时间。
3.代码审查:使用代码审查工具找出潜在的内存泄漏问题,如未释放对象、未关闭流等。
4.优化代码:及时释放不再需要的对象,避免使用静态变量引用对象,注意长生命周期对象和短生命周期对象之间的引用关系。
内存溢出
内存溢出( Out Of MemoryError ),指程序申请的内存超过了虚拟机所能提供的内存大小。
常见原因:
1.堆内存不足:过大的对象或数据集 —— 一次性加载到内存中的对象或数据集过大,超出了 JVM 堆内存的限制。内存泄漏 —— 长时间运行的应用程序中,对象被持续引用但不再使用,导致这些对象无法被垃圾回收器回收,从而逐渐耗尽堆内存。
2.永久代/元空间不足:在 Java 8 之前,JVM 使用永久代来存储类的元数据,如类和方法信息。如果加载的类太多,可能会导致永久代内存溢出。Java 8 及更高版本用元空间取代了永久代,用于存储类的元数据。如果元空间设置得太小,而加载的类又太多,也会导致内存溢出。
3.栈溢出( StackOverflowError ):这不是堆内存溢出,但经常与内存问题一起讨论。栈溢出通常是由于过深的递归调用或大量的嵌套方法调用导致的。
4.直接内存( Direct Memory )溢出:在使用 NIO 时,可以通过 ByteBuffer.allocateDirect() 等方法分配直接内存。直接内存不是 JVM 堆内存的一部分,而是由操作系统管理。如果直接内存分配过多,也可能导致内存溢出。
5.第三方库或框架的问题:使用的第三方库或框架可能存在内存管理上的缺陷,导致内存泄漏或不必要的内存占用。
6.系统限制:操作系统可能对 JVM 进程可使用的内存总量有限制。
解决方法:
第一步,修改 JVM 启动参数,直接增加内存
第二步,检查错误日志
第三步,对代码进行排查分析,找出可能发生内存溢出的位置
- 1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询,尤其大的系统。
- 2.检查代码中是否有死循环或递归调用。
- 3.检查是否有大循环重复产生新对象实体。
- 4.检查 List 、Map 等集合对象是否有使用完后,未清除的问题。集合对象会始终存有对对象的引用,使得这些对象不能被 GC 回收。
第四步,使用内存查看工具动态查看内存使用情况