Java虚拟机(JVM)的Java内存管理
Java 虚拟机(JVM)的 Java 内存管理
Java 内存管理是一项持续的挑战,也是正确调整应用程序可扩展性所必须掌握的核心技能。从根本上讲,这是关于分配新对象并正确回收未使用对象的过程。
在本文中,我们将深入探讨 Java 虚拟机(JVM),了解内存管理机制、内存监视工具、内存使用情况监控以及垃圾回收(GC)活动。正如您将看到的,有许多不同的模型、方法、工具和技巧可用于真正优化系统性能。
Java 虚拟机(JVM)
JVM 是使计算机能够运行 Java 程序的抽象计算机。JVM 包含三个核心概念:
- 规范(Specification):指定 JVM 的工作方式(实现已由 Sun 和其他公司提供)。
- 实现(Implementation):称为 Java 运行时环境(JRE)。
- 实例(Instance):在编写 Java 命令之后运行 Java 类时,将创建 JVM 的实例。
JVM 负责加载代码、验证代码、执行代码、管理内存(包括从操作系统分配内存、管理 Java 分配、堆压缩和垃圾对象删除),并最终提供运行时环境。
Java(JVM)内存结构
JVM 内存分为多个部分:堆内存(Heap Memory)、非堆内存(Non-Heap Memory)和其他。

图 1.1 来源 https://www.yourkit.com/docs/kb/sizes.jsp
堆内存
堆内存是运行时数据区,所有 Java 类实例和数组的内存均从中分配。JVM 启动时会创建堆,并且在应用程序运行时堆的大小可能会增加或减少。
- 可以使用
-XmsVM 选项指定堆的初始大小。 - 堆可以是固定大小的,也可以是可变大小的,具体取决于垃圾回收策略。
- 可以使用
-Xmx选项设置最大堆大小。默认情况下,最大堆大小通常设置为 64 MB(取决于具体 JVM 实现)。
非堆内存
JVM 具有堆以外的内存,称为非堆内存。它是在 JVM 启动时创建的,存储每个类的结构,例如运行时常量池、字段和方法数据、方法和构造函数的代码以及内部字符串。非堆内存的默认最大大小通常为 64 MB,可以使用 -XX:MaxPermSize VM 选项更改(注:Java 8 后已被元空间取代)。
其他内存
JVM 使用此空间存储 JVM 代码本身、JVM 内部结构、已加载的探查器代理代码和数据等。
Java(JVM)堆内存结构

