Java笔记(一):内存与对象分配

       前言:最近翻出来一本以前买的书《深入理解Java虚拟机》,已经不记得具体是什么时候买的了,但是可以肯定的是之前没有看。正好最近不是很忙,就读一读这本书吧,总得对得起自己花的银子吧。而如今这本书已经读了三分之一了,感觉很好,有必要记录一下。所以我准备写几篇博客,用来当做学习笔记。


       废话不多说,直接开整!要说Java就必须要从Java虚拟机开始,而内存管理是Java虚拟机所有工作内容中最为重要的,所以我们就从内存开始说起。


一、内存区域划分

       Java虚拟机在执行Java程序的时候会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁时间。这些区域分别是:方法区、虚拟机栈、本地方法栈、Java堆、程序计数器。


       程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码执行,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个程序计数器来完成。它是线程私有的,随线程的启动和结束而建立和销毁,同时它是所有内存区域中唯一一个不会有内存溢出的区域。


       Java虚拟机栈是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存放了编译期可知的8种基本数据类型以及对象引用和returnAddress类型。局部变量表所需的内存空间在编译期间完成分配,也就是说当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,方法在运行期间不会改变局部变量表的大小。此外,这个区域也是线程私有的。如果线程的请求深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。


       本地方法栈和虚拟机栈发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。


       Java堆是Java虚拟机所管理的内存中最大的一块儿,它是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域唯一的目的就是存放 对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作GC堆。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。


       方法区和Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器变异后的代码等数据。这块区域也经常被人称作“永久代”。


二、对象的创建
       当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经加载、解析和初始化或。如果没有则必须先执行类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成之后便可完全确定,为对象分配空间的任务等同于把一块确定的内存从Java堆中划分出来。

       分配内存的方式主要有两种,指针碰撞和空闲列表。

       指针碰撞(Bump the Pointer):假如Java堆中的内存是绝对规整的,所有勇敢过的内存放在一边,空闲的内存存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲那边挪动一段与对象大小相等的一段距离。

       空闲列表(Free List):如果Java堆的内存不是规整的,虚拟机就必须维护一个列表,记录上哪些内存是可用的,在分配的时候在列表中找到一块足够大的空间划分给对象实例,并更新表上的记录。

       选用哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

       不管选用哪种方式分配,都将面临线程安全问题。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB是,才需要同步锁定。虚拟机是否使用TLAB可以通过参数配置。

       内存分配完成之后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。接下来虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄,这些信息存放在对象头之中。上面的工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序员的角度看还需要执行init方法。


三、对象的内存布局
       在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域,对象头、实例数据和对齐填充。

      对象头(Header):HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit,官方称它为Mark Word。其实运行时数据很多,所以Mark Word是非固定的数据结构,他会根据对象的状态复用自己的存储空间。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,那在对象头中还必须有一块用于记录数组长度的数据。

       实例数据(Instance Data):实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。父类和子类定义的都要记录,这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义的顺序影响。从策略中可以看出,相同宽度的字段总是被分配到一起,在满足这个的前提下,在父类中定义的变量会出现在子类前面。

       对齐填充(Padding):对象的大小必须是8字节的正数倍。


以上是我的理解,如果疏漏请各位指正!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值