咱们一般所说的 Java 虚拟机(JVM)的内存布局,一般是指 Java 虚拟机的运转时数据区(Runtime Data Area),也就是当字节码被类加载器加载之后的履行区域划分。当然它一般是 JVM 模块的第一个面试问题,所以,接下来咱们一同来看它里边包含了哪些内容。

官方界说

《Java虚拟机标准》中将 JVM 运转时数据区域划分为以下 5 部分:

  1. 程序计数器(Program Counter Register)
  2. Java虚拟机栈(Java Virtual Machine Stacks)
  3. 本地办法栈(Native Method Stack)
  4. Java 堆(Java Heap)
  5. 办法区(Methed Area)

如下图所示:

面试必问:说一下 Java 虚拟机的内存布局?
接下来,咱们分别来看每个模块的效果及具体介绍。

1.程序计数器

《Java虚拟机标准》中对程序计数器的界说如下:

The Java Virtual Machine can support many threads of execution at once (JLS 17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine’s pc register is undefined. The Java Virtual Machine’s pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.

以上内容翻译成中文,简略来说它的意义是:JVM 中能够有多个履行线程,每个线程都有自己的程序计数器,在程序计数器中包含的是正在履行线程的指令地址。

也就是说,程序计数器(Program Counter Register)是线程独有一块很小的内存区域,保存当时线程所履行字节码的方位,包含正在履行的指令、跳转、分支、循环、反常处理等。

1.1 效果

咱们知道,CPU 核数是比较少的,而使命(线程)是比较多的,所以实在的状况是,CPU 会不断的切换线程以履行一切的程序,当然由于(CPU)切换的速度比较快,所以咱们是感知不到的,咱们感觉好像一切的程序都是一直在履行,其实从微观的层面来看,一切的程序都是切换履行的。

那么问题来了,CPU 一直在切换线程履行使命,那 CPU 再次切换到某个线程时,它是怎样知道当时的线程上次知道到哪了?

这就是程序计数器的效果,程序计数器里边保存了当时线程履行的行号,这样当 CPU 切换到当时线程时,才干接着上次履行的方位,继续履行

PS:程序计数器中实在记载的是下一行使命的履行指令。程序计数器也是 JVM 运转时数据区中履行最快的一块区域。

1.2 线程同享

程序计数器记载的是每个线程的履行行号,所以每个线程都具有自己的程序计数器,所以此区域不是线程同享的,而是线程私有的

1.3 GC

GC 是 Garbage Collection 的缩写,译为废物搜集。 此区域不存在 GC。

1.4 OOM

OOM 是 Out of Memory 的缩写,译为内存溢出。 此区域不存在 OOM 的问题。

2.Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stack)也叫做 JVM 栈,《Java虚拟机标准》对此区域的阐明如下:

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous. In the First Edition of The Java Virtual Machine Specification, the Java Virtual Machine stack was known as the Java stack. This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created. A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of Java Virtual Machine stacks, as well as, in the case of dynamically expanding or contracting Java Virtual Machine stacks, control over the maximum and minimum sizes. The following exceptional conditions are associated with Java Virtual Machine stacks:

  • If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
  • If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

以上内容翻译成中文的意义如下:

Java 虚拟机栈是线程私有的区域,它跟着线程的创立而创立。它里边保存的是局部变量表(根底数据类型和目标引用地址)和核算过程中的中间成果。Java 虚拟机的内存不需要接连,它只要两个操作:入栈和出栈。

Java 虚拟机栈要么巨细固定,要么依据核算动态的扩展和缩短。程序员能够对 Java 虚拟机栈进行初始值的巨细设置和最大值的设置。

