为什么学习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)。