原创

JVM实战(1)——理论基础篇:JAVA内存模型(JMM-java memory model)

温馨提示:
本文最后更新于 2023年07月10日,已超过 291 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

了解Java内存模型是对于严肃的Java开发人员来说必不可少的学习内容,他们开发、部署、监控、测试和调优Java应用程序的性能。在本博文中,我们将讨论Java内存模型以及JVM内存的每个部分如何对我们的程序运行做出贡献。

首先,请检查您是否理解以下JVM架构图。如果您对此不熟悉,我强烈建议您浏览一下我之前的博文("Java生态系统(第一部分):理解JVM架构"),并更新您的知识。

JVM内存模型

在运行资源密集型Java程序时,您可能会使用以下一些JVM内存配置。

-Xms设置:初始堆大小

-Xmx设置:最大堆大小

-XX:NewSize设置:新生代堆大小

-XX:MaxNewSize设置:最大新生代堆大小

-XX:MaxPermGen设置:永久代的最大大小

-XX:SurvivorRatio设置:新生代堆大小比例(例如,如果Young Gen大小为10m且内存开关为-XX:SurvivorRatio=2,则会为Eden空间保留5m,每个Survivor空间保留2.5m,默认值为8)

-XX:NewRatio:提供老年代和新生代大小的比例(默认值为2)

但是,您是否曾经想过JVM是如何存在于内存中的?让我来展示给您。就像其他任何软件一样,JVM占用主机操作系统内存中的可用空间。

然而,在JVM内部,存在着独立的内存空间(堆、非堆、缓存),用于存储运行时数据和编译代码。

1)堆内存

堆内存分为两个部分:新生代和老年代

堆内存在JVM启动时分配(初始大小:-Xms

应用程序运行时,堆内存的大小会增加或减小

最大大小:-Xmx

1.1) 新生代

这部分用于存放新分配的对象

新生代包括三个部分:伊甸园空间和两个幸存者空间(S0、S1)

大部分新创建的对象都存放在伊甸园空间中

当伊甸园空间填满对象时,会进行次要GC(也称为新生代回收),将所有幸存者对象移动到其中一个幸存者空间

次要GC还会检查幸存者对象,并将它们移动到另一个幸存者空间。因此,始终有一个幸存者空间是空的

经过多次GC循环后幸存下来的对象会被移动到老年代内存空间。通常,在年轻代对象达到一定年龄阈值之前,它们不会被提升到老年代。

1.2) 老年代

这部分用于存放经过多次次要GC后仍然存活的长生命周期对象

当老年代空间满时,会进行主要GC(也称为老年代回收),通常需要较长时间

2) 非堆内存

这包括永久代(在Java 8中被元空间(Metaspace)取代)

永久代存储每个类的结构,例如运行时常量池、字段和方法数据以及方法和构造函数的代码,以及字符串池中的字符串

可以使用-XX:PermSize-XX:MaxPermSize来更改其大小

3) 缓存内存

这包括代码缓存

存储由JIT编译器生成的编译代码(即本机代码)、JVM内部结构、加载的分析器代理代码和数据等。

当代码缓存超过阈值时,它会被清空(对象不会被GC重新定位)。

栈与堆

到目前为止,我还没有提及Java堆栈内存,因为我想单独强调它的区别。首先,看一下下面的图像,看看你是否知道这里发生了什么。我在之前的文章中已经讨论过JVM堆栈。

总之,简而言之,Java堆栈内存用于线程的执行,它包含方法特定的值和对堆中其他对象的引用。让我们将堆栈和堆放入一个表格中,看看它们的区别。

下面是一个很好的例子(来自baeldung.com),展示了堆栈和堆如何协同执行一个简单的程序(通过代码检查堆栈顺序)。

class Person {
int pid;
String name;// constructor, setters/getters
}
public class Driver {
public static void main(String[] args) {
int id = 23;
String pName = "Jon";
Person p = null;
p = new Person(id, pName);
}
}

修改内容:

上述的Java内存模型是最常讨论的实现方式。然而,最新的JVM版本进行了一些修改,引入了以下新的内存空间。

 

Keep区域:在Young Generation中新增的内存空间,用于存储最近分配的对象。在下一个Young Generation之前不会进行垃圾回收。这个区域可以防止对象因为在Young Collection开始前被分配而被提升。

Metaspace:自Java 8开始,Metaspace取代了Permanent Generation。它可以自动增加其大小(最高达到底层操作系统提供的限制),而Perm Gen总是有一个固定的最大大小。只要类加载器还存活,元数据就会保持在Metaspace中,并且无法释放。

注意:建议您阅读供应商文档,以了解适用于您的JVM版本的情况。

 

内存相关问题

当出现关键的内存问题时,JVM会崩溃并在程序输出中抛出以下错误提示。

 

java.lang.StackOverflowError — 表示堆栈内存已满

java.lang.OutOfMemoryError: Java heap space — 表示堆内存已满

java.lang.OutOfMemoryError: GC Overhead limit exceeded — 表示GC已达到其开销限制

java.lang.OutOfMemoryError: Permgen space — 表示Permanent Generation空间已满

java.lang.OutOfMemoryError: Metaspace — 表示Metaspace已满(自Java 8开始)

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– 表示我们的应用程序使用的数组大小超过了底层平台允许的大小

然而,您必须彻底理解的是,这些输出只能表示JVM的影响,而不能表示实际的错误。实际的错误及其根本原因可能出现在您的代码中的某个地方(例如内存泄漏、GC问题、同步问题)、资源分配,甚至可能是硬件设置。因此,我不能建议您简单地增加受影响资源的大小来解决问题。也许您需要监控资源使用情况,对每个类别进行分析,检查并调试/优化您的代码等。如果所有的努力都没有效果,或者您的上下文知识表明您需要更多的资源,那么请增加资源。

正文到此结束
该篇文章的评论功能已被站长关闭
本文目录