JVM内存结构及程序执行的内存分析过程

一. JVM内存结构 

1. JVM的内存结构大概分为 

在这里插入图片描述

堆(Heap)

线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。

方法区(Method Area)

又叫静态区,线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。

方法栈(JVM Stack)

线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。
每个线程会有一个私有的栈。每个线程中方法的调用又会在本栈中创建一个栈帧。在方法栈中会存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小 

本地方法栈(Native Method Stack)

线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。

程序计数器(Program Counter Register)

线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖于此计数器。JVM中的程序计数器也是在Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。在任意时刻一条JVM线程只能执行一个方法的代码,方法可以是Java方法,或者是native方法。

重点

重点 在Java内存运行时的各个部分中,程序计数器、虚拟机栈、本地方法栈这三个区域随线程生随线程死。栈中的栈帧随着方法的进入和退出有条不紊的进行着出入栈操作。这几个区域是不需要过多的考虑内存回收的问题,因为方法或者线程结束,内存自然就跟着被回收。 然而,Java堆和方法区则不同——一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样。这部分内存的分配是动态的,我们只有在程序运行期间才能知道会创建哪些对象。这部分内存,就是我们关注的重点。

2.  jvm主内存与工作内存

主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。

  • 所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的。
  • 每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
  • 线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。

java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改。总结java的内存模型,要解决两个主要的问题:可见性和有序性。
多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的。当线程操作某个对象时,执行顺序如下:

  • 从主存复制变量到当前工作内存 (read and load)
  • 执行代码,改变共享变量值 (use and assign)
  • 用工作内存数据刷新主存相关内容 (store and write)

2.1 volatile关键字

任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于Valatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是有序的。

2.2 synchronized关键字

java用synchronized关键字做为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。
一个线程执行临界区代码过程如下:

  1. 获得同步锁
  2. 清空工作内存
  3. 从主存拷贝变量副本到工作内存
  4. 对这些变量计算
  5. 将变量从工作内存写回到主存
  6. 释放锁

可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
 

 二. 程序执行的内存分析过程

示例代码:

public class Person {

       String name;

       int age;

       public void show(){

              System.out.println("姓名:"+name+",年龄:"+age);

       }

}

public class TestPerson {

       public static void main(String[ ] args) {

              // 创建p1对象

              Person p1 = new Person();

              p1.age = 24;

              p1.name = "张三";

              p1.show();

              // 创建p2对象

              Person p2 = new Person();

              p2.age = 35;

              p2.name = "李四";

              p2.show();

       }

}

对上面代码执行过程的内存分析(借助上图理解):

1、从main()方法开始设置栈帧,因为main方法是程序的入口,args的值为null

2、创建p1对象,p1变量的值为null,因其是引用类型的

3、new Person()执行构造方法,虚拟机栈中要新开辟一个栈桢Person()

4、此时堆里面会新增一个对象,对象里name默认值为null,age默认值为0

5、方法区里有Person类,而show()方法的具体信息会存放在Person类中

6、若方法区的show()有地址,则堆中的show指向方法区的show();假设堆中的对象地址为0x1024,因Person p1 = new Person()对象赋值给p1,故p1的地址为0x1024,main()栈帧的p1指向堆中的对象

7、执行完Person p1 = new Person(),Person()栈帧出栈

8、p1.age = 24进行赋值操作,地址里的name不再是null,值变为24

9、p1.name = "张三"进行赋值操作,“张三”并不是基本数据类型,String在java中相当于一个类(class)属于引用数据类型,在方法区的字符串常量池,将该地址给name

10、p1.show()调用会在虚拟机栈中开辟一个为p1.show()的栈帧,该栈帧有默认参数this,地址为0x1024,this对应的是当前调用的方法

11、执行p1.show(),打印姓名、年龄后,p1.show()栈帧出栈

12、同理创建p2对象之后代码的执行过程跟p1一样,重复2-10的步骤

13、最后main栈帧出栈,虚拟机栈回收;堆被垃圾处理器回收,方法区也在内存中被清除
 

猜你喜欢

转载自blog.csdn.net/m0_69057918/article/details/131129808