Java 虚拟机栈呈现的反常有两种:

  • 当 Java 虚拟机栈巨细固守时,假如程序中的栈分配超过了最大虚拟机栈就会呈现 StackOverflowError 反常。

  • 假如 Java 虚拟机栈是动态扩展的,那么当内存不足时,就会引发 OutOfMemoryError 的反常。

    2.1 效果

    Java 虚拟机栈主要是管 Java 程序运转的,它保存的是办法的局部变量、办法履行中的部分成果,并参加办法的调用和返回。

    简略来说,栈是运转时单位,而堆是存储单位。也就是说:栈处理的是程序运转的问题,即程序怎样履行?或许说怎样处理数据。堆处理的是数据存储的问题,即数据怎样放?放在哪儿。

2.2 线程同享

Java 虚拟机栈是线程私有的,它的生命周期和线程的生命周期一致。

2.3 GC

Java 虚拟机栈由于只要入栈和出栈两个操作,所以它是不触及废物收回的。

2.4 OOM

此区域虽然没有 GC,但存在两种反常:

  • 当 Java 虚拟机栈巨细固守时,假如程序中的栈分配超过了最大虚拟机栈就会呈现 StackOverflowError 反常。

  • 假如 Java 虚拟机栈是动态扩展的,那么当内存不足时,就会引发 OutOfMemoryError 的反常。

也就是,Java 虚拟机栈是或许存在 OOM 的

2.5 常见参数设置

设置 Java 虚拟机栈巨细:-Xss。 如设置:-Xss128k,表明设置每个线程的栈巨细为 128k,此设置等价于 -XX:ThreadStackSize=128k。

2.6 常见问题演示

最简略的错误示例就是死循环,办法自己调自己,这样 Java 虚拟机栈就会只入栈不出栈,当抵达 Java 虚拟机栈的最大数之后就会呈现 StackOverflowError 反常,如下图所示:

面试必问:说一下 Java 虚拟机的内存布局?

3.本地办法栈

本地办法栈(Native Method Stacks),《Java 虚拟机标准》对此区域的阐明如下:

An implementation of the Java Virtual Machine may use conventional stacks, colloquially called “C stacks,” to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine’s instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created. This specification permits native method stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the native method stacks are of a fixed size, the size of each native method stack may be chosen independently when that stack is created. A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the native method stacks, as well as, in the case of varying-size native method stacks, control over the maximum and minimum method stack sizes. The following exceptional conditions are associated with native method stacks:

  • If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
  • If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

以上内容,挑要点简略翻译一下:本地办法栈俗称“C栈”,它是 Native(本地)办法(用 Java 编程言语以外的言语编写的办法),此区域和 Java 虚拟机栈相似,这不过比方 C 言语等运用的栈空间。它也是存在两种反常:StackOverflowError 和 OutOfMemoryError

PS:由于此区域是非 Java 言语完成和运用的,所以本文就不做过多的赘述,总之,记得一句话:此区域和 Java 虚拟机栈相似,不过是给 C/C++ 言语运用的

4.堆

堆(Heap)《Java 虚拟机标准》对此区域的阐明如下:

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated. The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor’s system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous. A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size. The following exceptional condition is associated with the heap:

  • If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError.

以上内容,挑要点简略翻译一下:

堆是线程同享的,程序中一切类实例和数组的内存都存储在此区域,它在 Java 虚拟机启动时就会创立。目标不会被显式开释,只会在废物搜集时开释。堆的巨细能够是固定的,也能够动态扩展或缩短。堆的内存在物理层面不需要是接连的。

程序员能够对堆进行初始巨细操控,或许设置最大、最小堆的容量。 堆或许会呈现 OutOfMemoryError 反常。

4.1 效果

堆是 Java 虚拟机的主要存储单位,Java 中一切的目标和数组都是保存在此区域的。

4.2 线程同享

堆是线程同享的,堆上的目标或许被多个线程一起拜访。

4.3 GC

堆是 JVM 最大的一块区域,也是废物收回器进行废物收回最频频的一块区域。

4.4 OOM

当堆空间不足时,会发生 OutOfMemoryError 反常。

