JAVA虚拟机(二) ----内存区域划分

前一段时间,在网上划水,偶然看见一篇关于虚拟机内存区域划分的博文,个人感觉写的还不错,可惜这篇文章的作者已经褪去了程序员的衣衫转职卖水果了…
这真是令人有些遗憾! https://www.cnblogs.com/whgk/p/6138522.html

下面,用自己理解的方式结合他的博文,学习一下java虚拟机的内存分布

1、运行时数据区域:
Java虚拟机在执行Java程序时会将其管理的内存按照用于划分为若干个不同的数据区域,这些区域有着各自不同的生命周期。根据《JAVA虚拟机规范》,Java虚拟机管理的内存会包含以下几个区域。其中可以分为共享内存区以及线程隔离数据区两个部分,下图是具体内存分布情况。
在这里插入图片描述

1.1、程序计数器:
程序计数器是一块很小的内存区域,可以看做当前线程执行字节码的行号指示器,每一个线程,线程私有,是Java虚拟机规范中唯一没有规定任何OutOfMemoryError情况区域,随着JVM启动而生,关闭而死。

举个例子:
多个线程的执行,其中某个线程执行到了一半,停了下来让出cpu资源让其他线程执行,等待其他线程执行完毕,获得CPU资源又可以在原先执行到的地方开始执行,之所以能在停下来的地方重新开始执行,就是程序计数器的作用。

1.2、本地方法栈:
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务。虚拟机规范中对本地方法栈中的方法使用的语言,使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

很多的算法或者一个功能的实现,都被java封装到了本地方法中,程序直接通过调用本地的方法就行了,本地方法栈就是用来存放这种方法的,实现该功能的代码可能是C也可能是C++,不一定就是java实现的。只需了解一下就行了,下面是学习的重点。

1.3 虚拟机栈:

虚拟机栈同程序计数器一样,线程私有,生命周期和线程相同。
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用来存放存储局部变量表、操作数表、动态连接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。虚拟机栈中还有很多概念,这次就不讲了,有时间再写一个单章出来研究一下

在这里插入图片描述
举个例子:
比如我们看springboot的启动类:

public static void main(String[] args) {
		SpringApplication.run(Application.class, args);              
	}

在这里插入图片描述
springboot在启动类中运行了main方法创建一个栈帧,然后会调用run方法有创建栈帧,当然run方法里面还会其他的方法调用又会继续创建栈帧,一个方法执行完毕了,会根据返回地址返回,然后此方法创建的栈帧会有一个出栈的动作,当main方法出栈了,程序也就结束了。

1.4、堆:
堆空间的分布情况,我的上一篇博文有一点介绍 ,有兴趣的可以看一下,这儿就不多介绍了。

1.5、方法区:

和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、和编译器编译后的代码(也就是存储字节码文件。.class)等数据,这里可以看到常量也会在方法区中,是因为方法区中有一个运行时常量池,为什么叫运行时常量池,因为在编译后期生成的是各种字面量(字面量的意思就是值,比如int i=3,这个3就是字面量的意思)和符号引用,这些是存放在一个叫做常量池(这个常量池是在字节码文件中)的地方,当类加载进入方法区时,就会把该常量池中的内容放入运行时常量池中。这里要注意,运行时常量池和常量池,不要搞混淆了,字节码文件中也有常量池。现在只需要知道方法区中有一个运行时常量池,就是用来存放常量的。还有一点,运行时常量池不一定就一定要从字节码常量池中拿取常量,可能在程序运行期间将新的常量放入池中,比如String.intern()方法,这个方法的作用就是:先从方法区的运行时常量池中查找看是否有该值,如果有,则返回该值的引用,如果没有,那么就会将该值加入运行时常量池中。

2、分析:

public class Test {

    public static void main(String[] args) {
        Car car = new Car();
        car.color = "red";
        car.num = 10;
        car.run();

        Car car2 = new Car();
        car2.color = "blue";
        car2.num = 5;
        car2.run();

    }

     static class Car{
         String color;
         int num;

        public void run(){
            System.out.println("----car ");
        }
    }
}

运行… …

在这里插入图片描述
jvm运行编译后的class文件:
1、首先将Test.class 文件 加入方法区,若字节码文件常量池中有常量,则加入运行时常量池
2、运行到main方法,则在虚拟机栈中创建一个栈帧,存放着局部变量等等
3、运行到Car car = new Car(); 检查方法区是否含有Car.class 若没有则加入方法区,new Car() 则会在堆空间中开辟一个空间存放,地址是0x001,含有两个属性color和num 初始值为 null和0;
4、通过car 这个引用为堆空间中的属性设值:color = “red”,num=10;
5、运行到car.run()方法,先在虚拟机栈中创建一个run()方法的栈帧,然后运行打印语句,出栈
6、运行到Car car2 = new Car(); 方法区已有Car.class ,直接在堆空间中开辟一块区域存放 new Car(); 地址为0x002 含有color和num属性,初始值也是null和0;
7、通过car2引用设值 color = “blue” num = 5;
8、运行run() 方法 ,则现在虚拟机栈中创建一个栈帧,打印语句,出栈
9、程序执行完毕,main方法的栈帧处于虚拟机栈最顶层,出栈,程序结束

以上的流程只是参照别人的理解画的,好记性不如烂笔头,自己动手实践一遍,还是能感受到很大差别的!!!

猜你喜欢

转载自blog.csdn.net/qq_37323658/article/details/88964154