《深入理解Java虚拟机》 -- 内存

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《深入理解Java虚拟机》 -- 内存相关的知识,希望对你有一定的参考价值。

  JVM对于操作系统来说是一种应用程序,JVM要运行的时候,操作系统会创建对应的进程而且分配一定大小的内存。

 一、内存结构

  当虚拟机得到系统分配的内存后,它在其内存空间中就是老大,管理对象内存的分配以及对象内存的回收,同时可以根据虚拟机的规范对其内存空间划分不同的区域。主要分为运行数据区、执行引擎、本地接口与本地类库。结构如下图:

技术分享

  大多数程序员更多关注的是变量是怎么存储的或者存在哪个区域、还有对象的内存分配关系,所以在这个层面上来看可以分为堆与栈两个内存区域,而这两个区域都是在运行数据区,所以我们常说的JVM内存常常指的是运行时数据区。其实,这个区域还可以继续划分,分为堆,虚拟机栈,本地方法栈,方法区。这里主要介绍的也就是这五个区域。

二、五个内存空间

  1.堆

    这是一块大家挺熟悉的区域,有点基础的都知道通过new创建的对象都是在这里分配的。关于这点,Java虚拟机规范中是这样描述的:

            the heap is the runtime area from which memory for all class instances and arrays is allocated

  意思就是说:所有的对象实例以及数组都要在堆上分配。同时,这块区域是虚拟机所管理的最大一块区域。

  我们知道Java是支持多线程的。在只有一个Java程序的时候,对象都在这存储没有问题;那多个程序同时运行呢?也就可以理解为多个线程的情况下,对象还是存储在同一个区域吗?答案是的。所有就是包括所有线程的创建的对象都会在这个区域存放,也就意味着这块区域是所有线程共享的。

  既然是所有线程共享的区域,也就是这个区域的对象是对所有线程都是公开的,那么就会有一个问题:怎么确保A线程要访问自己内部的对象的时候不会去访问到B线程的对象?JVM采用的是引用的方法,它不会让你直接访问在堆上的对象,而是同时在栈上的引用来访问对象,引用类型是在虚拟机栈上存放的,这个是线程独有的。

  虽然这些区域的内存是虚拟机管理的,档在一个需要存放更多的对象的时候,可能堆的大小不足以满足需要,就需要改变可是堆的大小。可以设置相应的参数大小。

-Xms : 初始堆的大小
-Xmx : 最大堆内存大小
一般设置这两个一样的大小
例如: java -Xmx128m -Xms128m
-Xmx128m:设置JVM最大可用内存为128M
-Xms128m:设置JVM最小内存为128m

另外,堆的空间还可以继续划分为新生代与老年代;具体可查阅Java垃圾回收机制内容。

2、虚拟机栈

  这个主要描述的是Java方法执行的内存模型:每个方法被执行的时候,都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。栈帧是保存在虚拟机栈中的,是用来存储数据和存储部分过程结果的数据结构。

  这里不太多说栈帧的结构以及各个组成部分的内容。要说一说这个局部变量表;这个存放的是八种基本数据类型(分别是boolean、byte、char、short、int、float、long、double),引用类型(指向堆中的对象)、returnAddress 类型(指向了一条字节码指令的地址)。

  这里有一个叫局部变量空间(slot)的概念,也叫做变量槽。首先局部变量表可以看做是一个存储变量的的一种类似线性表的结构。

技术分享

  一个slot可以存放等于小于32位的数值。即便是没有32位的基本数据类型数据也会占据一个slot。2个Slot可以存储一个类型为long或double的64bit数值。

  还有这里引用类型是指向堆内存中的对象的。也就是Object obj = new Object();中的obj变量,通过访问这个引用就可以间接访问到对象,而且这个虚拟机栈不是线程共享的,虚拟机会为每一个线程都创建一个栈,与线程的生命周期一致。A线程不能访问到B线程的引用,也就是说即便是堆上的对象是共享的,这样也不能访问到B上的对象。

3.本地方法栈

  这个与虚拟栈类型,不同的是虚拟机栈是为执行Java方法服务的,而本地方法栈是为native方法服务的,也就是本地方法。

  在Java中,有些方法涉及到与操作系统交互,获取和使用操作系统的硬件或软件的资源,单纯的用Java实现要么实现很困难,或者不能实现,这时候就要使用本地方法,本地方法就是用其他语言(比如c)写的方法,然后在JVM中调用。一般而言,本地方法速度会比Java方法要快。

技术分享

4、方法区

  这个就是大家所说的“静态区”啦~类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息都是在这个区域存放的。方法区与Java 堆一样,是各个线程共享的内存区域。当虚拟机要创建一个A a = new A();对象的时候,会先去方法区找A类的类信息,找到了就在堆上创建一个对象,并让对象指向该类信息;如果没有找到,那就从外部load进来,放到方法区。

  此外,这里还有一个重要的区域就是运行时常量池。用于存放编译期生成的各种字面常量和符号引用。

  字面常量:相当于在Java语言层面上的常量的意思,包括字符串,还有final修饰的变量;

  符号引用:包括类和接口的全限定名、字段名称和描述符、方法名称和描述符

  我们知道在Java中基本数据类型的包装类大多数都会有常量池,比如Integer的整数池,默认创建了数值在[-128,127]的对象在常量池中泡着呢~同时,String也有字符串常量池。要注意的是:

  1.基本数据的包装类中float与double是没有常量池的;

  2.要区分常量池与方法区的运行时常量池,我们常说的常量池是指存放String等类型的值,而运行时常量池它能够放这些东西,但是不仅仅放这些还会有符号引用!

5、程序计数器

  程序计数器一块是很小的内存区域,主要作用是记录当前线程所执行的字节码的行号。字节码解释器工作时就是通过改变当前线程的程序计数器选取下一条字节码指令来工作的。任何分支,循环,方法调用,判断,异常处理,线程等待以及恢复线程,递归等等都是通过这个计数器来完成的。因为每个线程有自己的执行代码与流程,所以这个是线程独有的。

三、主内存与工作内存

  虚拟机的内存还有另一种划分方式:

    1.主内存:所有的变量都存放在主内存中,对于所有线程都是共享的;

    2.工作内存:工作内存中保存的是主内存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量;

                  技术分享

  这里讲的主内存与工作内存是和上面的堆,栈等是不同的一种划分方式,不同层次上的划分依据。堆栈的方式更多是从对象、变量的角度来看,关注的是怎么存,存在哪;而工作内存与主内存呢更多是从线程的角度上来看,关注的是线程怎么与内存交互。

  如果硬要勉强关联起来的话,从是否是线程共享的角度看,那主内存包括堆与方法区,工作内存包括是虚拟机栈与本地方法栈。如果觉得有些乱,这部分可以不看,这里只是说下有这种划分方法。

四、小结

  堆:存放对象以及数组,线程共享

  虚拟机栈:描述的是Java方法,线程独有

  本地方法栈:描述本地方法(native),线程独有

  方法区:存放类信息、常量、静态常量,包括常量池,线程共享

  程序计数器:记录下一条要执行的字节码指令,线程独有


以上是关于《深入理解Java虚拟机》 -- 内存的主要内容,如果未能解决你的问题,请参考以下文章

深入理解java虚拟机:java内存溢出实战

深入理解Java虚拟机——内存分配

深入理解Java虚拟机-常用vm参数分析

《深入理解JAVA虚拟机》——学习笔记

深入理解java虚拟机系列:java内存区域与内存溢出异常

深入理解 Java 虚拟机之学习笔记