4.5 常见参数设置

  • -Xms:设置初始 Java 堆巨细,比方:-Xms10m,表明设置堆的初始巨细为 10MB。

  • -Xmx:设置最大 Java 堆巨细,比方:-Xmx10m,表明设置堆的最大空间为 10MB。

    4.6 常见问题演示

    接下来,咱们来演示一下堆空间 OOM 的问题,咱们先运用“-Xmx50m”的参数来设置一下 Idea,它表明将程序运转的最大内存设置为 50m,假如程序的运转超过这个值就会呈现内存溢出的问题,设置办法如下:

    面试必问:说一下 Java 虚拟机的内存布局?
    设置后的终究效果这样的:
    面试必问:说一下 Java 虚拟机的内存布局?

    PS:由于我运用的 Idea 是社区版,所以或许和你的界面不一样,你只需要点击“Edit Configurations…”找到“VM options”选项,设置上“-Xmx50m”参数就能够了。

装备完 Idea 之后,接下来咱们来完成一下事务代码。在代码中咱们会创立一个大目标,这个目标中会有一个 10m 大的数组,然后咱们将这个大目标存储在 ThreadLocal 中,再运用线程池履行大于 5 次增加使命,由于设置了最大运转内存是 50m,所以抱负的状况是履行 5 次增加操作之后,就会呈现内存溢出的问题,完成代码如下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalOOMExample {
    /**
     * 界说一个 10m 大的类
     */
    static class MyTask {
        // 创立一个 10m 的数组(单位转化是 1M -> 1024KB -> 1024*1024B)
        private byte[] bytes = new byte[10 * 1024 * 1024];
    }
    // 界说 ThreadLocal
    private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
    // 主测验代码
    public static void main(String[] args) throws InterruptedException {
        // 创立线程池
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(5, 5, 60,
                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
        // 履行 10 次调用
        for (int i = 0; i < 10; i++) {
            // 履行使命
            executeTask(threadPoolExecutor);
            Thread.sleep(1000);
        }
    }
    /**
     * 线程池履行使命
     * @param threadPoolExecutor 线程池
     */
    private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
        // 履行使命
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("创立目标");
                // 创立目标(10M)
                MyTask myTask = new MyTask();
                // 存储 ThreadLocal
                taskThreadLocal.set(myTask);
                // 将目标设置为 null,表明此目标不在运用了
                myTask = null;
            }
        });
    }
}

以上程序的履行成果如下:

面试必问:说一下 Java 虚拟机的内存布局?
从上述图片可看出,当程序履行到第 5 次增加目标时就呈现内存溢出的问题了,这是由于设置了最大的运转内存是 50m,每次循环会占用 10m 的内存,加上程序启动会占用必定的内存,因此在履行到第 5 次增加使命时,就会呈现内存溢出的问题。

5.办法区

办法区(Method Area)《Java 虚拟机标准》对此区域的阐明如下:

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (2.9) used in class and instance initialization and interface initialization. The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous. A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size. The following exceptional condition is associated with the method area:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

以上内容,挑要点简略翻译一下: 办法区是线程同享的,办法区相似于传统言语的编译代码的存储区,或许相似于操作系统进程中的“文本”段。它存储每个类的结构,如运转时常量池、字段和办法数据,以及办法和构造函数的代码。

办法区域是在 Java 虚拟机启动时创立的,虽然办法区域在逻辑上是堆的一部分,但简略的完成或许选择不进行废物搜集或压缩。办法区域能够是固定的巨细,也能够动态扩展。办法区的(物理)内存不需要接连。

Java 虚拟机完成能够为程序员或用户供给对办法区域初始巨细的操控,以及在可变巨细的办法区域的状况下,对最大和最小办法区域巨细的操控。

假如办法区中的内存无法满足分配恳求,Java 虚拟机将抛出一个 OutOfMemoryError。

5.1 效果

用于存储每个类的结构,包含运转时常量池、静态变量、字段和办法数据

5.2 HotSpot 办法区完成

