JVM特性之二:自动内存管理机制(上)

纸上得来终觉浅 绝知此事要躬行

前言:本文参考自 周志明先生的《深入理解Java虚拟机》作学习记录作用,想详细学习java虚拟机的朋友建议买一本书仔细研读。

JVM的自动内存管理机制的内容就是 内存自动分配内存自动回收两个部分。


在理解内存如何自动分配前,我们有必要了解JVM管理的内存的区域的结构。我们称之为 运行时数据区

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区
  5. 运行时常量区
  6. 直接内存

程序计数器:这是一块比较小的内存区域,可以理解成当前行号的计数,字节码解释器在工作的时候需要改变这个计数器的值选取下一跳。(线程私有)
虚拟机栈:这个区域描述的是方法执行的内存模型,每一个方法在执行的时候都会创建一个栈帧,栈帧中会存放着局部变量表、操作数栈、动态链接、方法出口等信息。一个方法的执行对应的就是一个栈帧从入栈到出栈的过程。
本地方法栈:本地方法栈的作业其实和虚拟机栈并无大的差别,虚拟机规范中对这个区域并没有做特别的规定,这块区域留给具体的虚拟机实现,例如HotSpot虚拟机直接把本地方法栈和虚拟机栈合并成一块。
:堆是JVM中管理的内存最大的一块区域。堆是一块线程共享的区域,它在虚拟机开启的时候创建。虚拟机规范中说明,所有对象实例以及数组都要在堆上分配。GC回收的目标是对象,所以堆也就成了垃圾收集器管理的主要区域。由于现在的回收算法都采用分代算法,所以堆中可以分成新生代和老年代。
方法区:用来存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。如何实现方法区不受虚拟机规范限制,有的虚拟机对用永久代来实现方法区。虚拟机规范对方法区限制很宽松,除了同堆一样不需要连续的内存空间和可以选择固定大小或者可扩展外,还可以选择不实现垃圾回收。
运行时常量区:是方法区的一部分,class文件中除了类的版本、字段、方法、接口等类型信息之外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区运行时常量池。
直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范定义中的内存数据区域。但是这部分被频繁的使用。

介绍完虚拟机的运行时数据区之后,我们知道了内存的大致情况,接下来我们需要用具体的虚拟机梳理相关细节,比如如何创建、如何访问等问题。以下以JVM中常用内存

一、对象的创建

当虚拟机遇到一个new指令时,第一步会检查这个指令的参数能否在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否已经被加载、解析和初始化过了。如果没有的话,那就必须先进行对应类的加载过程。(PS:类是怎么加载的呢?)
当这个类加载完成了之后,虚拟机开始为这个对象进行分配内存,需要知道的是对象需要多少内存,在类加载的过程中是确定下来的。所以虚拟机分配内存就是在大小确定的堆内存中划出一块给新生对象。
在内存是绝对规整的情况下,只需要将指针移动一块与对象所需内存大小相同的区域即可。这种情况叫做"指针碰撞"。另外一种情况就是内存不是规整的,这种情况无法简单的用指针碰撞,此时可以维护一个列表,这个表用来记录哪块内存是可以用的,在分配的时候会在空闲列表中寻找一块符合要求的区域进行分配。这种方法叫做"空闲列表"。

采用哪种分配方式与内存是否规则有关,内存规整与否与GC是否带有压缩整理功能挂钩。
举个栗子:
指针碰撞的虚拟机:Serial、ParNew
空闲列表的虚拟机:基于Mark-Sweep算法的收集器

除了划分空间之外,还有一个线程安全的问题需要考虑,由于创建对象的动作过于频繁,可能出现对象A还没来得及修改,对象B又用了原来的指针进行分配。为了解决这个问题有两个解决方案。

  • 对分配内存的动作进行同步处理(实际上虚拟机采用CAS+失败重试的方案来保证更新操作的原子性)
  • 把内存分配的动作按线程划分在不同的空间之中,即每个线程先分配一块内存。叫做本地线程分配缓冲

内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作的作用是保证了对象实例在java代码中可以不赋初始值就可以直接使用,程序能访问这些字段的零值。
初始化之后虚拟机还要对对象进行必要的设置,例如这个的对象时哪个类的实例,如何找到类的元数据信息、对象的哈希吗、对象GC分代年龄等信息。这些信息会存放在对象头中。到这里的话在虚拟机看来对象的创建已经完成了,但是java代码角度来看的话,创建才刚开始,一般来new指令之后会紧跟着 方法将对象按程序员的医院初始化。

二、对象内存分布

对象在内存中存储的布局可以分成三块区域:对象头、实例数据、对齐填充。

  • 对象头
    对象头包含两部分信息:一部分用于存储对象自身的运行时数据。如哈希码、GC分代年龄、锁状态标志等。在32位或者64位的虚拟机分别为32Bit和64Bit。其实对象运行时数据很多 32或者64bit根本不够,但是由于对象头与对象自身定义的内容无关,所以它的空间可以复用。
    例如:Mark Word 对象处于未锁定的状态,那么32bit中25bit用于存储对象哈希码、4bit用于存储对象分代年龄2bit用于存储标志位…
    在这里插入图片描述
    另一部分类型指针用来指向他的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。但是并不是所有在所有对象数据上都必须保存这个数据。另外,当对象是一个java数组的时候对象头中还要有一块用于记录数组长度的数据。

  • 实例数据
    实例数据是真正存储的有效信息,也是程序代码中所定义的各种类型字段,无论是父类继承还是子类定义的都需要记录起来。存储顺序受到虚拟机分配策略参数以及字段在源码中的顺序影响,分配策略中会将内存宽度相同的数据放在一起(long/double、short/char)

  • 对齐填充
    不是必然存在,也没有特别的含义仅仅是占位符的作用。

三、对象的访问定位

对象创建出来就是为了使用的。java程序中需要用reference数据来操作对上的具体对象。由于reference在JVM规范中只定义了一个指向对象的引用,没有定义具体方法。所以访问方法取决于虚拟机如何设计。

目前主要有两种方法:使用句柄以及直接指针。

  • 使用句柄
    采用句柄的方式,那么java堆中需要划分出一块内存来作为句柄池。reference中存储的就是对象的句柄地址,而句柄中存储着对象实例数据和类型数据的具体地址信息。
    在这里插入图片描述

  • 直接引用
    直接引用中,reference中存储的直接就是对象地址,所以JAVA堆对象的布局中必须考虑如何放置访问类型数据的相关信息。
    在这里插入图片描述

总结两种方式的优劣:
使用句柄的方式最大的好处就是reference中存储的是稳定的句柄地址,就算对象移动改变的也只是句柄中的实例数据指针。reference本身无需更改。
直接访问的优点就是快!

本文理到了内存的结构、对象创建时内存分配的方式、内存存储了什么以及访问对象的两种方法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值