图 1.2 来源 http://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java
JVM 堆在物理上分为两部分(或几代):新生代(Young Generation) 和 老年代(Old Generation)。
- 新生代:是为分配新对象而保留的堆的一部分。当新生代变满时,将通过运行特殊的年轻代集合(Young Collection) 来收集垃圾。其中将在新生代中生活了足够长的所有对象提升(移动)到老年代,从而腾出新生代用于更多对象分配。此垃圾回收称为 Minor GC。新生代分为三个部分:伊甸园内存区(Eden Space) 和两个 幸存者内存空间(Survivor Space)。
有关新生代空间的要点:
- 大多数新创建的对象位于 Eden 内存空间中。
- 当 Eden 空间中充满对象时,将执行 Minor GC,并将所有幸存者对象移至其中一个 Survivor 空间。
- Minor GC 还可以检查幸存者对象并将其移动到其他 Survivor 空间。因此,一次通常有一个 Survivor 空间是空的。
- 在许多 GC 循环中幸存下来的对象将移至旧的内存空间。通常,可以通过设置对象的年龄阈值,使其有资格晋升为老年代。
当老年代内存已满时,将在此处收集垃圾,该过程称为旧集合(Old Collection)。老年代存储器包含经过多次 Minor GC 寿命长且可以存活的对象。通常,垃圾回收已满时在老年代内存中执行。较旧的垃圾回收称为 Major GC 或 Full GC,通常需要更长的时间。
新生代背后的原因是,大多数对象都是临时的且寿命短。一个年轻代集合旨在迅速找到仍然活着的新分配对象并将其移出新生代。通常,年轻代集合释放给定数量的内存比单代堆(没有新生代的堆)的旧集合或垃圾回收要快得多。
最新版本包括称为 保留区(Retained Region) 的新生代的一部分,并且已保留。保留区域包含新生代中最近分配的对象,直到下一代出现时才进行垃圾收集。这样可以防止仅由于在开始年轻收集之前就已分配了对象而对其进行升级。
Java 内存模型
永久生成(从 Java 8 开始由 Metaspace 代替)
永久生成或"Perm Gen"包含 JVM 所需的应用程序元数据,用于描述应用程序中使用的类和方法。JVM 在运行时根据应用程序使用的类填充 PermGen。Perm Gen 还包含 Java SE 库类和方法。Perm Gen 对象是在完整垃圾收集中收集的垃圾。
元空间(Metaspace)
在 Java 8 中,没有 Perm Gen,这意味着不再有 java.lang.OutOfMemoryError: PermGen space 问题。与驻留在 Java 堆中的 Perm Gen 不同,Metaspace 不是堆的一部分。现在,类元数据的大多数分配都是从本机内存中分配的。默认情况下,元空间会自动增加其大小(达到基础操作系统所提供的大小),而 Perm Gen 始终具有固定的最大大小。
可以使用两个新标志来设置元空间的大小:
-XX:MetaspaceSize-XX:MaxMetaspaceSize
Metaspace 背后的主题是类及其元数据的生存期与类加载器的生存期匹配。也就是说,只要类加载器处于活动状态,元数据就在 Metaspace 中保持活动状态,并且无法释放。
代码缓存(Code Cache)
运行 Java 程序时,它将以分层方式执行代码。在第一层中,它使用客户端编译器(C1 编译器)以便通过检测来编译代码。分析数据在第二层(C2 编译器)中用于服务器编译器,以优化的方式编译该代码。默认情况下,Java 7 中未启用分层编译,而 Java 8 中已启用分层编译。
即时(JIT)编译器将编译后的代码存储在称为代码缓存的区域中。这是一个特殊的堆,用于存放已编译的代码。如果该区域的大小超过阈值,则将刷新该区域,并且 GC 无法重新定位这些对象。
Java 8 中已解决了一些性能问题和编译器未重新启用的问题,而在 Java 7 中避免这些问题的解决方案之一是将代码缓存的大小增加到一个永远无法达到的程度。
方法区(Method Area)
方法区是 Perm Gen 中空间的一部分(Java 8 后对应 Metaspace 的一部分),用于存储类结构(运行时常量和静态变量)以及方法和构造函数的代码。
内存池(Memory Pool)
内存池是由 JVM 内存管理器创建的,用于创建不可变对象的池。内存池可以属于 Heap 或 Perm Gen,具体取决于 JVM 内存管理器的实现。
运行时常量池(Runtime Constant Pool)
运行时常量池是类中常量池的每类运行时表示形式。它包含类运行时常量和静态方法。运行时常量池是方法区域的一部分。
Java 堆栈内存
Java 堆栈内存用于执行线程。它们包含短期的特定于方法的值以及对从该方法引用的堆中其他对象的引用。
Java 堆内存开关
Java 提供了很多内存开关,我们可以用来设置内存大小及其比率。一些常用的内存开关是:
| VM 开关 | VM 开关说明 |
|---|---|
-Xms | 用于在 JVM 启动时设置初始堆大小 |
-Xmx | 用于设置最大堆大小 |
-Xmn | 为了确定年轻一代的大小,其余空间留给了老年代 |
-XX:PermSize | 用于设置永久存储器的初始大小(Java 8 前) |
-XX:MaxPermSize | 用于设置 Perm Gen 的最大大小(Java 8 前) |
-XX:SurvivorRatio | 为了提供 Eden 空间的比例。例如,如果年轻一代的大小为 10m,并且 VM 开关为 -XX:SurvivorRatio=8,则将为 Eden 空间保留 8m,为两个 Survivor 空间保留 2m。默认值为 8 |
-XX:NewRatio | 用于提供新旧大小的比例。默认值为 2 |
垃圾收集
垃圾回收是释放堆中空间以分配新对象的过程。Java 的最佳功能之一是自动垃圾收集。垃圾收集器是在后台运行的程序,它可以查看内存中的所有对象并找出程序的任何部分未引用的对象。删除所有这些未引用的对象,并回收空间以分配给其他对象。垃圾收集的基本方法之一涉及三个步骤:
- 标记(Marking):这是第一步,垃圾回收器将识别正在使用的对象和未使用的对象。
- 正常删除(Sweeping):垃圾收集器删除未使用的对象并回收要分配给其他对象的可用空间。
- 压缩(Compacting):为了获得更好的性能,在删除未使用的对象之后,可以将所有剩余的对象移动到一起。这将提高内存分配给较新对象的性能。
垃圾收集的标记和扫描模型
JVM 使用标记和清除垃圾收集模型来执行整个堆的垃圾收集。标记和清除垃圾收集包含两个阶段:标记阶段和清除阶段。
在标记阶段,将从 Java 线程、本机处理程序和其他根源可访问的所有对象,以及从这些对象可访问的对象等标记为活动。此过程将识别并标记所有仍在使用的对象,其余的可以视为垃圾。
在清除阶段,遍历堆以查找活动对象之间的间隙。这些间隙记录在空闲列表中,可用于新对象分配。
Java 垃圾回收类型
我们可以在应用程序中使用五种类型的垃圾收集类型。我们只需要使用 JVM 开关来为应用程序启用垃圾回收策略即可。
串行 GC(-XX:+UseSerialGC):串行 GC 使用简单的 mark-sweep-compact 方法进行年轻一代和老一代的垃圾回收,即 Minor 和 Major GC。
要启用串行收集器,请使用:
-XX:+UseSerialGC并行 GC(-XX:+UseParallelGC):并行 GC 与串行 GC 相同,不同之处在于,它生成 N 个线程用于年轻一代垃圾回收,其中 N 是系统中 CPU 内核的数量。我们可以使用 -XX:ParallelGCThreads=n JVM 选项来控制线程数。这是 JDK 8 中 JVM 的默认收集器。
要启用并行 GC,请使用:
-XX:+UseParallelGC并行旧 GC(-XX:+UseParallelOldGC):与并行 GC 相同,只是它使用多个线程进行年轻代和旧代垃圾回收。
要启用并行 OLDGC,请使用:
-XX:+UseParallelOldGC并发标记扫描(CMS)收集器(-XX:+UseConcMarkSweepGC):CMS 也称为并发低暂停收集器。它为老一代进行垃圾收集。CMS 收集器试图通过在应用程序线程中同时执行大多数垃圾收集工作来最大程度地减少由于垃圾收集而造成的暂停。年轻一代的 CMS 收集器使用与并行收集器相同的算法。此垃圾收集器适用于响应性应用程序,在这些应用程序中我们无法承受更长的暂停时间。我们可以使用 -XX:ParallelCMSThreads=n JVM 选项来限制 CMS 收集器中的线程数。
要启用 CMS 收集器,请使用:
-XX:+UseConcMarkSweepGCG1 垃圾收集器(-XX:+UseG1GC):Java 7 中提供了垃圾优先或 G1 垃圾收集器,其长期目标是替换 CMS 收集器。G1 收集器是一个并行、并发且增量紧凑的低中断垃圾收集器。垃圾优先收集器不能像其他收集器那样工作,也没有年轻一代空间的概念。它将堆空间分成多个相等大小的堆区域。调用垃圾收集器时,它将首先收集活动数据较少的区域,因此称为“垃圾优先”。
要启用 G1 收集器,请使用:
-XX:+UseG1GC计划将 G1 作为并发标记扫描收集器(CMS)的长期替代产品。将 G1 与 CMS 进行比较,有一些差异使 G1 成为更好的解决方案。一个区别是 G1 是压紧收集器。G1 足够紧凑,可以完全避免使用细粒度的空闲列表进行分配,而是依赖于区域。这大大简化了收集器的各个部分,并消除了潜在的碎片问题。同样,G1 提供了比 CMS 收集器更可预测的垃圾收集暂停,并允许用户指定所需的暂停目标。
在 Java 8 中,G1 收集器具有惊人的优化功能,即 String Deduplication。它使 GC 能够识别在堆中多次出现的字符串,并修改它们以指向同一内部 char[] 数组,从而使堆中没有多个副本。可以使用 -XX:+UseStringDeduplication JVM 参数启用它。
G1 是 JDK 9 中 的 默认 垃圾收集器。
使用案例
- 超过 50%的 Java 堆被实时数据占用。
- 对象分配率或提升率差异很大。
- 不必要的长时间垃圾收集或压缩暂停(长于 0.5 到 1 秒)。
监视内存使用和 GC 活动
内存不足通常是 Java 应用程序不稳定和无响应的原因。因此,我们需要监视垃圾收集对响应时间和内存使用的影响,以确保稳定性和性能。但是,仅监视这两个元素并不能告诉我们应用程序响应时间是否受垃圾收集影响,因此监视内存利用率和垃圾收集时间是不够的。只有 GC 挂起会直接影响响应时间,并且 GC 也可以与应用程序并行运行。因此,我们需要将垃圾收集导致的暂停与应用程序的响应时间相关联。基于此,我们需要监视以下内容:
- 利用不同的内存池(Eden,Survivor 和老年代)。内存不足是增加 GC 活动的第一原因。
- 如果尽管进行了垃圾回收,但总体内存利用率仍在不断提高,则存在内存泄漏,这将不可避免地导致 内存不足(OutOfMemoryError)。在这种情况下,必须进行内存堆分析。
- 年轻一代收藏的数量提供了有关流失率(对象分配率)的信息。数字越高,分配的对象越多。大量的年轻藏品可能是响应时间问题和老一辈人成长的原因(因为年轻一代无法再应付大量物品)。
- 如果在 GC 之后老年代的利用率波动很大而又没有上升,则对象会不必要地从年轻一代复制到老一代。可能有以下三个原因:年轻一代太小,流失率高或事务性内存使用过多。
- 高 GC 活动通常会对 CPU 使用率产生负面影响。但是,只有暂停(Stop-The-World 事件)会直接影响响应时间。与大众观点相反,停赛不限于主要 GC。因此,重要的是要监视与应用程序响应时间相关的挂起。
jstat
jstat 工具使用内置在 Java HotSpot 虚拟机的仪器,提供有关运行应用程序的性能和资源消耗的信息。诊断性能问题,尤其是与堆大小和垃圾回收有关的问题时,可以使用该工具。jstat 实用程序不需要虚拟机与任何特殊的选项启动。默认情况下,Java HotSpot VM 中的内置工具是启用的。该实用程序包含在所有操作系统的 JDK 下载中。jstat 实用程序使用虚拟机标识符(VMID)来识别目标的过程。
使用带有 gc 选项的 jstat 命令来查找 JVM 堆内存使用情况。
<JAVA_HOME>/bin/jstat -gc <JAVA_PID>
图 1.3
| 字段 | 说明 |
|---|---|
| S0C | Current survivor space 0 capacity (KB) |
| S1C | Current survivor space 1 capacity (KB) |
| S0U | Survivor space 0 utilization (KB) |
| S1U | Survivor space 1 utilization (KB) |
| EC | Current eden space capacity (KB) |
| EU | Eden space utilization (KB) |
| OC | Current old space capacity (KB) |
| OU | Old space utilization (KB) |
| MC | Metaspace capacity (KB) |
| MU | Metaspace utilization (KB) |
| CCSC | Compressed class space capacity (KB) |
| CCSU | Compressed class space used (KB) |
| YGC | Number of young generation garbage collection events |
| YGCT | Young generation garbage collection time |
| FGC | Number of full GC events |
| FGCT | Full garbage collection time |
| GCT | Total garbage collection time |
表 1.2
jmap
jmap 实用程序针对运行中的 VM 或核心文件的存储相关的统计信息。JDK 8 引入了 Java Mission Control, Java Flight Recorder 和 jcmd 实用程序,用于诊断 JVM 和 Java 应用程序的问题。建议使用最新的实用程序 jcmd 代替 jmap 实用程序,以增强诊断功能并降低性能开销。
-heap 选项可用于获取以下 Java 堆信息:
- GC 算法特定的信息,包括 GC 算法的名称(例如,并行 GC)和特定于算法的详细信息(例如,并行 GC 的线程数)。
- 堆配置可能已被指定为命令行选项或由 VM 根据计算机配置选择。
- 堆使用情况摘要:对于每一代(堆的区域),该工具都会显示堆的总容量,使用中的内存和可用的可用内存。如果将一个世代组织为一组空间(例如,新一代),则将包括一个特定于空间的内存大小摘要。
<JAVA_HOME>/bin/jmap -heap <JAVA_PID>
图 1.4
jcmd
jcmd 工具被用来发送诊断命令请求到 JVM,这些请求是控制 Java 的飞行录像,排查有用的,诊断 JVM 和 Java 应用程序。它必须在运行 JVM 的同一台计算机上使用,并且必须具有用于启动 JVM 的相同有效用户和组标识符。
可以使用以下命令创建堆转储(hprof 转储):
jcmd <JAVA_PID> GC.heap_dump filename=<文件>上面的命令与使用以下命令相同:
jmap -dump:file=<文件> <JAVA_PID>但是 jcmd 是推荐使用的工具。
jhat
jhat 工具提供了一个方便的手段来浏览对象拓扑在堆快照。该工具替代了堆分析工具(HAT)。该工具以二进制格式解析堆转储(例如,jcmd 产生的堆转储)。该实用程序可以帮助调试 意外对象关系。该术语用于描述不再需要的对象,但由于通过根集中的某些路径进行引用而使该对象保持活动状态。例如,如果在不再需要该对象之后仍然保留了对该对象的无意识静态引用,或者在不再需要该对象时观察者或侦听器无法从其对象注销自己,或者引用了该对象的线程,则可能会发生这种情况。对象在应有的情况下不会终止。意外对象关系是 Java 语言的内存泄漏等效项。
我们可以使用以下命令使用 jhat 分析堆转储:
jhat <HPROF_FILE>此命令读取 .hprof 文件并在端口 7000 上启动服务器。