HotSpot 虚拟机是 Sun JDK 和 Open JDK 中自带的虚拟机,也是现在运用范围最广的 Java 虚拟机。作为官方 Java 虚拟机的化身,现在所讲的一切常识,几乎都是针对此虚拟机的,所以咱们要看 HotSpot 虚拟机对《Java 虚拟机标准》中办法区的完成。

对于 HotSpot 虚拟机来说,不同的 JDK 办法区的完成是不同的,在 JDK 1.7 之前,HotSpot 技术团队运用的是永久代来完成办法区的,但这种完成有一个致命的问题,这样规划更容易形成内存溢出。由于永久代有 -XX:MaxPermSize(办法区分配的最大内存)的上限,即便不设置也会有默许的巨细。例如,32 位操作系统中的 4GB 内存约束等,并且这样规划导致了部分的办法在不同类型的 Java 虚拟机下的表现也不同,比方 String::intern() 办法。所以在 JDK 1.7 时 HotSpot 虚拟机已经把原本放在永久代的字符串常量池和静态变量等移出了办法区,并且在 JDK 1.8 中彻底废弃了永久代的概念。

JDK 1.8 之后,HotSpot 虚拟机开始运用元空间(Meta Space)来完成办法区了。

5.3 线程同享

办法区是线程同享的。

5.4 GC

《Java 虚拟机标准》中规则办法区能够没有 GC(废物收回),但 HotSpot 中对此区域完成了 GC 操作。

5.5 OOM

办法区是存在 OOM 状况的,比方在 JDK 1.8 中,假如元空间设置空间过小,而类信息发生的过多就会发生 OOM,如下示例所示:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
 * 办法区 OOM 演示(JDK 1.8+)
 * 设置 -XX:MaxMetaspaceSize=10m 元空间最大内存为 10MB。
 */
public class MethodAreaOOMExample {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MethodAreaOOMExample.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object object, Method method,
                                        Object[] args,
                                        MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(object, args);
                }
            });
            enhancer.create();
        }
    }
}

以上程序的履行成果如下图所示:

面试必问:说一下 Java 虚拟机的内存布局?
以上代码是通过 CGLIB 不断的发生动态代理类将办法区填满,从而就导致 OOM 的问题。

PS:在运用 CGLIB 之前,需要现在当时项目中导入 CGLIB 才干够正常运用。

5.6 常用参数设置

永久代(HotSpot 虚拟机,JDK 1.7 之前设置有用):

  • -XX:PermSize=100m:设置永久代初始值为 100MB。
  • -XX:MaxPermSize=100m:设置永久代最大值为 100MB。

元空间(HotSpot 虚拟机,JDK 1.8 之后设置有用):

  • -XX:MetaspaceSize=100m:设置元空间初始巨细为 100MB。
  • -XX:MaxMetaspaceSize=100m:设置元空间最大容量为 100MB,默许不设置,则没有约束。
  • -XX:CompressedClassSpaceSize=100m:设置 Class Metaspace 的巨细为 100MB,默许值为 1G。

直接内存(HotSpot 虚拟机,JDK 1.8 之后设置有用): -XX:MaxDirectMemorySize=100m:指定直接内存的最大容量为 100MB。

总结

《Java虚拟机标准》中将 JVM 运转时数据区域划分为以下 5 部分:

  1. 程序计数器(Program Counter Register)
  2. Java 虚拟机栈(Java Virtual Machine Stacks)
  3. 本地办法栈(Native Method Stack)
  4. Java 堆(Java Heap)
  5. 办法区(Methed Area)

其中线程私有的区域是:程序计数器、Java 虚拟机栈、本地办法栈;而线程同享的是:Java 堆和办法区。

而除了程序计数器,其他区域都是能够会呈现 OOM 的。

参阅 & 道谢

docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1 zhuanlan.zhihu.com/p/518151056

本文已收录到 Gitee 开源仓库《Java 面试指南》,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、规划形式、音讯行列等模块。Java 面试有它就够了:超全 Java 常见面试题,继续更新…