Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8 和之前的版本略有不同。
下图是 JDK 1.8 对JVM做的改动,把方法区的具体实现----元空间已到了本地内存中。

各线程共享的:堆、方法区(元空间)、直接内存;
各线程私有的:程序计数器、虚拟机栈、本地方法栈;
:one: 程序计数器
它是个什么?
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。 字节码解释器 工作时 通过改变这个计数器的值 来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
它有什么用?
1、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制;
2、在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了;
补充:
有什么需要注意的点?
- OutOfMemoryError
:two: 虚拟机栈
它是个什么?
每个线程运行时所需要的内存,称为“虚拟机栈”。所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。
每一个线程运行时需要给每一个线程划分一块内存空间,那虚拟机栈就是每一个线程运行时所需要的内存空间。一个线程就有一个虚拟机栈,多个线程就有多个栈。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
每个栈由一个个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。每个栈帧内部都拥有:局部变量表、操作数栈、动态链接、方法返回地址。栈顶部的,即正在被执行的方法,叫活动栈帧。
它有什么用?
栈用来执行所有方法,并返回结果。
什么情况下会栈内存溢出?
- StackOverFlowError
- OutOfMemoryError
有什么需要注意的点?
有没有试过线程诊断?(Linux环境下)
- # 可以实时监测后台进程对CPU和内存的占用情况,top命令可以得到进程编号,但不能定位这个进程中哪个线程有问题
- top
-
- # ps命令进一步可以查看该进程下,所有线程对CPU的占用情况,看是哪一个线程占用过高(注意:输出的线程编号是十进制的)
- ps H -eo pid,tid,%cpu | grep 进程id
-
- # jstack命令可以得到该进程下的所有线程运行信息(注意:输出的线程中有个参数是nid,值就是线程编号,16进制的,把上面10进制换算成16进制)
- jstack 进程id # 而且,可以定位到Java哪个类中第几行代码有问题
上面的三步法,对于常见的死循环、死锁等问题,都可以排查出来。
其他面试问题?
Q:垃圾回收是否涉及栈内存?
Q:栈内存分配越大越好吗?
-Xss size 可以指定栈内存的大小,如 -Xss256k (中间没有空格),把栈内存设为256k;Q:方法内的局部变量是否线程安全?
:three: 本地方法栈
它是个什么?
和虚拟机栈的作用非常相似, 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务 。
JVM在调用本地方法时,需要给这些本地方法提供的内存空间。本地方法不是由java代码编写的,因为java有时不能直接跟OS底层打交道,所以就需要用C或C++语言编写的本地方法来与OS底层的API打交道。java代码可以通过本地方法接口调用OS底层的功能。那这些本地方法运行时使用到的内存就是本地方法栈。
它有什么用?
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
:four: 堆(:rocket::rocket::rocket:)
它是个什么?
简称 Heap,JVM中内存最大的一块;堆是所有线程共享的一块内存区域,在虚拟机启动时创建;
Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
它有什么用?
此内存区域的唯一目的就是存放对象实例, 几乎 所有的对象实例以及数组都在这里分配内存。
Q:为什么说几乎,对象不就是在堆中存放的吗?
JDK1.8和1.8之前的区别?
JDK 1.6 及以前,存在永久代;
在 JDK 1.7,有永久代,但已经逐步“去永久代”。堆内存通常分为三个部分:新生代(伊甸园区、幸存区)、老年代、永久代;
JDK 1.8 及以后,移除 PermGen(永久代), 被 Metaspace(元空间) 取代,元空间使用的是“直接内存”,受本机物理内存大小限制。