图 1.5
当我们使用 http://localhost:7000 连接到服务器时,我们可以执行标准查询或创建对象查询语言(OQL)。默认情况下显示“所有类”查询。该默认页面显示堆中存在的所有类,平台类除外。该列表按完全限定的类名排序,并按包分类。单击一个类的名称以转到“类”查询。此查询的第二个变体包括平台类。平台类包括其完全限定名称以诸如 java,sun 或 javax.swing 之类的前缀开头的类。另一方面,类查询显示有关类的信息。这包括其超类,任何子类,实例数据成员和静态数据成员。在此页面上,您可以导航到所引用的任何类,也可以导航至实例查询。
HPROF
HPROF 是每个 JDK 版本附带的用于堆和 CPU 性能分析的工具。它是一个动态链接库(DLL),它使用 Java 虚拟机工具接口(JVMTI)与 JVM 接口。该工具以 ASCII 或二进制格式将概要分析信息写入文件或套接字。HPROF 工具能够显示 CPU 使用率,堆分配统计信息并监视争用概要文件。另外,它可以报告完整的堆转储以及 JVM 中所有监视器和线程的状态。在诊断问题方面,HPROF 在分析性能,锁争用,内存泄漏和其他问题时很有用。
我们可以使用以下命令调用 HPROF 工具:
java -agentlib:hprof ToBeProfiledClass
java -agentlib:hprof=heap=sites ToBeProfiledClass根据请求的分析类型,HPROF 指示 JVM 将其发送到相关事件。然后,该工具将事件数据处理为配置文件信息。默认情况下,堆分析信息被写到当前工作目录中的 java.hprof.txt(ASCII)。
以下命令可用于获取堆分配配置文件。堆概要文件中的关键信息是程序各个部分中发生的分配量。
javac -J-agentlib:hprof=heap=sites Hello.java同样,可以使用 heap=dump 选项获得 堆转储。输出包括由垃圾收集器确定的根集,以及可以从根集访问的堆中每个 Java 对象的条目。
javac -J-agentlib:hprof=heap=dump Hello.javaHPROF 工具可以通过采样线程来收集 CPU 使用率信息。以下命令可用于获取 CPU 使用率采样概要文件结果:
javac -J-agentlib:hprof=cpu=samples Hello.javaHPROF 代理会定期对所有正在运行的线程的堆栈进行采样,以记录最频繁的活动堆栈跟踪。
还有其他工具,例如 VisualVM,它以 GUI 的形式向我们提供了内存使用情况,垃圾回收,堆转储,CPU 和内存分析等详细信息。
VisualVM
VisualVM 是从 NetBeans 平台派生的工具,其体系结构在设计上是模块化的,这意味着很容易通过使用插件进行扩展。当 Java 应用程序在 JVM 上运行时,VisualVM 允许我们获取有关 Java 应用程序的详细信息,它可以在本地或远程系统中。可以使用 Java 开发工具包(JDK)工具检索生成的数据,并且可以快速查看本地和远程运行的应用程序上多个 Java 应用程序上的所有数据和信息。也可以保存和捕获有关 JVM 软件的数据,并将数据保存到本地系统。VisualVM 可以执行 CPU 采样,内存采样,运行垃圾回收,分析堆错误,创建快照等等。
启用 JMX 端口
我们可以通过在启动 Java 应用程序时添加以下系统属性来启用 JMX 远程端口:
-Dcom.sun.management.jmxremote-Dcom.sun.management.jmxremote.port=<端口>-Dcom.sun.management.jmxremote.authenticate=false(注:原文此处截断,建议补充完整配置以确保安全或明确说明)
现在,我们可以使用 VisualVM 连接到远程计算机,并查看 CPU 利用率,内存采样,线程等。通过 JMX 远程端口连接时,我们还可以在远程计算机上生成线程转储和内存转储。
图 1.6 显示了在本地和远程系统上运行的应用程序列表。要连接到远程系统,请右键单击“远程”并添加主机名,然后在“高级设置”下定义在远程计算机上启动应用程序时使用的端口。一旦在本地或远程部分下列出了应用程序,请双击它们以查看应用程序的详细信息。

