【JVM】01虚拟机内存模型
学习链接:https://blog.csdn.net/u010425776/article/details/51170118
博主整理的条理清晰,在这里先感谢博主分享
去年看视频学习写过一篇JVM的博客,今年已经忘得精光,归根结底是没有结合实践深入理解,为加深记忆,手打重新整理一下,一边思考,加深理解:
JVM内存模型
Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部分,分别是:
1. 程序计数器
2. Java虚拟机栈
3. 本地方法栈
4. 堆
5. 方法区。
1、程序计数器:
1.1什么是程序计数器:
程序计数器是较小的一块内存空间,记录了当前线程执行到的字节码文件的行号
1.2、程序计数器的作用:
1、字节码解释器通过改变程序计数器依次读取指令,实现代码流程控制,如顺序执行,循环等
2、多线程情况下记录当前线程执行到的位置,方便线程激活时继续执行
1.3、程序计数器的特点:
1、是一片较小的存储空间
2、线程私有,每条线程都有自己的程序计数器
3 、是唯一一个不会出现OutOfMemoryError的内存区域
4、生命周期随线程的创建而创建,随现成的销毁而销毁
2. Java虚拟机栈(JVM Stack)
2.1什么是java虚拟机栈:
java虚拟机栈是描述方法运行过程的内存模型
2.2java虚拟机栈的作用:
java虚拟机栈会为每个即将运行的方法分配内存,称作栈帧,存储运行过程中需要的一些信息。
包括:
1、局部变量表(基本数据类型、引用引用数据类型变量(内存地址?)、returnAddress(没听过))
2、操作数栈
3、动态链接
4、方法出口信息
5、等
2.3过程与特点:
当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块“栈帧”,栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等。当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中(看不懂)。
当这个方法执行完毕后,这个方法所对应的栈帧将会出栈,并释放内存空间。
注意:人们常说,Java的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!这里的“堆”可以这么理解,但这里的“栈”只代表了Java虚拟机栈中的局部变量表部分。真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息.
3.本地方法栈
与虚拟机栈类似,好像是用于native修饰的本地方法balabala~
4.堆
4.1什么是堆与堆的作用:
堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。
4.2堆的特点:
1、线程共享:
整个java虚拟机只有一个堆,所有线程都访问同一个堆。而程序计数器,java虚拟机栈,本地方法栈都是一个线程对应一个。
2、虚拟机启动时创建
3、垃圾回收的主要场所
4、可以进一步细分为新生代,老年代(这个记得JDK7之后与之前有所改动)。新生代又可以分为Eden(伊甸,起点的意思?)、from servior、to servior。不同的区域存放不同周期的对象。虚拟机根据不同的区域采取不同的垃圾回收算法,从而更高效。
5、主流虚拟机堆都是可扩展的,当线程请求虚拟机分配内存,但堆已满的时候,会抛出outofmemoryError
5.方法区
5.1、什么是方法区:
java虚拟机规范中定义方法区是堆的一个逻辑部分。
方法区中存放被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
5.2、方法区的特点:
1、线程共享:由于是堆的一部分,所以和堆一样都是线程共享的,整个虚拟机都只有一个方法区。
2、永久代:方法区中的数据一般要长期存在,而且它又是堆的逻辑分区,所以按照堆的划分方法,我们把方法区称为老年代。
3、方法去中的数据一般长期存在,垃圾回收器一次只能回收少量数据,所以垃圾回收策略主要是对常量池的回收以及类型的卸载。
4、java虚拟机规范对方法区要求比较宽松,允许固定或扩展大小,也允许不执行垃圾回收。
5.3、运行时常量池
方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码(这是啥)。其中常量存储在运行时常量池中。
我们一般在一个类中通过public static final来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。
当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。
当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。
6. 直接内存
直接内存是除Java虚拟机之外的内存,但也有可能被Java使用。
在NIO中引入了一种基于通道和缓冲的IO方式(看不懂)。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。
直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OOM异常。
创建对象过程与访问对象的过程
创建对象的过程:https://blog.csdn.net/u010425776/article/details/51190801
摘抄对象内存模型:
对象的创建过程
当虚拟机遇到一条含有new的指令时,会进行一系列对象创建的操作:
-
检查常量池中是否有即将要创建的这个对象所属的类的符号引用;
- 若常量池中没有这个类的符号引用,说明这个类还没有被定义!抛出ClassNotFoundException;
- 若常量池中有这个类的符号引用,则进行下一步工作;
-
进而检查这个符号引用所代表的类是否已经被JVM加载;
- 若该类还没有被加载,就找该类的class文件,并加载进方法区;
- 若该类已经被JVM加载,则准备为对象分配内存;
-
根据方法区中该类的信息确定该类所需的内存大小;
一个对象所需的内存大小是在这个对象所属类被定义完就能确定的!且一个类所生产的所有对象的内存大小是一样的!JVM在一个类被加载进方法区的时候就知道该类生产的每一个对象所需要的内存大小。 -
从堆中划分一块对应大小的内存空间给新的对象;
分配堆中内存有两种方式:- 指针碰撞
如果JVM的垃圾收集器采用复制算法或标记-整理算法,那么堆中空闲内存是完整的区域,并且空闲内存和已使用内存之间由一个指针标记。那么当为一个对象分配内存时,只需移动指针即可。因此,这种在完整空闲区域上通过移动指针来分配内存的方式就叫做“指针碰撞”。 - 空闲列表
如果JVM的垃圾收集器采用标记-清除算法,那么堆中空闲区域和已使用区域交错,因此需要用一张“空闲列表”来记录堆中哪些区域是空闲区域,从而在创建对象的时候根据这张“空闲列表”找到空闲区域,并分配内存。
综上所述:JVM究竟采用哪种内存分配方法,取决于它使用了何种垃圾收集器。
- 指针碰撞
-
为对象中的成员变量赋上初始值(默认初始化);
-
设置对象头中的信息;
-
调用对象的构造函数进行初始化
此时,整个对象的创建过程就完成了。
访问对象的过程
我们知道,引用类型的变量中存放的是一个地址,那么根据地址类型的不同,对象有不同的访问方式:
-
句柄访问方式
堆中需要有一块叫做“句柄池”的内存空间,用于存放所有对象的地址和所有对象所属类的类信息。
引用类型的变量存放的是该对象在句柄池中的地址。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址再访问对象。 -
直接指针访问方式
引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。
但对象所在的内存空间中需要额外的策略存储对象所属的类信息的地址。