JVM运行时内存结构是怎么的?
运行时内存划分为:堆、java虚拟机栈、本地方法栈、程序计数器、方法区。
程序计数器
记录当前执行的字节码行号,可以用于线程恢复,改变它可以用于执行下一个字节码指令、跳转等,很明显是线程私有的,占用内存空间很小,是唯一JVM没有规定任何OOM(OutOfMemoryError)的区域。
java虚拟机栈
用于存放java线程在执行时存放的本地数据,如局部变量表、操作数栈、动态链接(当前方法指向运行时常量池的应用)、返回地址(返回被调用的地方)。是线程私有的,在线程创建的时候会分配默认为1M(通过-Xss指定)。每进入一个方法都会创建一个栈帧,退出一个方法就会销毁一个栈帧。
如果一直进入栈帧不退出栈帧就会出现StackOverflowError。如果在分配虚拟机栈时候没有内存来就会OOM。
本地方法栈
调用JNI(java native interface)时使用到的,虚拟机可以自由实现它,和java虚拟机栈类似,也会发生StackOverflowError和OOM。
堆
用于存放java对象,线程共享的,GC回收的主要区域。可以使用-Xms和-Xmx指定最小和最大空间。如果空间不足会发生OOM。
对象一定放堆中吗?
不一定,即时编译器的存在(缓存编译结果(机器码)不用变解释边执行)使逃逸分析变的简单。
逃逸分析,就是判断对象的作用域是不是当前方法,如果不是说明可能存在逃逸。如果不存在逃逸就分配在栈中,出栈的时候就销毁了不用gc了。可以提高JVM的效率。
我们可以试下,默认是开启逃逸分析的。
/**
* 逃逸分析-栈上分配
* -XX:-DoEscapeAnalysis -XX:PrintGc
*/
public class EscapeAnalysisTest {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000000; i++) {//5000万次---5000万个对象
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
Thread.sleep(3000);
}
static void allocate() {//逃逸分析(不会逃逸出方法)
//这个myObject引用没有出去,也没有其他方法使用
MyObject myObject = new MyObject(2020, 2020.6);
}
static class MyObject {
int a;
double b;
MyObject(int a, double b) {
this.a = a;
this.b = b;
}
}
}
复制代码
直接java -XX:PrintGC 执行class结果:
19 ms
复制代码
直接java -XX:-DoEscapeAnaysis -XX:PrintGC 执行class结果:
[GC (Allocation Failure) 33280K->496K(125952K), 0.0047785 secs]
[GC (Allocation Failure) 33776K->376K(159232K), 0.0008720 secs]
[GC (Allocation Failure) 66936K->408K(159232K), 0.0006539 secs]
[GC (Allocation Failure) 66968K->424K(225792K), 0.0005009 secs]
[GC (Allocation Failure) 133544K->392K(225792K), 0.0006146 secs]
[GC (Allocation Failure) 133512K->456K(354304K), 0.0004851 secs]
[GC (Allocation Failure) 266696K->312K(354304K), 0.0009039 secs]
[GC (Allocation Failure) 266552K->312K(621056K), 0.0003459 secs]
263 ms
复制代码
效率提高的很明显,没有发现gc。
方法区
用于存放类的元信息、常量、静态变量、即时编译后的机器码等数据,线程共享的。如果空间不足会发生OOM。可以通过-XX:MaxDirectMemorySize来设置。
常量池
静态常量池
class文件中存在的,存放编译器生成的过程中存放的各种字面变量和引用(符号引用,具体可以见下面的字节码),能节约字节码空间。随便找个class文件使用javap -v
查看就能找到。
Constant pool:
#1 = Methodref #16.#42 // java/lang/Object."<init>":()V
#2 = Class #43 // com/study/juc/SyncUnFairLockTest$DiningRoom
#3 = Methodref #2.#44 // com/study/juc/SyncUnFairLockTest$DiningRoom."<init>":(Lcom/study/juc/SyncUnFairLockTest$1;)V
#4 = Class #45 // java/lang/Thread
#5 = InvokeDynamic #0:#50 // #0:run:(Lcom/study/juc/SyncUnFairLockTest$DiningRoom;)Ljava/lang/Runnable;
#6 = Class #51 // java/lang/StringBuilder
#7 = Methodref #6.#42 // java/lang/StringBuilder."<init>":()V
#8 = String #52 // 同学编号:00
复制代码
运行时常量池
就是在运行时产生的,和静态常量池的区别是产生时间不一样,引用使用的都是直接引用(真实的内存地址)。目的都是为了节约空间避免重复创建。
字符串常量池
JVM实例全局共享的,而运行时常量池和静态常量池的作用域是类。
直接内存
为了避免java堆和Native堆的来回复制,直接使用堆外内存,可以提高效率。比如NIO的DirectByteBuffer引用的是堆外内存。如果空间不足会发生OOM。
使用XX:MaxDirectMemorySize可以进行设置。