第一章 Java内存区域

1.内存模型

1.程序计数器,线程私有区域,占用空间较小,主要记录当前线程所执行的字节码的行号指示器。由于java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的(意思就是在任何一个确定的时刻,一个处理器,都只会执行一个线程中的指令),因此在多线程切换后为了确保程序能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,并且相互隔离。
2.栈,一般说栈都是指虚拟机栈,线程私有区域,生命周期和线程相同,栈主要描述的是Java方法执行时的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法被调用到执行完毕也就对应的是栈帧从入栈到出栈的过程。main方法一般在栈底。局部变量表存放了编译期可知的各种JVAVA虚拟机八大基本数据类型,对象引用等信息,局部变量表中的存储空间以局部变量槽来表示,64位的long和double占用两个插槽,而其余的数据类型只占用一个,局部变量表所需的内存空间在编译期便确定好并完成分配,当进入一个方法后,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,并且在运行期间是不会改变的,这里的大小是指插槽的数量,虚拟机真正使用多大的内存空间(1个插槽算32bit还是64bit这种)由虚拟机自己决定。
3.本地方法栈,线程私有区域,作用和栈相似,不过其功能是为调用本地方法服务
4.堆,线程共享区域。几乎所有对象实例以及数组都在堆上进行内存分配,GC活动的主要区域,几乎所有的对象都在堆内分配内存空间,简单描述一下堆堆划分,一般来说默认情况下,堆内存三分之一是新生代,三分之二是老年代,划分的目的在于方便GC管理,为什么这么划分后面章节细说。最早时期,堆采用标记-清除的算法进行GC活动,将一些不可达对象进行标记,然后进行清除,这种方法存在很大的缺陷,一个是容易内存不连续,容易碎片化,导致一些大对象的内存分配容易引发多次GC活动,所以在后面进行了一次优化,也就是所谓的标记-复制,这种算法将堆内存一分为二,姑且称之为A和B区域,首先在A中为新生对象进行空间分配,当A空间不足需要GC时,将A空间所有未被标记的对象复制到B空间中,然后彻底清除A空间内存,然后在B空间进行内存分配。这种方式显著提升了效率,但是弊端也很明显,严重浪费空间,一半的堆内存未被使用,在长期实验结果后IBM公司的专项研究中表明,新生代中的对象有98%都熬不过第一轮GC,因此并不需要按照1:1的比例进行划分空间。于是发展出Appel回收:将新生代划分为一块大的Eden区,两块小的Suvivor区,每次为对象分配内存只使用Eden和一块Suvivor区,他们的比例是8(Eden):1(Suvivor#From):1(Suvivor#To),每次GC时,将Eden和用过的一块Suvivor中仍然存活的对象复制到未使用的那块Suvivor中,然后直接清理掉Eden和使用过的Suvivor。也就是说,新生代中用于给新对象的空间占到了90%,只有10%的区域的新生代会被浪费。新生代采用的是标记-复制算法,前文已赘述便不在细说,整个新生代只占用堆内存的三分之一的区域,老年代则占用三分之二的区域,Eden+一块Suvivor区域被GC后,存活对象将进入到未使用的Suvivor区,每个对象都有个计数器,记载了经历过几次GC,当大于15时,这些对象便进入了老年代,老年代的对象几乎不会被GC所清除。但不是所有的对象都会在Eden中分配内存,有些大对象会直接进入到老年代中。这也是后续将提及的“分配担保”针对老年代,不需要频繁的GC,但还是很有必要进行GC活动,标记-复制已经不适合,诞生了新的算法,标记-整理法。
5.方法区(元空间+运行时常量池),线程共享区域,存储已被虚拟机加载的类型信息,常量,静态变量,编译后的代码缓存等数据,在JDK1.8中,方法区已经没有所谓的永久代的说法了,采用的是本地内存来实现元空间,永久代的概念主要是方便GC可以管理堆一样来管理方法区,但是效果很难令人满意,因此JDK1.8之后废弃了永久代的概念。这里得说一下运行时常量池,Class文件中除了有类的版本、字段、方法 、接口等描述信息外,还有一项叫做常量池表,用于存放编译期生成的各种字面量和符号引用,这部分内容在累加载后存放到方法区的运行时常量池中,运行时常量池另外一个重要特征就是具备动态性,具体表现在java语言并不要求常量必须在编译期才能产生,在运行期间也可以将新的常量放入池中,比如String的intern()方法

2.对象创建

当java虚拟机在遇到new这个指令时,会先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个类是否已经被加载、解析、初始化,如果没有则必须执行类加载的相应过程,类加载检查通过后,虚拟机将会为新生对象分配内存,对象所需要点内存其实在类加载完毕后就已经确定,对象分配内存本质就是在堆中划分一块内存区域而已。但是这个划分将收到GC影响。
如果GC是由CMS这种基于Sweep(清除)算法的收集器,那么堆的内存是不连续的,那么虚拟机就必须维护一个列表,记录哪些内存区域是可用的,然后找到一块合适大小的内存区域分配给新生对象,并且更新这个列表,这中方式被称为FreeList
如果GC是由Serial、ParNew这种收集器时,堆内存将是连续的,它划分内存的本质是移动指针,将堆内存分为空闲区域和 非空闲区域,创建对象时,仅需要挪动指针即可完成内存的分配,但是指针会存在一个问题,多线程的情况下,这个指针必须要保证线程安全,于是有两种方案可以解决这个问题

  1. CAS+失败重试机制,后续章节讨论
  2. 预先给每个线程在堆中划分一小块内存,被称之为本地线程分配缓冲,TLAB,哪个线程需要分配内存,直接使用本地缓冲即可,如果用完了,在分配新的缓冲区域时进行同步锁定即可。

2.1 对象内存布局

1.对象头,对象头有两部分构成(分情况):1.存放了对象的运行时数据,包括GC分代年龄,hashcode,锁状态表示等信息,2.对象类型数据指针,值得注意的是这个指针不一定必须存在(由不同的虚拟机决定),但是一定会有一个指针指向对象类型数据的,只是说这个指针不一定存放在对象头中(这里解释一下,刚开始可能有点懵逼,对象内存由三部分构成没错,换个角度看的话,我们所说的对象实际上是由对象实例数据和对象类型数据两部分组成,对象头其实是与对象自身无关的额外成本信息),如果通过句柄访问,那么对象头里不会保存有对象类型指针,因为句柄访问对象(对象实例数据和对象类型数据)并不通过对象本身,堆中会生成一个句柄池存放对象实例数据指针和对象类型数据指针,reference仅仅存放某个具体对象的句柄池地址。访问对象经的过程就是reference------(句柄池地址)----->句柄池------(对象数据类型指针/对象实例数据指针)----->对象类型数据/对象实例数据。如果经过指针访问,那么对象头中会存放对象类型数据指针,访问对象reference------(对象地址)----->对象------(对象头中的对象类型数据/实例数据)----->对象类型数据/对象实例数据,详细看下图
2.实例数据,没什么好说的,对象真正存储的本身信息,比如各种类型的字段内容,无论是父类继承下来的还是子类定义的都会进行记录。
3.对齐填充,无实际意义,仅仅是占位符,虚拟机要求对象起始地址必须是8字节的整数倍,所以任意对象的大小都是8字节的整数倍,如果不满8字节,便进行填充对齐。

2.2对象分访问定位

在这里插入图片描述

  • 句柄访问的优势在于reference中只存放句柄地址,在GC频繁的时候,对象会被移动,这时后只会改变句柄池中实例数据对象指针即可,reference本身不需要改变,即可重新定位到对象实例数据。(一般说的对象都是指对象实例数据)
  • 指针的优势在于速度快,节省了一次指针定位的开销,如果存在频繁访问对象的场景,那么这将是一项极为可观的成本节省,单论HotSpot来说,主要采用指针访问。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值