图 1.6
该应用程序的详细信息有四个选项卡:“概述”,“监视器”,“线程”和“采样器”。
在 概述 选项卡包含有关启动的应用程序的主要信息。概述选项卡中提供了主类,命令行参数,JVM 参数,PID,系统属性以及任何已保存的数据(例如线程转储或堆转储)。
有趣的选项卡是 监视 选项卡。此选项卡显示应用程序的 CPU 和内存使用情况。此视图中有四个图形。

图 1.7
第一个图显示 CPU 使用率和垃圾收集器 CPU 使用率。X 轴显示时间戳与利用率的百分比。
右上方的第二个图显示堆空间和 Perm Gen 空间或元空间。它还显示堆内存的最大大小,应用程序正在使用的内存量以及可用的内存量。该图在分析遇到 java.lang.OutOfMemoryError: Java heap space 错误的应用程序中特别有用。当应用程序正在执行内存密集型作业时,已使用的堆(在图上以蓝色表示)应始终小于堆大小(在图上以橙色表示)。当使用的堆与堆大小几乎相同时,或者当没有更多空间供系统分配/扩展堆大小并且使用的堆不断增加时,我们可以预料到堆错误。可以通过“堆转储”获取有关堆的更多信息。当出现内存不足错误时,可以通过添加以下的 VM 参数来获得堆转储:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=[文件路径]这将使 .hprof 文件可以在指定的路径中创建。