什么情况下堆会溢出?
堆这里最容易出现的就是 OutOfMemoryError 错误 ,有多种情况:
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded : 当 GC 花费大量时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误;java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误;补充:
- -Xmx
堆内存诊断工具知道哪些?
- jps // 查看当前系统中有哪些Java进程,并且显示它们的进程编号
-
- jmap -heap 进程id // 拿到Java进程编号后,通过jmap工具可以查看这个Java进程的堆内存使用情况(只能查看某一个时刻)
使用方式:直接在控制台上运行上面两条命令即可。
jconsole // 相比jmap能实时查看某个Java进程的堆内存使用情况
使用方式:控制台输入 jconsole 命令,打开一个图形化界面,选择本地要查看的进程,连接,选择不安全连接,此时就可以看到堆内存的使用了(可以查看实时的堆内存变化)。
jps和jmap是以命令行的方式操作的,而jconsole是以图形界面操作的。
测试代码:
- "color:#444444">"background-color:#f6f6f6">"color:#333333">public "color:#333333">static "color:#333333">void "color:#880000">main(String[] args) {
- System."color:#333333">out.println("color:#880000">"1...");
- Thread.sleep("color:#880000">20000);
- "color:#333333">byte[] arr = "color:#333333">new "color:#333333">byte["color:#880000">1024 * "color:#880000">1024 * "color:#880000">10]; "color:#888888">// 10Mb
- System."color:#333333">out.println("color:#880000">"2...");
- Thread.sleep("color:#880000">20000);
- arr = "color:#78a960">null;
- System.gc(); "color:#888888">// 手动gc,回收堆中的 byte 数组
- System."color:#333333">out.println("color:#880000">"3...");
- Thread.sleep("color:#880000">20000000);
- }
:five: 方法区
它是个什么?
方法区是各个线程共享的内存区域,它在虚拟机启动时被创建(跟堆一样);
它有什么用?
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据 。
方法区和永久代以及元空间是什么关系?
方法区是一种规范,是抽象的概念。而永久代和元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种具体实现方式。永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
Q: 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢 ?
- MaxPermSize
方法区常用参数有哪些?
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小:
- "color:#444444">"background-color:#f6f6f6">-"color:#880000">XX:PermSize=N "color:#888888">// 方法区 (永久代) 初始大小
- -"color:#880000">XX:MaxPermSize=N "color:#888888">// 方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存:
- "color:#444444">"background-color:#f6f6f6">-"color:#880000">XX:MetaspaceSize=N "color:#888888">//设置 Metaspace 的初始(和最小大小)
- -"color:#880000">XX:MaxMetaspaceSize=N "color:#888888">//设置 Metaspace 的最大大小
运行时常量池是什么?
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息;
常量池表会在类加载后存放到方法区的运行时常量池中。
运行时常量池是方法区的一部分,自然受到方法区内存的限制(1.8以后为直接内存),当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误;
字符串常量池是什么?
字符串常量池 是 JVM 为了 提升性能 和 减少内存消耗 针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
- // 在堆中创建字符串对象”ab“
- // 将字符串对象”ab“的引用保存在字符串常量池中
- String aa = "ab"; // aa是对象的引用,存在栈中
- // 直接返回字符串常量池中字符串对象”ab“的引用
- String bb = "ab";
- System.out.println(aa==bb);// true
注: StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。
JDK1.7 之前(1.6),字符串常量池存放在永久代(方法区)。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
Q:JDK 1.7 为什么要将字符串常量池从永久代移动到堆中 ?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
Q:对于StringTable,如何性能调优 ?
StringTable 本质上就是一个 HashSet ,容量为 StringTableSize (可以通过 -XX:StringTableSize 参数来设置)。
-XX:StringTableSize = 桶个数
:six: 直接内存
搞懂 直接内存是什么?有什么用?直接内存的分配和释放如何实现的?
是什么?
Direct Memory
有什么用?
直接内存常用于NIO操作,在NIO进行数据读写时用于数据缓冲区内存。例如NIO中有个类叫ByteBuffer,ByteBuffer所分配和使用的内存就是直接内存。
是否会内存溢出?
直接内存并不属于 Java 虚拟机的内存,而是属于操作系统内存。本地直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
- "color:#444444">"background-color:#f6f6f6">"color:#333333">import java.nio.ByteBuffer;
- "color:#333333">import java.util.*;
- "color:#888888">/**
- * 演示直接内存溢出
- */
- "color:#333333">public "color:#333333">class Demo1_10 {
- "color:#333333">static "color:#333333">int _100Mb = "color:#880000">1024 * "color:#880000">1024 * "color:#880000">100;
-
- "color:#333333">public "color:#333333">static "color:#333333">void "color:#880000">main(String[] args) {
- List
"color:#397300">list = "color:#333333">new ArrayList<>(); - "color:#333333">int i = "color:#880000">0;
- "color:#333333">try {
- "color:#333333">while ("color:#78a960">true) {
- ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
- "color:#397300">list.add(byteBuffer);
- i++;
- }
- } finally {
- System.out.println(i); "color:#888888">// 打印循环多少次,会报内存溢出(不同工作环境、不同配置的电脑输出不同)
- }
- "color:#888888">// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
- "color:#888888">// jdk8 对方法区的实现称为元空间
- }
- }
如何分配和释放?
直接内存的分配和释放是通过 Unsafe 类来管理的,直接内存不会自动释放,必须手动调用 freeMemory() 方法释放内存。而垃圾回收只能释放 JVM中的堆内存。看下面的例子:
- "color:#444444">"background-color:#f6f6f6">"color:#333333">public "color:#333333">classstrong>span> <span style="color:#880000"><strong>Demo1_27strong>span> {
-
- "color:#333333">static "color:#333333">int _1Gb = "color:#880000">1024 * "color:#880000">1024 * "color:#880000">1024;
-
- "color:#333333">public "color:#333333">static "color:#333333">void "color:#880000">main(String[] args) throws IOException {
- "color:#888888">// 通过反射拿到 Unsafe 对象实例
- Unsafe "color:#333333">unsafe = getUnsafe();
- "color:#888888">// 分配内存1个G,可以打开任务管理器查看进程情况
- "color:#333333">long "color:#333333">base = "color:#333333">unsafe.allocateMemory(_1Gb);
- "color:#333333">unsafe.setMemory("color:#333333">base, _1Gb, ("color:#333333">byte) "color:#880000">0);
- System."color:#333333">in.read(); "color:#888888">// 程序运行阻塞,控制台回车才会继续向下执行
-
- "color:#888888">// 释放内存
- "color:#333333">unsafe.freeMemory("color:#333333">base);
- System."color:#333333">in.read();
- }
-
- "color:#333333">public "color:#333333">static Unsafe "color:#880000">getUnsafe() {
- "color:#333333">try {
- "color:#888888">// theUnsafe 是 Unsafe 类中一个私有的静态成员变量,类型是 Unsafe
- Field f = Unsafe.class.getDeclaredField("color:#880000">"theUnsafe");
- f.setAccessible("color:#78a960">true);
- Unsafe "color:#333333">unsafe = (Unsafe) f."color:#333333">get("color:#78a960">null);
- "color:#333333">return "color:#333333">unsafe;
- } "color:#333333">catch (NoSuchFieldException | IllegalAccessException e) {
- "color:#333333">throw "color:#333333">new RuntimeException(e);
- }
- }
- }
查看下 ByteBuffer 类的 allocateDirect 方法的源码:
- "color:#444444">"background-color:#f6f6f6">"color:#888888">/* ByteBuffer.java */
- "color:#333333">public "color:#333333">static ByteBuffer "color:#880000">allocateDirect("color:#333333">int capacity) {
- "color:#333333">return "color:#333333">new DirectByteBuffer(capacity);
- }
-
- "color:#888888">/* DirectByteBuffer.java */
- DirectByteBuffer("color:#333333">int cap) {
- ......
- "color:#333333">try {
- "color:#333333">base = "color:#333333">unsafe.allocateMemory(size); "color:#888888">// 调用了Unsafe类的方法分配内存
- } "color:#333333">catch (OutOfMemoryError x) {
- Bits.unreserveMemory(size, cap);
- "color:#333333">throw x;
- }
- "color:#333333">unsafe.setMemory("color:#333333">base, size, ("color:#333333">byte) "color:#880000">0); "color:#888888">// 调用了Unsafe类的方法分配内存
- ......
- cleaner = Cleaner.create("color:#333333">this, "color:#333333">new Deallocator("color:#333333">base, size, cap)); "color:#888888">// 释放内存时会执行这里
- att = "color:#78a960">null;
- }
-
- "color:#888888">/* Cleaner.class */
- "color:#333333">public "color:#333333">void "color:#880000">clean() {
- "color:#333333">if ("color:#333333">remove("color:#333333">this)) {
- "color:#333333">try {
- "color:#333333">this.thunk.run();
- .....
-
- "color:#888888">/* DirectByteBuffer.java */
- "color:#333333">private "color:#333333">static "color:#333333">classstrong>span> <span style="color:#880000"><strong>Deallocatorstrong>span> <span style="color:#880000"><strong>implementsstrong>span> <span style="color:#880000"><strong>Runnablestrong>span> {
- "color:#333333">public "color:#333333">void "color:#880000">run() {
- "color:#333333">if (address == "color:#880000">0) {
- "color:#888888">// Paranoia
- "color:#333333">return;
- }
- "color:#333333">unsafe.freeMemory(address); "color:#888888">// 这里调用了 freeMemory 释放直接内存
- address = "color:#880000">0;
- Bits.unreserveMemory(size, capacity);
- }
- }
ByteBuffer 实现类内部,使用了 Cleaner 虚引用来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程(守护线程)执行 Cleaner 类的 clean 方法,内部调用 freeMemory() 来释放直接内存;
ByteBuffer类:它是抽象类
public abstract class ByteBuffer extends Buffer implements Comparable
Cleaner类:它是虚引用类型(继承 PhantomReference )
public class Cleaner extends PhantomReference