1.内存区域

1.概述

  • 优点:对于Java开发程序员来说,虚拟的自动内存管理机制的帮助下,我们不在关心每一个对象创建的时候的内存的分配,也不需要关心每个对象的销毁和释放内存资源,不容易出现内存溢出和内存泄漏的问题,由虚拟机来帮我们管理内存!
  • 缺点:也正是由于我们的内存是虚拟机来帮我们分配和管理,一旦出现了内存泄漏和内存溢出的问题,如果不了解虚拟机是怎样使用内存的,那么排查问题将会是一项异常艰难的工作!

2.运行时数据区

        Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁!

2.1.程序计数器
  • 概述:是一块较小的空间,可以看做是当前线程所执行的字节码行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取吓一条需要执行的字节码指令(存储的就是当前线程执行的哪一行代码的位置,也就是行号),分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 作用:为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,同时也称为(线程私有内存)的内存
  •  注意:如果正在执行的是native方法,这个计数器的值则为空(undefined)
2.2.虚拟机栈
  • 概念:与程序计数器一样,Java中虚拟机栈也是线程私有的,他的生命周期与线程相同
  • 作用:虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等信息,每个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
2.2.1.局部变量表
  • 存储的是当前方法(栈帧)的局部变量,是一个数组结构(表格Table)
2.2.2.操作数栈
  • 表示程序在运行中涉及到运算的地方就存放在这个中转区域,而一但运算完毕就会存入局部变量表
2.2.3.动态链接

2.2.4.方法出口
  • 表示上一个栈帧的地址,也就是表示当我这个方法执行完成之后,要返回到上一个方法的调用位置
2.3本地方法栈
  • 概念:本地方法栈与虚拟机所发挥的作用是一样的,区别是虚拟机栈是虚拟机在执行Java方法的时候创建的服务,而本地栈是Java底层在调用native修饰的方法的时候创建,因为Java底层是其他语言写的,而其他语言的执行所存放的内存就是在这个内存中
2.4.Java堆
  • 概念:堆空间是我们虚拟机所管理的最打的一快区域,堆空间被所有线程共享的一块内存区域,在虚拟机启动时创建。同时也是GC主要管理的一块区域,堆空间大致分为3部分,新生代,老年代,元空间
  • 作用:存放对象实列,Java世界里面的所有对象实例都存放在堆空间中
2.5.方法区
  • 概念:和堆空间一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
2.5.1.常量池
  • 运行时常量池是方法区中的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用
2.7.直接内存
  • 概念:直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致内存溢出异常出现
  • 作用:在JDK1.4之后,新加入了一NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
2.8.内存区域图

3.虚拟机对象

3.1.对象的创建
  • Java是一门面向对象的编程语言,Java程序运行过程中每时每刻都有对象被创建出来,在语言层面上,创建对象通常(复制,反序列化)仅仅是一个new关键字而已。当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有那就必须要进行相应的类加载过程,

3.2.对象的分配
3.2.1.概念

        对象所需的内存大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。

3.2.2.方式
  1. 指针碰撞:如果Java堆中的内存是绝对规整的所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间防线挪动一段与对象大小相等的距离
  2. 空闲列表:如果Java堆中的内存不是规整的,已被使用的内存和空闲相互交错在一起,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
3.2.3.具体使用

        具体使用哪种方式由Java堆是否规整决定的,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理的能力决定。因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内村 

注意:CMS的实现里面,为了能在多数情况下分配得更快,设计了一个叫做(LinearAllocation Buffer)分配缓冲区,通过空闲列表拿到一大块分配缓冲区之后,在它里面仍然可以使用指针碰撞得方式来分配

3.2.4.线程问题

        对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置;在并发情况下也并不是线程安全的,

例如:在给对象A分配内存时,指针还没来得及移动,对象B又同时使用了原来的指针来分配内存的情况。两种方案解决

  1. 一种是堆分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
  2. 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了(或者不够),分配新的缓冲区时在需要同步锁定,可以通过虚拟机参数(-XX:+/-UseTLAB)来设置。
3.2.5.原理

        内存分配完成后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行,这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值,

        接下来Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息都存放在对象头(Header)里面

        在上面工作都完成后,从虚拟机的视角来看,对象其实就已经创建完成,但是从程序员的视角来看,对象的创建才刚刚开始——构造函数,即Class文件中的init()方法还没有执行,所有的字段都默认为零值,通过最后的初始化,这样一个真正的对象才算完全被构造出来

3.3.对象的内存布局

        在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分,对象头、实例数据、对齐填充

3.3.1.对象头

        对象头包括以下两个部分

  1. 运行时数据:用户存储对象自身的运行时数据,如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为 “Mark Word” 
  2. 类型指针:即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身。
3.3.2.实例数据

        实例数据是我们在内存中真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

3.3.3.对齐填充

        对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部已经被精心设计成正好是8字节的倍数,因此,如果对象实例数据部分没有对齐填充的话,就需要通过对齐填充来不全。

3.4.对象的访问

        创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种

  1. 句柄池:Java堆中将可能划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
  2. 直接指针:Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果知识访问对象本身的话,就不需要多一次间接访问的开销。
3.4.1.区别
  1. 句柄池:使用句柄来访问的最大好处就是reference中存储的是稳定的句柄池,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
  2. 直接指针:使用直接指针的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本

对于HotSpot虚拟机而言,它主要使用了直接指针的方式进行对象的访问(如果使用了Shenandoah收集器的话也会有一次额外的转发),但是从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也是十分常见。

3.4.2.图形

        

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值