图 1.8
图 1.8 显示了其中一个应用程序的堆转储。摘要选项卡显示一些基本信息,例如类总数,实例总数,类加载器,GC 根目录以及应用程序运行所在的环境详细信息。图 1.8 中的分析显示了分配最多的对象类型以及发生这些分配的位置。大型对象会在其构造函数中创建许多其他对象,或者具有许多字段。我们还应该分析在生产条件下大规模并发的代码区域。在负载下,这些位置不仅会分配更多空间,而且还会增加内存管理本身的同步性。高内存利用率是导致大量垃圾回收的原因。在某些情况下,硬件限制使得不可能简单地增加 JVM 的堆大小。在其他情况下,增加堆大小并不能解决问题,只会延迟问题,因为利用率一直在增长。使用堆转储标识内存泄漏和标识内存消耗者,可以进行以下分析。
不再需要但仍被应用程序引用的每个对象都可以视为内存泄漏。实际上,我们只关心正在增长的或占用大量内存的内存泄漏。典型的内存泄漏是指重复创建指定的对象类型但不进行垃圾收集的泄漏。为了标识此对象类型,需要多个堆转储,可以使用趋势转储进行比较。每个 Java 应用程序都有大量的 String, char[] 和其他 Java 标准对象。实际上,String 和 char[] 通常具有最多的实例数,但是分析它们将使我们无所适从。即使我们泄漏 String 对象,也很可能是因为它们被应用程序对象引用,这代表了泄漏的根本原因。因此,专注于我们的应用程序类别将产生更快的结果。
我们有几种情况需要详细分析:
- 趋势分析并没有导致我们内存泄漏。
- 我们的应用程序使用了过多的内存,但是没有明显的内存泄漏,因此我们需要优化代码。
- 我们无法进行趋势分析,因为内存增长过快并且 JVM 崩溃了。
在所有三种情况下,根本原因很可能是一个或多个对象位于较大对象树的根目录。这些对象可防止垃圾回收树中的许多其他对象。如果发生内存不足错误,则少数对象可能会阻止释放大量对象,从而触发内存不足错误。堆的大小通常是内存分析的大问题。生成堆转储需要内存本身。如果堆大小处于可用或可能的限制(32 位 JVM 不能分配超过 3.5 GB),则 JVM 可能无法生成一个。另外,堆转储将挂起 JVM。手动找到一个阻止整个对象树迅速被垃圾收集的对象成为大海捞针。
幸运的是,诸如 Dynatrace 之类的解决方案能够自动识别这些对象。为此,我们需要使用基于图论的控制算法。该算法应该能够计算对象树的根。除了计算对象树的根,内存分析工具还可以计算特定树的内存量。这样,它可以计算哪些对象阻止释放大量内存–换句话说,哪个对象主导内存。

