JVM-初识java虚拟机(一)-- jvm内存模型及对象管理

引言:什么是jvm

Jvm是一个虚拟机,是运行字节码(class)文件的机器(只要是字节码就可以,并不一定是java)
在这里插入图片描述

根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域。
在这里插入图片描述

Metadata元数据空间(方法区)

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

在这里插入图片描述
:java jdk1.7中的常量池移到了堆中,同时在jdk1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域

程序计数器

程序计数器(唯一不会发生内存溢出的区域): 程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

为什么需要程序计数器 : 我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

程序计数器的运用 :程序计数器运用在打印异常的时候,可以具体看到哪一块地方报错。 也运用到多线程环境(主要运用)下,记录各线程运行的行号方便下次运行

注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。

虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置
-Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

扩展:java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。(也就是分配对象也可能在栈中分配)

即时编译器:可以把Java的字节码,包括需要被解释的指令的程序转换成可以直接发送给处理器的指令的程序

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。

栈: (线程私有)
  多个栈帧
    每个栈帧
         1. 本地变量表(参数,局部变量)
         2. 操作数栈(参数值,局部变量值)
         3. 动态链接(调用方法的符号引用,进而转化为内存地址)
         4. 返回地址(返回值)(方法出口)
         5. 其他信息 
注:每个方法被执行的时候都会创建一个栈帧用于存储局部变量表, ,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。(栈先进后出)

拓展

本地变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress类型:指向一条字节码指令的地址

需要注意的是,本地变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。

虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常

  栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,他是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法的返回地址等信息。每一个方法从调用开始直至执行完成的过程,都对应的一个栈帧在虚拟机栈里入栈和出栈的过程。

  在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中,因此一个栈帧需要多大的内存,不会受到程序运行期变量数据的影响。

 一个线程的方法调用链可能会很长,很多方法会同时处于执行状态。对于执行引擎来说,在当前活动的线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧,与这个栈帧相关联的方法成为当前方法。

在这里插入图片描述

2.对象的创建过程

2.1对象的创建过程

对象的创建过程一般是从new指令(我说的是JVM的层面),JVM首先对符号引用进行解析,如果找不到对应的符号引用,那么这个类还没有被加载,因此JVM便会进行类加载过程(具体加载过程后详述)。符号引用解析完毕之后,JVM会为对象在堆中分配内存,HotSpot虚拟机实现的JAVA对象包括三个部分:对象头、实例字段和对齐填充字段,其中要注意的是,实例字段包括自身定义的和从父类继承下来的(即使父类的实例字段被子类覆盖或者被private修饰,都照样为其分配内存)。相信很多人在刚接触面向对象语言时,总把继承看成简单的“复制”,这其实是完全错误的。
JAVA中的继承仅仅是类之间的一种逻辑关系(具体如何保存记录这种逻辑关系,则设计到Class文件格式的知识,唯有创建对象时的实例字段,可以简单的看成“复制”。为对象分配完堆内存之后,JVM会将该内存(除了对象头区域)进行零值初始化,这也就解释了为什么JAVA的属性字段无需显示初始化就可以被使用,而方法的局部变量却必须要显示初始化后才可以访问。
最后,JVM会调用对象的构造函数,当然,调用顺序会一直上溯到Object类。至此,一个对象就被创建完毕。

在这里插入图片描述
在这里插入图片描述

2.2实例化对象过程中的注意事项以及方法

jvm为实例对象分配空间主要有两种方法

一:指针碰撞:这种方法是在java堆中,将已用内存和未用的内存分开成两部分,两部分内存之间放这一个指针作为分界点,当有新的实例对象需要分配内存空间时,指针向未用内存一侧移动相应大小的距离,将新的实例对象存储在该内存空间上。这种方式需要内存是规整的。

二:空闲列表:这种方法分配空间是随机,每次分配内存空间都是从空闲的内存中选取一块分配给实例对象。那么就需要一个列表来存放这些空闲的内存空间地址,每当有实例对象需要空间,就从这个列表中选取出一块内存分配给实例对象。这种情况下内存是不规则的。

两种方法的选择取决于内存的结构是否规整,而内存结构是否规整则取决于采用的垃圾回收器是否带有压缩整理功能。例如:Serial、ParNew等带有compact过程的收集器,就是带有压缩整理功能的CMS这种基于Mark—Sweep算法的收集器就是没有压缩整理功能的有些时候,创建对象操作很频繁,这样就有可能导致指针刚刚分配好,还没来得及创建对象,就被另一个线程抢先,先占用了指针,这时候就会产生问题,解决这种问题主要有两种办法: 一:对创建对象动作行为进行同步处理,这种同步处理实质是CAS配上失败重试的方式实现保证更新操作的原子性的。 二:把每一个创建对象的动作行为按照线程划分为不同的空间中进行,这种方式就是将创建对象行为放入到线程中,为每一个线程分配一小块内存空间(TLAB),每个线程要分配内存就在自己的TLAB上运行分配,只有当TLAB满了,需要重新分配TLAB时,才需要进行同步锁定。TLAB方式的开启需要通过-XX:+/-UseTLAB参数设定。

3.对象的内存布局

3.1 在HotSpot虚拟机中。对象在内存中存储的布局

1.对象头2.实例数据3.对齐填充

3.1.1对象头【markword】

在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】。

markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息,假设当前为32个字节,在对象未被锁定情况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。

不同状态下存放数据
在这里插入图片描述

这其中锁标识位需要特别关注下。锁标志位与是否为偏向锁对应到唯一的锁状态。

锁的状态分为四种无锁状态、偏向锁、轻量级锁和重量级锁

不同状态时对象头的区间含义,如图所示。
在这里插入图片描述
HotSpot底层通过markOop实现Mark Word,具体实现位于markOop.hpp文件。
markOop中提供了大量方法用于查看当前对象头的状态,以及更新对象头的数据,为synchronized锁的实现提供了基础。[比如说我们知道synchronized锁的是对象而不是代码,而锁的状态保存在对象头中,进而实现锁住对象]。

关于synchronized锁升级过程(对象头和锁之间的转换),网上大神总结
在这里插入图片描述

3.1.2 实例数据

存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。分配策略:相同宽度的字段总是放在一起,比如double和long

3.1.3 对齐填充

这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求。

由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。

3.2 对象的访问定位

java程序需要通过引用(ref)数据来操作堆上面的对象,那么如何通过引用定位、访问到对象的具体位置。

对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式1.句柄访问对象2.直接指针访问对象。(Sun HotSpot使用这种方式)

参考Java对象访问定位

3.2.1 句柄访问

简单来说就是java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。

优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。

在这里插入图片描述

3.2.2 直接指针

与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样。

优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值