其实这个问题非常简单,JVM在运行我们写好的代码时,他是必须使用多块内存空间的,不同的内存空间用来放不同的数据,然后配合我们写的代码流程,才能让我们的系统运行起来。
举个最简单的例子,比如咱们现在知道了JVM会加载类到内存里来供后续运行,那么我问问大家,这些类加载到内存以后,放到哪儿去了呢?想过这个问题吗?
所以JVM里就必须有一块内存区域,用来存放我们写的那些类。
包括我们定义的成员变量,类变量,方法,局部变量等等,都在jvm内存中对应着一块内存来记录存储。
在JDK1.8之前的版本里,代表JVM的一块区域。在1.8版本以后,这块区域的名字改了,叫做“Matespace”,可以认为是“元数据空间”这样的意思,当然这里主要存放的还是我们自己写的各种类的相关信息。
举个例子。有如下两个类,User1类没有成员变量,而User2类有一个realName的类变量。
public class User1 { private String userName = "wangwu"; } public class User2 { private static String realName = "zhangsan"; private String userName = "lisi"; } 复制代码
这两个类被加载到JVM,就会存放在这个方法区里面(类的所有类变量都会被赋值)。如下图
我们知道,被加载到jvm的类对象是我们写的.java文件被编译之后的.class文件。
在编译过后会将我们的代码编译成计算机能读懂的字节码。而这个.calss文件就是,就是我们代码编译好的字节码了。
加载到内存以后,字节码执行引擎就开始工作了。去执行我们编译出来的代码指令,此时问题来了,我们是不是需要一块内存空间来记录我们字节码执行引擎目前执行到了哪行代码?这一块特殊的内存区域就是“程序计数器”,就是用来记录当前执行的字节码指令的位置。
注:多线程环境并发执行的情况下,计算器CPU是一定的,在CPU从线程1切换到线程2,再切换回线程1的时候,是不是要知道线程1执行到哪一步了,这就是程序计数器的作用。因此,当线程再次上下文切换到之前的代码时,就需要一个专门记录当前线程执行到了哪一条字节码。所以,每一个线程都有这自己的程序计数器。
当线程执行到某个方法的时候,如果这个方法有局部变量,那么就需要一块区域来存放局部变量的数据信息。这个区域就叫做java虚拟机栈。
每一个线程都有一个自己的java虚拟机栈,比如说当执行main方法的时候就会有一个main线程,用来存放main方法中定义的局部变量
public class TestController { public static void main(String[] args) { int i = 1; User1 user1=new User1(); user1.setUserName("sss"); } } 复制代码
比如上面的main()方法中,其实就有一个"user1"的局部变量,他是引用一个User1的实例对象的,还有一个"i"的局部变量. 如下图:
既然是栈,那就遵循后进先出的原则。当方法执行完毕以后,这个栈桢就会出栈,里面的局部变量信息就会从内存删除。所以局部变量是线程安全的。因为只有当前线程能获取到这个值。
Q:为什么要用后进先出的数据结构?
A:假设a方法中调用b方法,此时a方法的栈桢先入栈,b方法的栈桢后入栈。当b方法执行完毕后,b方法的栈桢先出栈,继续执行a方法,然后a方法的栈帧再出栈。所以使用一个后进先出的栈结构是非常完美的。
还是上面代码,当main线程执行main()方法的时候,首先在堆内存中实例化User1对象,然后在局部变量中创建user1,user1存的是实例化User1对象的内存地址。然后执行Student对象的getName()方法。
如下图:
其实在JDK的很多底层代码API中,比如NIO。
如果你去看源码会发现很多地方的代码不是java写的,而是走的native方法去调用本地操作系统里面的一些方法,可能调用的都是c语言写的方法。
比如说:
public native int hashCode(); public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2); 复制代码
在调用这种native方法的时候,就会有线程对应的本地方法栈,这个其实类似于java虚拟机栈。也是存放各种native方法的局部变量表之类的信息。
还有一块区域,是不是jvm的,通过NIO中的allocateDirect这种API,可以在jva堆外分配内存空间,然后通过java虚拟机栈里的DirectByteBuffer来引用和操作堆外内存空间。