图 1.9
回到“监视器”选项卡下可用于应用程序的图(图 1.7),是位于左下角的类图。该图显示了应用程序中加载的类的总数,最后一个图显示了当前正在运行的线程数。通过这些图,我们可以查看我们的应用程序是否占用了过多的 CPU 或内存。
第三个选项卡是 线程 选项卡。

图 1.10
在“线程”选项卡中,我们可以看到应用程序的不同线程如何改变状态以及它们如何演化。我们还可以观察每个状态下的时间流逝以及有关线程的许多其他细节。有过滤选项可仅查看活动线程或完成线程。如果我们需要线程转储,则可以使用顶部的“线程转储”按钮获得它。
第四个选项卡是 采样器 选项卡。最初打开此选项卡时,它不包含任何信息。在查看信息之前,我们必须开始一种采样/分析。我们将从 CPU 采样开始。单击"CPU"按钮后,表中将显示 CPU 采样的结果。

图 1.11
从图 1.11 中,我们看到 doRun() 方法占用了 CPU 时间的 54.8%。我们还看到 getNextEvent() 和 readAvailableBlocking() 是接下来的两个消耗更多 CPU 时间的方法。
下一个采样是内存采样。采样期间将冻结应用程序,直到获取结果。从图 1.12,我们可以推断出应用程序存储了 Object, int 和 char 数组。

