深入理解jvm(一)jvm内存区域

前言:最近打算深入学习一下jvm相关的知识,对于jvm之前一直是零零散散了解的程度,看过一些快餐视频背过一些八股文便止步于此了,一直想系统的学习一下,但都没有落实下来,现在刚好公司有要求新入职学习的任务,趁着这个任务,每天下班后那点时间出来学习,在这里记录和分享一下学习笔记,这次的学习以书本为主,所用书籍为《深入理解java虚拟机》第三版,书籍是导师送的,感谢!


正题:

看了大概两三周,这次学习完了书本的前两章,第一章主要讲了整个的java体系,java语言和各种虚拟机的发展历程,也大致讲了下java各个版本的区别、特点,学习这里快速过了一下,可以简单了解,看完再和其他程序员吹牛时也多一份谈资吧。

第二章主要讲了虚拟机中内存区域中各个模块的功能作用,同时也讲了一些内存溢出时的异常情况,这里觉得先记录一下概念性的一些东西即可,在后面的章节学习了类加载器和gc等,再结合内存模型输出流程图。

2.2.1程序计数器

程序计数器 线程私有 是当前线程所执行的字节码的行号指示器,它会指出下一条将要执行的指令的地址,字节码解释器就是通过改变计数器的值来选取程序接下来执行的操作,以使线程切换后恢复到正确的执行位置。如果线程正在执行java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行native方法,则计数器为空。程序计数器是唯一一个不会出现OOM的内存区域。

2.2.2java虚拟机栈

线程私有、与线程生命周期相同,每个方法被执行的时候会在栈里创建一个栈帧,用于存放局部变量表,操作数栈动态链接方法出口等信息,每一个方法从调用直至执行完成,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

一般所谓的“栈”,指的是虚拟机栈中局部变量表部分,其中存放了各种基本数据类型(8种),对象引用(refrence类型) 和 returnAddress 类型(returnAddress 类型的值就是指向特定指令内存地址的指针)。局部变量表所需的空间在编译期就已经确定并完成分配,在方法运行期间不会被改变。当然java虚拟机还有本地方法栈用来执行本地方法。

java虚拟栈中可能出现两种异常:

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度

OutOfMemoryError:虚拟机栈扩展时无法申请到足够的内存

2.2.3 java堆

Java堆是是虚拟机中最主要的内存区域。它为线程共享,在虚拟机启动时创建,几乎所有的对象实例都存储在 Java堆中(这里强调是几乎)。Java堆也被称作 "GC" 堆,可分为新生代和老年代。而新生代又可分为Ende区、from Survivor区、To Survivor区等。Java堆可以通过 -Xmx-Xms 控制堆大小。如果堆中内存满时,会OOM异常。

2.2.4 方法区

方法区与 Java堆一样,为线程共享。用于存储已被虚拟机加载类信息常量静态变量即时编译器编译后的代码等数据。也叫作Non-heap(非堆)。方法区、永久代、元空间,jdk1.7以前,方法区实现在永久代上,1.8后方法区用元空间实现,可以理解为方法区为接口,元空间和永久代为不同实现,元空间不在jvm设置的内存中,位于本地内存。

2.2.5 运行时常量池

运行时常量池是方法区的一部分。Class文件中的常量池表中,用于编译期生成的各种字面量符号引用(类A import了某个类B,在加载类A的时候,虚拟机不知道类B的具体地址,这个时候就会编译成符号引用),这部分内容在类加载后被存入运行时常量池。动态性是运行时常量池相对于 Class文件常量池的一个重要特征,即不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入池中,如:String类的intern()方法。

2.2.6直接内存

是一个数据缓冲区,属于操作系统内存,不由jvm管理,也会OOM,常用于NIO(ByteBuffer分配的就是直接内存),分配回收成本较高,但读写性能高

正常文件读写过程:磁盘文件 -> 系统缓冲区 -> java缓冲区(new Byte[])

直接内存:在操作系统里划分出一块直接内存,java代码和系统都可以直接访问它

直接内存的分配与释放是通过一个Unsafe类型对象(释放通过调用freeMemory),而不是GC


2.3.1 对象的创建

jvm遇到一条new指令时,首先会检查是否被类加载器加载,如果没有,则执行相应的类加载过程,接下来是为新对象分配内存,分配内存有两种方式,一种是指针碰撞,一种是空闲列表

jvm创建对象非常频繁,所以并发情况下给新对象分配内存会可能会线程不安全,这个时候可以采用CAS加锁的形式保证分配内存动作的原子性,也可以采用TLAB(本地线程分配缓冲),在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,前者时间换空间,后者空间换时间。

接下来就是成员变量初始化零值,比如int类型赋值0,再接下来会设置对象头的相关信息,再接下来会执行<init>()方法,对对象进行初始化。

2.3.2 对象的内存布局

对象在堆内存中的布局分为三块区域:对象头、实例数据和填充数据。

对象头:

Java对象的对象头由 mark wordclass pointer 两部分组成。mark word储存对象自身的运行时数据 ;class pointer储存对象的类型指针,该指针指向它的类元数据

mark word部分如下图(该处涉及到偏向锁、轻量级锁、重量级锁,该处后面章节学习,这里只知道mark word储存的是对象运行时的数据即可):

class pointer可以指向对象的类型元数据,即指向方法区里该对象的类。

如果是java数组对象,对象头中还会记录数组长度数据;

 实例数据部分:

存字段,正真存储对象有效信息的部分,代码中定义或父类继承的字段存储于此,jvm会按照顺序(空间)进行储存,相同宽度的字段被分配在一起,默认父类变量出现在子类之前;

对齐填充:

没有特殊含义,主要是使对象长度为8字节倍数。

2.3.3 对象的访问定位

对于对象的访问方式有两种实现:一种是使用句柄,另一种是使用直接指针

使用句柄方式,堆中会拿出一块区域作为句柄池,栈中reference类型储存的就是对象的句柄地址,句柄中包含了对象实例数据和类数据各自的地址信息;好处是,当堆中对象被移动(gc时)只用改变句柄中的实例数据指针,reference不用改变。

使用指针,栈中reference类型储存的就是对象堆内存地址,可以直接访问,不用有间接访问的开销,速度更快,HotSpot实现方式。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值