Java的内存管理

为什么学习Java的内存管理

正如你已经知道了Java自己管理内存,并且不需要程序员特意的介入。GC扮演着一个清理在工作内存中应用程序不再使用对象的角色。然而,为什么程序员还需要学习Java中有关于自动的内存管理的知识呢?尽管它叫做自动,GC不确保释放的内存或对象是依然处于引用状态的。程序员不知不觉的让对象被引用,甚至在这些对象被使用完成的情况下,导致内存泄漏或是不能被JVM所控制的严重后果。程序员懂得内存管理写程序时会尽量减少内存泄漏。

作为一个程序员,你应该知道什么样的对象符合自动垃圾回收机制,并且哪些不符合。因此学习内存管理对于写出高效并且高性能的程序是至关重要的。

概述

一旦我们启动JVM,操作系统就会为进程分配内存。在这里,JVM本身就是一个进程,分配给JVM进程的内存有堆、元空间等。我们称其为本地内存。本地内存是通过操作系统提供给进程的。然而,操作系统分配给Java进程多少内存取决于操作系统、处理器和JRE。

JVM为运行期间的程序定义了各种运行时数据区域。一些内存区是JVM创建的,而另外一些则是由程序中使用的线程创建的。然而,这些内存区通过JVM创建,并且随着JVM的退出而被销毁。线程运行期间由线程创建的内存会随着线程的结束而被销毁。

方法区(Method Area)

Java虚拟机有一个方法区,在所有Java虚拟机线程之间是共享的。这个方法区与常规语言的编译代码的存储区很相似,和操作系统进程中的文本段也很相似。它存储每一个类的结构,如运行时的常量池、字段和方法数据、方法和构造函数的代码、包括类中、实例中以及接口实现中用到的特殊方法(见JDK)。

方法区相当于接口,JDK7中的实现被称为永久代,JDK8中的实现被称为元空间

方法区在逻辑上是堆的一部分,但实际上,你是可以区分开堆区与方法区的,方法区也被称为非堆(non-heap),它实际上是堆,但是jvm中设置的堆的大小中却不包含方法区。

方法区类似于堆,都属于启动时jvm创建的多线程共享方法。同样的,方法区的大小可以在物理上是不连续的也很像堆,可以设置固定大小或动态扩展。方法区的大小决定了系统可以创建多少个类,如果类的数量过多,方法区会报OOM:jdk7中会报永久代内存不足,jdk8会报元空间内存不足。

关闭jvm将销毁方法区,释放方法区内存。

其详细说明见《Java内存—方法区

堆区(Heap)

堆区是一个共享的运行时数据区,可以分配给所有类实现与数组的真实对象。依赖于系统的配置,堆的尺寸可以设置为动态或者是固定的。它在Java虚拟机的启动期间被初始化。分配给对象的堆内存可以通过GC回收。

从Java11及以上,最大的堆内存的大小是物理内存的25%,但最大不超过25G。然而,如果物理内存低于2G,那么堆内存的取值范围就是物理内存的50%。

对于一个运行中的Java虚拟机进程始终只有一个堆。当堆区已经满了,垃圾回收器会适当的清理对象。

配置堆的大小的JVM参数配置如下:

  • -Xmx:设置java堆最大值,如-Xmx2g
  • -Xms:设置java堆最小值,如-Xms2g

详细说明见《Java内存—堆内存管理

JVM栈区(Stack)

栈是随着程序创建的每一个线程生成的。它是通过线程关联的。每一个线程都有自己的栈。所有局部变量及函数调用都存放在栈中。它的生成周期依赖于线程的生命周期,线程活着它就活着,反之亦然。

栈区是随着创建线程同时创建的。栈区常用于存储特定于方法的值,它们存活的时间短并且可以引用堆区的其他对象。栈的内存不需要是连续的。它的大小也是可以固定的或者动态的。

一个特定的线程栈也可以称之为:运行时栈。该线程执行的每一个方法调用都存储在对应的运行时栈中,包括参数、局部变量、中间计算以及其他数据。一个方法完成以后,将从栈中删除对应的条目。当所有的方法调用都完成以后,栈会变空的,并且在线程结束之前,这个空栈会被JVM立即删除。存储在线程中的数据对于适当的线程是可用的,而对于其他的线程则不可用。因些我们可以说:本地数据线程安全。

栈中的每一个条目我们都称之为:栈帧或活动记录。

栈帧的结构

  • 每一帧都包含它自己的局部变量数组(Local Variable Array——LVA)、帧数据(Frame Data——FD)以及操作数栈(Operand Stack——OS)。
  • LVA、FD和OS的大小都是在编译期内确定的
  • 在一个线程控制中,每次只有一帧是活动的,称为当前帧。

其详细说明见《Java内存—栈区

本地方法栈

本地方法是使用除Java语言以的其他语言编写的方法。它可以是C、C++或其他编程语言。本地方法栈通常是在每一个线程创建的时候分配给每一个线程的。本地方法栈也被称为C栈。Java本地接口(Java Native Interface—JNI)调用本地方法栈。

本地方法栈可以设置为固定值,也可以根据计算需要动态的扩展或收缩。如果我们创建一个新的线程,但是却没有足够的内存分配给新线程的本地方法栈,那么程序就会抛出内存不足的错误。当然,如果本地方法栈设置为动态扩展,而程序需要扩大方法栈的时候没有提供足够的内存,也会抛出内存不足的错误。

简单的说,它与java栈相同,只是它用于本地方法。本地方法栈的性能取决于操作系统。

在JVM中设置本地方法栈大小的参数为-Xoss1m,格式如下:

java.exe -jar -Xoss1m xxx.jar

注意:

上面的参数只在JDK9以下(包括JDK7及JDK8)生效,从JDK9开始警告此参数已经被忽略。从JDK10开始此参数已经在JVM中删除,使用该参数启动JVM会报错。

在JDK9+版本以后,本地方法栈与Java线程栈不进行区分,也就是说使用-Xss1m设置的栈空间中已经包含了本地方法栈。

程序计数器(Program Counter )寄存器

每一个JVM线程被创建的时候都会为其分配程序计数器寄存器(下面我们简称PC寄存器)。本地方法中是没有定义程序计数器的值,而非本地方法都有程序计数器来存储当前正大执行的JVM指令的地址。它也存储本地指针或返回地址。

更新一下JVM内存的分配

我们知道,本地方法栈、栈、PC寄存器都是随着线程的创建而分配的。所以把JVM的内存的认知更新一下。

堆内存

Java虚拟机有一个堆内存空间,它是在所有Java虚拟机的线程中共享的。堆是运行时数据区,这些数据来自所有类的实现与数组分配的内存。

堆是在虚拟机启动时创建的。堆存储的对象是通过自动存储管理系统(垃圾回收器——GC)回收的。对象不再需要显示的回收。如果JVM没有指定特定的GC机制,用户可以根据自己的需要自行选择。堆的大小可以设置为固定值,或者通过计算根据需求来扩展或缩小。堆的内存区域可以是不连续的。

程度员或使用者可以通过虚拟机提供的方法控制堆的初始值,同样也可以动态的扩展或缩小,控制堆的最大小值与最小值。

如果堆区的内存不能满足分配请求,Java虚拟机会抛出内存不足的错误(OutOfMemoryError)。

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注