在这两种类型的采样中,我们都可以将结果保存起来,以备以后使用。例如,可以以固定间隔多次采样,然后可以比较结果。这可以帮助我们改进应用程序以使用更少的 CPU 和内存。最后,开发人员的任务是检查这些区域并改进代码。
Java 垃圾收集优化
Java 垃圾回收优化应该是我们用于提高应用程序吞吐量的最后一个选择,并且仅当由于较长的 GC 导致应用程序超时而导致性能下降时才使用。
如果遇到 java.lang.OutOfMemoryError: PermGen space 错误,请尝试使用 -XX:PermSize 和 -XX:MaxPermSize JVM 选项监视并增加 Perm Gen 的内存空间。对于 Java 8 及更高版本,我们看不到此错误。如果我们看到很多完整的 GC 操作,则应尝试增加旧的内存空间。总体而言,垃圾回收调整需要大量的精力和时间,对此没有硬性规定。我们需要尝试不同的选择并进行比较,以找出最适合我们应用的选择。
一些性能解决方案是:
- 应用软件采样/分析。
- 服务器和 JVM 调优。
- 正确的硬件和操作系统。
- 根据应用程序的行为和采样结果进行代码改进(说起来容易做起来难!)。
- 正确使用 JVM(具有最佳 JVM 参数)。
-XX:+UseParallelGC(如果有多处理器)。
请记住一些其他有用的技巧:
- 除非我们在暂停方面遇到问题,否则请尝试为 JVM 分配尽可能多的内存。
- 将
-Xms和-Xmx设置为相同的值。 - 请确保随着我们增加处理器数量而增加内存,因为分配可以并行化。
- 别忘了调整 Perm Gen(针对 Java 7 及以前)。
- 最小化同步的使用。
- 如果有好处,请使用多线程,并注意线程开销。另外,请确保它在不同环境中的工作方式相同。
- 避免过早创建对象。创作应接近实际使用地点。这是一个我们容易忽视的基本概念。
- JSP 通常比 servlet 慢。
- 使用
StringBuilder代替字符串 concat。 - 使用基元并避免对象(
long而不是Long)。 - 尽可能重用对象,并避免创建不必要的对象。
- 如果我们要测试一个空字符串,
equals()会很昂贵。请改用length属性。 "=="比equals()快(针对基本类型或引用地址比较)。n += 5快于n = n + 5。在第一种情况下,生成的字节码更少。- 定期刷新并清除休眠会话。
- 批量执行更新和删除。
为 GC 生成日志
垃圾收集器日志或 gc.log 是存储所有 JVM 内存清除事件的文本文件:MinorGC,MajorGC 和 FullGC。
使用以下参数启动 JVM 以创建 gc.log:
直到 Java 8
-XX:+PrintGCDetails -Xloggc:/app/tmp/myapp-gc.log从 Java 9 开始
-Xlog:gc*:file=/app/tmp/myapp-gc.log资料来源
- https://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/garbage_collect.html
- https://zh.wikipedia.org/wiki/Java_virtual_machine
- https://www.javatpoint.com/internal-details-of-jvm
- https://dzone.com/articles/java-performance-tuning
- http://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java
- https://www.yourkit.com/docs/kb/sizes.jsp
- https://www.infoq.com/articles/Java-PERMGEN-已移除
- http://blog.andresteingress.com/2016/10/19/java-codecache
- https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/toc.html
- https://javaperformance.wordpress.com/2017/02/05/java-heap-memory-usage-using-jmap-and-jstat-command/
说明:本文内容主要基于 Java 7 及 Java 8 时期的 JVM 特性整理。部分术语(如 PermGen)在 Java 8 及更高版本中已被 Metaspace 取代,默认垃圾收集器在 Java 9 后变更为 G1。实际使用时请参考对应 JDK 版本的官方文档。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-xu-ni-ji-jvm-de-java-nei-cun-guan-li.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。