内存结构
- Heap (堆):所有的Java对象、数组都存储在JVM的堆内存中,在堆上创建的对象由垃圾回收器自动管理其生命周期。
- Method Area (方法区):存储类的元数据信息,类方法以及运行时常量池(String)、静态变量(static)和常量(final)。
- JVM Stacks (虚拟机栈):虚拟机栈用于存储每个线程的方法调用和局部变量,栈帧包含方法的局部变量表、返回地址等信息。
- Native Method Stacks (本地方法栈):本地方法栈类似于虚拟机栈,但是它是为执行native方法而准备的,与操作系统有关
- PC Register (程序计数器):保存的是jvm指令的地址
PC Register 程序计数器
本质是什么?其实就是电脑里面的寄存器,每个进入jvm的线程都会私有一份 PC Register
注意 :PC Register不存在栈溢出(OOM)的情况
有什么作用呢?
java源代码 会被 jvm 转换成 jvm指令(二进制字节码) 然后jvm会转发给解释器 由 解释器转换成机器码发给CPU
其中 PC Register 程序计数器 就是用来保存正在执行的jvm指令的下一条指令的地址
JVM Stacks 虚拟机栈
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法,也就是每个进入jvm的线程都会被分配一个虚拟机栈
问题辨析
-
垃圾回收是否涉及栈内存? 不涉及GC,因为虚拟机栈会出栈的。
-
栈内存分配越大越好吗? 当你栈内存分配的越大,就意味着jvm能提供的线程数越少,当栈内存总和达到某个值就无法再创建线程
-
方法内的局部变量是否线程安全?
-
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
-
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
线程安全代码示例
除了main方法的StringBuilder为线程安全外,其他的都不是线程安全的。
main方法的StringBuilder对象为局部对象,对于局部对象,它的生命周期与所在的方法或代码块的执行时间相对应。当方法或代码块执行完毕后,局部对象会被认为是不再被引用,因此可以被垃圾回收器回收。
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
栈内存溢出
-
栈帧过多导致栈内存溢出
-
栈帧过大导致栈内存溢出
Heap 堆
参考 原文链接:https://blog.csdn.net/qq_43911324/article/details/122736411
通过 new 关键字,创建对象都会使用堆内存
特点
-
它是线程共享的,堆中对象都需要考虑线程安全的问题
-
有垃圾回收机制
Java堆区在JVM启动时被创建,其空间大小同时也会被确定。堆空间是JVM管理的最大一块内存空间,同时堆内存空间也是可配置的。
Java虚拟机规范中规定,对可以处于物理上连续不断的内存空间中,但是逻辑上它应该被视为连续的。所有的对象实例以及数组都应当在运行时分配在堆空间上。The heap is the run-time data area from which memory for all class instances and arrays is allocated。数组和对象可能永远都不会存储在栈上,在栈帧中保存的是地址引用,这个地址引用指向对象或者数组在堆空间中的位置。在一个方法执行结束后,堆中的对象不会马上被移除,只有在进行垃圾回收的时候才会被移除。堆是GC(Garbage Collection)执行垃圾回收的重点区域。
JDK7堆空间内部结构
Java 7及之前堆的内存逻辑上分为三个部分:
- 新生区(Young Generation Space):其中新生区又被细分为Eden区和Survivor区。
- 养老区(Tenure generation space):老年代。
- 永久区(Permanent Space)
JDK8堆空间内部结构
Java 8及以后堆的内存逻辑上分为三个部分:
- 新生区(Young Generation Space):其中新生区又被细分为Eden区和Survivor区。
- 养老区(Tenure generation space):老年代。
- 元空间(Meta Space)
元空间和永久代的区别
参考 原文链接:https://blog.csdn.net/liuruiaaa/article/details/129039021
永久代(Permanent Generation)和元空间(Metaspace)都是 Java 虚拟机中用于存储类信息的内存区域,但它们有一些重要的区别:
- 存储位置:永久代是在 Java 堆中的一个特殊区域,而元空间是在本地内存中的。
- 大小调整:永久代的大小是有限制的,并且必须在启动时指定,而元空间可以根据需要自动调整大小。
- 垃圾收集:永久代使用 Java 堆的垃圾收集器进行垃圾回收,而元空间使用本地内存的垃圾收集器。
- 存储内容:永久代主要存储类的信息(如类名、方法名、字段名等),而元空间存储的是类的元数据(如类的结构、方法表、字段表等)。
类信息的存储方式:永久代中的类信息是使用永久代专用的类加载器加载和卸载的,而元空间中的类信息是使用与应用程序类加载器相同的类加载器加载和卸载的。
需要注意的是,永久代在 JDK 8 中已经被元空间所取代,所以在 JDK 8 及以后的版本中,永久代已不存在。
永久代是在 Java 堆中的一个特殊区域,而元空间是在本地内存中的,这个本地内存是什么内存?
元空间是 Java 8 引入的一个新概念,它代替了永久代(PermGen)的概念。与永久代不同,元空间不再是 Java 堆的一部分,而是使用本地内存来存储类的元数据。
本地内存指的是操作系统分配给进程的内存空间,与 Java 堆不同,它并不受 JVM 管理,因此在内存使用上更加灵活。元空间使用本地内存存储类的元数据,可以有效地避免了永久代的一些限制,例如永久代大小有限、永久代垃圾收集效率低等问题。
需要注意的是,由于元空间使用的是本地内存,因此它的大小不再受到 Java 堆大小的限制,但是它的大小仍然会受到操作系统本身的限制,因此需要根据具体的系统配置来进行调整。
Method Area 方法区
直接内存
Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
使用直接内存的好处
-
文件读写流程:
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
-
使用了 DirectBuffer 文件读取流程
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。
在Java中调用直接内存,这个直接内存由谁释放呢?
直接内存的回收机制总结
使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存