JVM内存分配
一、JVM内存区的划分
运行一个程序后,JVM
会把内存划分为几个大区:程序计数器、Java
虚拟机栈、本地方法栈、堆、方法区,以及一些辅助内存区如:运行时常量池、直接内存。各区域特点与作用如下:
1、程序计数器(PC Register
)
每条线程有一个,保存当前执行的字节码行号。类似于 CPU 的指令计数器,用以保证线程切换后能回到原来的位置。
2、Java 虚拟机栈(JVM Stack
)
每个线程有一个栈,里面一层层的是“栈帧”(方法调用时创建)。栈帧里存放局部变量表、操作数栈、动态链接信息、方法出口等。方法结束后,栈帧自动销毁。
所以:方法调用 = 压栈,方法返回 = 出栈。
3、本地方法栈(Native Method Stack
)
专门给本地方法服务。比如 JNI 调用时会用到。
4、堆(Heap
)
所有线程共享,几乎所有对象都在这里分配。GC
的主要工作区域。
堆区内部又进一步分代为新生代(Young Generation
)和老年代(Old Generation
),关于新生代和老年代的知识曾在JVM的垃圾处理机制中详细介绍
5、方法区(Metaspace,JDK8+
)
存储类元数据:类结构、常量池、静态变量、方法字节码。
JDK7 以前用 永久代(PermGen
),JDK8 改为直接用本地内存的 Metaspace
,避免 PermGen 容量不足。
6、运行时常量池
属于方法区的一部分,用于存放字符串常量、final 常量、符号引用等。
7、直接内存(Direct Memory
)
不属于 JVM
管理,而是直接用操作系统内存,主要目的是减少堆到系统内核的复制,提高 IO 性能。
二、Java程序运行时JVM的动态过程
1、编译:将源代码编译成.class
文件
2、类加载器:把 .class
加载到 方法区,解析符号引用。
3、运行时:
(1) 线程运行时从 PC 计数器 知道执行哪行。
(2) 方法调用会往 JVM
栈 压栈。
(3) 遇到 new
就在 堆 上分配对象。
(4) 需要 JNI
就进 本地方法栈。
三、对象储存结构
在运行时,会将绝大多数new
出来的对象存放到堆区中,方法区中只会存放类信息,但是不会存放对象的实例,而在栈区中,由于对象是一个引用数据类型,只会存放对象的对象引用(指针),而不会存放对象的实例。
在堆区中,对象又被分为了三个区:对象头,实例数据,对齐填充。
1、对象头:包含存放哈希码、GC信息、类状态等的Markword
(通常8字节)和指向对象所属类的元数据(通常为4到8字节,取决于是否使用压缩指针),一般对象头需要12~16个字节。
2、实例数据:存放对象的字段
3、对齐填充:由于JVM
要求对象的大小必须为8字节的倍数,所以如果当前对象的大小不满足要求,就必须要补齐,这就是对齐填充
当被询问Object
的内存大小时,要注意其询问的是Object
本身还是对象的实例,如果Object
本身,实际上就是指存在栈区的对象指针,如果问的是对象的实例,就要算堆区的相关数据的大小
四、JNI是什么
Java本地接口(JNI
,Java Native Interface
),是Java
调用本地语言(如C++、Python)的代码的桥梁。由于Java
是运行在JVM
虚拟机中的,如果想要调用本地的代码如C/C++
库,系统API
,就需要使用到JNI
。
但是使用JNI
也有一系列的缺点,例如跨平台性差(因为不是编译成字节码在JVM
跑的),安全性下降(如果JNI
的代码崩溃,可能导致JVM
跟着一起崩溃)等。
JNI
相关的语法如下:
public class HelloJNI {public native void sayHello(); // 本地方法(没有实现)static {System.loadLibrary("hello"); // 加载本地库}
}
然后在C++
或者别的语言中实现即可(这里使用C++
示例):
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {printf("Hello from C!\n");
}
其中,native
函数只是提示这里需要调本地方法,JVM
会去对应的库中调用所有的对应的本地方法
五、JNI的调用过程
System.loadLibrary("hello");
会让JVM
去操作系统中加载一个动态链接库,这个库不会被放到 JVM 的方法区里,而是由操作系统的动态链接器把它加载到进程的本地内存空间。JVM
只是记下这个库,并在调用 native
方法时跳到对应的地址。
JNI
运行时的调用过程如下:
1、Java
代码调用一个 native
方法。
2、JVM
发现这个方法没有 Java
字节码实现,于是切换到本地方法栈。
3、JVM
通过之前绑定的动态库地址,跳到对应的C/C++
实现执行。
4、本地代码执行完毕后,返回结果,再切回 Java
栈。