# 前言
前面学习了 JVM 运行大概的一个流程,经过痛苦的环境配置,也算是勉强开始进入到这块的学习,本篇是学习 Java 内存管理的记录,多是偏记忆的一些东西也比较简单,本篇中所有代码都在 Clown_java/src/main/java/JavaSE at master・clown-q/Clown_java (github.com)
# JVM 内存管理
Java 中与 C 不同的是,它不需要我们手动的控制内存,所有的内存管理机制都是由 java 自己来完成的,这给我们带来了极大的便利,但是并不是说这比 C 更好,一旦出现内存泄露等问题 C 可以通过更改代码来快速的修复,而在 java 中一般是 JVM 出现了问题,只能说二者各有利弊吧,学习这一块的主要目的是为了当出现有关问题的时候能找到对应的解决方案
# 内存区域的划分
JVM 对于内存管理的机制是分区治理,不同的内存有着不同的功能,在虚拟机运行时,内存区域划分可以看下面这张图
内存区域被分为 5 给区域,其中方法区和堆是所有的线程共享的区域,随着虚拟机的创建而创建,虚拟机的结束而销毁,而其他的 VM 栈,本地方法栈和程序计数器是线程隔离的数据,也就是说每个线程都创建后三者
# 程序计数器
这里的程序计数器和 CPU 中的 PC 寄存器实现的功能很相似,因为 JVM 的目的就是实现和物理机一样执行,在 CPU 中 PC 作为程序计数器,负责存储内存地址,指向下一条要执行的指令在 JVM 中也是如此,在一个线程中程序计数器会指向下一条即将执行的指令
# 虚拟机栈
虚拟机栈顾名思义,它是一个栈结构,当每个方法被执行的时候,JVM 都会同步创建一个栈帧,栈帧中包括了当前方法的一些信息
这里通过一个代码来模拟一下虚拟机栈的运行流程
public class test { | |
public static void main(String[] args) { | |
int sum = a(); | |
System.out.println(sum); | |
} | |
public static int a(){ | |
return b(); | |
} | |
public static int b(){ | |
return c(); | |
} | |
public static int c() | |
{ | |
int a = 10; | |
int b = 20; | |
return a+b; | |
} | |
} |
这里就是一个调用,然后算一下 10+20,这里直接构建一下
PS E:\study_java> javap -v /E:/study_java/target/classes/JavaJVM/JVMMemory/test.class | |
Classfile /E:/study_java/target/classes/JavaJVM/JVMMemory/test.class | |
Last modified 2023-9-7; size 760 bytes | |
MD5 checksum 2238b8ccd5d668265e702bcbdb31b216 | |
Compiled from "test.java" | |
public class JavaJVM.JVMMemory.test | |
minor version: 0 | |
major version: 52 | |
flags: ACC_PUBLIC, ACC_SUPER | |
Constant pool: | |
#1 = Methodref #8.#28 // java/lang/Object."<init>":()V | |
#2 = Methodref #7.#29 // JavaJVM/JVMMemory/test.a:()I | |
#3 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream; | |
#4 = Methodref #32.#33 // java/io/PrintStream.println:(I)V | |
#5 = Methodref #7.#34 // JavaJVM/JVMMemory/test.b:()I | |
#6 = Methodref #7.#35 // JavaJVM/JVMMemory/test.c:()I | |
#7 = Class #36 // JavaJVM/JVMMemory/test | |
#8 = Class #37 // java/lang/Object | |
#9 = Utf8 <init> | |
#10 = Utf8 ()V | |
#11 = Utf8 Code | |
#12 = Utf8 LineNumberTable | |
#13 = Utf8 LocalVariableTable | |
#14 = Utf8 this | |
#15 = Utf8 LJavaJVM/JVMMemory/test; | |
#16 = Utf8 main | |
#17 = Utf8 ([Ljava/lang/String;)V | |
#18 = Utf8 args | |
#19 = Utf8 [Ljava/lang/String; | |
#20 = Utf8 sum | |
#21 = Utf8 I | |
#22 = Utf8 a | |
#23 = Utf8 ()I | |
#24 = Utf8 b | |
#25 = Utf8 c | |
#26 = Utf8 SourceFile | |
#27 = Utf8 test.java | |
#28 = NameAndType #9:#10 // "<init>":()V | |
#29 = NameAndType #22:#23 // a:()I | |
#30 = Class #38 // java/lang/System | |
#31 = NameAndType #39:#40 // out:Ljava/io/PrintStream; | |
#32 = Class #41 // java/io/PrintStream | |
#33 = NameAndType #42:#43 // println:(I)V | |
#34 = NameAndType #24:#23 // b:()I | |
#35 = NameAndType #25:#23 // c:()I | |
#36 = Utf8 JavaJVM/JVMMemory/test | |
#37 = Utf8 java/lang/Object | |
#38 = Utf8 java/lang/System | |
#39 = Utf8 out | |
#40 = Utf8 Ljava/io/PrintStream; | |
#41 = Utf8 java/io/PrintStream | |
#42 = Utf8 println | |
#43 = Utf8 (I)V | |
{ | |
public JavaJVM.JVMMemory.test(); | |
descriptor: ()V | |
flags: ACC_PUBLIC | |
Code: | |
stack=1, locals=1, args_size=1 | |
0: aload_0 | |
1: invokespecial #1 // Method java/lang/Object."<init>":()V | |
4: return | |
LineNumberTable: | |
line 9: 0 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 5 0 this LJavaJVM/JVMMemory/test; | |
public static void main(java.lang.String[]); //main 方法 | |
descriptor: ([Ljava/lang/String;)V | |
flags: ACC_PUBLIC, ACC_STATIC | |
Code: | |
stack=2, locals=2, args_size=1 | |
0: invokestatic #2 // Method a:()I | |
3: istore_1 | |
4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; | |
7: iload_1 | |
8: invokevirtual #4 // Method java/io/PrintStream.println:(I)V | |
11: return | |
LineNumberTable: | |
line 11: 0 | |
line 12: 4 | |
line 13: 11 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 12 0 args [Ljava/lang/String; | |
4 8 1 sum I | |
public static int a(); | |
descriptor: ()I | |
flags: ACC_PUBLIC, ACC_STATIC | |
Code: | |
stack=1, locals=0, args_size=0 | |
0: invokestatic #5 // Method b:()I | |
3: ireturn | |
LineNumberTable: | |
line 15: 0 | |
public static int b(); | |
descriptor: ()I | |
flags: ACC_PUBLIC, ACC_STATIC | |
Code: | |
stack=1, locals=0, args_size=0 | |
0: invokestatic #6 // Method c:()I | |
3: ireturn | |
LineNumberTable: | |
line 18: 0 | |
public static int c(); | |
descriptor: ()I | |
flags: ACC_PUBLIC, ACC_STATIC | |
Code: | |
stack=2, locals=2, args_size=0 | |
0: bipush 10 | |
2: istore_0 | |
3: bipush 20 | |
5: istore_1 | |
6: iload_0 | |
7: iload_1 | |
8: iadd | |
9: ireturn | |
LineNumberTable: | |
line 22: 0 | |
line 23: 3 | |
line 24: 6 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
3 7 0 a I | |
6 4 1 b I | |
} | |
SourceFile: "test.java" |
在编译后,操作数栈深度,局部变量表都是确定的了
main 函数相关信息会被封装栈帧,放到虚拟机栈中
mian 中动态链接到 a () 方法
也是相关的数据封装成栈帧,压入栈顶的位置
这里 a () 方法动态链接这个 b () 方法
就这样一直到 c () 方法,最后的虚拟机栈结构
到这里需要进虚拟机栈的栈帧就已经全部压入栈了,然后栈顶的先执行,执行结束出栈
# 本地方法栈
本地方法栈和虚拟机栈作用差不多,不过他是为了存放本地方法数据的
# 堆
这块区域是整个 Java 程序共享的区域,它主要是存放和管理对象,所以在垃圾回收也是主要作用这一块
# 方法区
方法区和上面的堆也是一样的,都是整个 Java 程序共享的区域,这块区域用于存储所有的类信息,当然在方法区中也不是将所有的信息都堆在一起,方法区中还能细分为几个区域
字符串常量池在 JDK7 之后移动到了堆中
这就很好的解释了为什么 new 两个一样的对象却不相等,看下面这段代码
public class test2 { | |
public static void main(String[] args) { | |
String str1 = "aaa"; | |
String str2 = "aaa"; | |
System.out.println(str1 == str2); | |
System.out.println(str1.equals(str2)); | |
} | |
} |
这里直接通过复制的两个字符串
这里可以看见常量池中有了这个 aaa,这里实际上是下图这种方法存储
在方法区中,公用一个常量,再看下面这段代码
public class test3 { | |
public static void main(String[] args) { | |
String str1 = new String("aaa"); | |
String str2 = new String("aaa"); | |
System.out.println(str1 == str2); | |
System.out.println(str1.equals(str2)); | |
} | |
} |
而这个
因为 equals 比的是值,也就是方法区的值,所以运行结果也就很显而易见了
到这里就可以合理的解释为什么两个值一样的对象不相等
# 内存溢出 & 栈溢出
内存的容量是有闲的,当我们的内存不够用的时候会出现错误
public class test4 { | |
public static void main(String[] args) { | |
int[] ints = new int[Integer.MAX_VALUE]; | |
} | |
} |
像上图中就得到了一个 OutOfMemoryError 的错误,发生了常见的内存溢出。这里通过控制参数将堆内存设置为 1m 大小,并在抛出内存溢出异常的时候保存当时的内存堆转储快照
然后接着用下面这个例子
public class test5 { | |
public static void main(String[] args){ | |
ArrayList list = new ArrayList(); | |
while (true){ | |
list.add(new Object()); | |
} | |
} | |
} |
这里就成功抛出了一个内存溢出的异常,解析一下这个生成的文件
这里可以看见,创建了 360227 个 object 对象,其他的相差很多,这里就已经抛出了内存溢出的错误,接下来再看下面这段代码
public class test6 { | |
public static void main(String[] args) { | |
test(); | |
} | |
public static void test(){ | |
test(); | |
} | |
} |
这是一个无限递归的程序,会不断的调用 test 自身,而方法会被压入到虚拟机栈中,而栈的深度是有闲的,就会出现栈满的情况
# 申请堆外内存
虽然在 java 中内存管理都由 JVM 来自行完成,但是我们也可以像 C 语言那样手动管理,使用到一个堆外内存操作类 Unsafe,就像它的名字一样它是不安全的类,当然我们去操作内存本身就是不安全的
public final class Unsafe { | |
private static native void registerNatives(); | |
static { | |
registerNatives(); | |
sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe"); | |
} | |
private Unsafe() {} | |
private static final Unsafe theUnsafe = new Unsafe(); | |
···· |
可以看到它的构造方法是被 private 修饰的,所以很显然它并不希望我们使用他,通过反射来拿到获取这个类
public class UnsafeTest { | |
public static void main(String[] args) throws IllegalAccessException { | |
Field unsafeField = Unsafe.class.getDeclaredFields()[0]; | |
unsafeField.setAccessible(true); | |
Unsafe unsafe = (Unsafe) unsafeField.get(null); | |
long addr = unsafe.allocateMemory(4); | |
unsafe.putInt(addr,123456);// 向地址中设置值 | |
System.out.println(unsafe.getInt(addr));// 获取对应地址上的内容 | |
unsafe.freeMemory(addr);// 释放内存 | |
} | |
} |
# 补充
# JNI
在 Java 中有一个 JNI 机制,全称是 Java Native Interface 直译过来就是 Java 本地接口。它允许在 Java 虚拟机内运行的 Java 代码利用其他编程语言来实现一些功能
先看下面这个例子,比如说想要用 C 实现两个 int 类型数据之和
public class JNIsum { | |
public static void main(String[] args) { | |
System.out.println(sum(10,20)); | |
} | |
public static native int sum(int a,int b); // 使用关键字 native 修饰,表示 sum 方法是一个本地方法 | |
} |
这样一段代码直接运行肯定是会报错的
可以看到这里报了一个链接错误,接下来这里使用 Javah 命令来生成头文件
javah -classpath .\target\classes\ -d ./jni JavaJVM.JNITest.JNIsum |
这个路径要写对,包的部分用点号连接,然后就会生产一个名字对应的文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JavaJVM_JNITest_JNIsum */
#ifndef _Included_JavaJVM_JNITest_JNIsum
#define _Included_JavaJVM_JNITest_JNIsum
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JavaJVM_JNITest_JNIsum
* Method: sum
* Signature: (II)I //表示两个参数都是Int类型,返回值也是int类型
*/
JNIEXPORT jint JNICALL Java_JavaJVM_JNITest_JNIsum_sum
(JNIEnv *, jclass, jint, jint);//函数定义,需要我们实现的函数
#ifdef __cplusplus
}
#endif
#endif
这里新建一个 C 的项目,然后修改配置将 JNI 相关的头文件导入
这个路径要根据存放 java 的路径对应修改,然后将刚刚生成的头文件也移动过来
最后将这个头文件也加到 CMakelists 这个文件中
现在就万事大吉,实现目标函数就可以了
#include "JavaJVM_JNITest_JNIsum.h" | |
/* | |
* Class: JavaJVM_JNITest_JNIsum | |
* Method: sum | |
* Signature: (II)I | |
*/ | |
JNIEXPORT jint JNICALL Java_JavaJVM_JNITest_JNIsum_sum | |
(JNIEnv *, jclass, jint a, jint b){ | |
return a+b; | |
} |
因为就是一个简单的和,这里直接返回二者和就行,接下来将这个 cpp 文件编译为动态链接库,win 上面就是 dll 文件
gcc .\JavaJVM_JNITest_JNIsum.cpp -I D:\Java\jdk1.8.0_382\include\ -I D:\Java\jdk1.8.0_382\include\win32\ -fPIC -shared -o JNIsum.dll -lstdc++ |
mac 中是 dylib 文件,linux 中是 so 文件
这里就生成了一个 dll 文件
public class JNIsum { | |
static { | |
System.load("E:\\C\\JNIsum.dll"); | |
} | |
public static void main(String[] args) { | |
System.out.println(sum(10,20)); | |
} | |
public static native int sum(int a,int b); // 使用关键字 native 修饰,表示 sum 方法是一个本地方法 | |
} |
这里通过静态代码块来连接到方法上
这样就能够正常执行了,这里简单的补充一下这个 JNI 的写法