《深入理解java虚拟机》读书笔记(一)java内存模型和对象探秘
《深入理解java虚拟机》读书笔记(一)java内存模型和对象探秘
在看书学习的过程中,我把书上的知识做了一下笔记,并且根据自己的理解总结一些知识点,方便以后复习,也希望能给一起学习的有需要的朋友们一些帮助。如果有写得不对的地方,欢迎大家指正。
1、概述
在java中,我们很少关注内存泄漏和内存溢出的问题,java虚拟机帮助我们自动进行内存管理。但是如果一旦出现问题,如果不了解虚拟机是怎么工作的,那么修正问题将会变得非常困难。
2、运行时数据区
java虚拟机在执行java程序的过程中把它所管理的内存分为若干个不同的数据区:
按照是否线程共享来进行分类:
线程共享:方法区、堆。
线程私有:程序计数器、虚拟机栈、本地方法栈。
下面来对它们进行逐一介绍
2.1、程序计数器
程序计数器是一块比较小的内存区域。
线程私有。
字节码解释器通过改变程序计数器的值来选择下一条字节码指令。
线程切换完成后需要恢复到正确的执行位置,所以每个线程都需要一个程序计数器。
如果线程运行的是一个java方法,程序计数器保存的是字节码指令的地址。
如果线程运行的是一个本地(native)方法,程序计数器为空。
2.2、java虚拟机栈
线程私有。
每个java方法被执行,虚拟机都会创建一个栈帧。
栈帧由局部变量表、操作数栈、动态连接、方法入口组成。
每个java方法从开始执行到结束的过程,对应着一个栈帧入栈和出栈的过程。
局部变量表存储基本数据类型、对象引用、returnAddress类型(指向一条字节码指定的地址)。
基本数据类型由局部变量槽存储。
除了long和double类型的数据占两个局部变量槽,其他基本数据类型的数据都只占一个。
局部变量槽的数量在进入方法时就确定了。
每个变量槽有多大由虚拟机决定(一个变量槽占32比特或64比特或者其他数量)。
2.3、本地方法栈
为本地方法(native方法)服务。
有的虚拟机将本地方法栈和虚拟机栈合而为一,比如Hotspot虚拟机。
2.4、堆
线程共享。
存放对象实例(包括数组)。
堆可以处于逻辑上连续,物理上不连续的内存空间。
可以固定大小,也可以选择可扩展,由虚拟机决定。不过主流虚拟机的堆大小都是可扩的。
2.5、方法区
线程共享。
存放加载的类型信息、常量池、静态变量、代码缓存。
内存回收目标:常量池和类型信息。
使用元空间(使用本地内存)实现方法区。
2.6、运行时常量池
属于方法区。
存放字面量、符号引用、直接引用。
3、HotSpot对象探秘
3.1、对象的创建
过程:
在遇到new关键字之后,会发生以下这些行为:
1、在常量池中查找对应对象的类型信息。
2、分配内存。
3、初始化零值。
4、初始化对象头信息。
5、执行构造函数。
下面进行详细说明。
第一步:看能否在常量池中找到这个对象对应的类(Class)的符号引用,并且检查类是否被加载、解析、初始化。
如果有一个条件不满足,就执行类加载过程。
第二步:将堆内存划分一块给对象使用。对象的大小在类加载完成后就确定了。
如果堆的内存是规整的,则采用"指针碰撞"方式分配内存。如果不规整则采用"空闲列表"的方式。
堆的内存是否规整由垃圾收集器是否有空间压缩整理能力决定。
指针碰撞:空闲的内存放一边,已经被使用的内存放一边,中间维护一个指针。给一个新对象分配内存,只需要把指针向已经使用的内存那边移动对应对象的大小即可。多线程情况下有同步问题。
空闲列表:列表维护着可用内存的信息,给对象分配内存之后,更新列表的信息。
第三步:给实例变量赋零值。
第四步:设置对象头信息,包括类型指针和自身的运行时数据(哈希码、GC分代年龄、线程持有的锁等)。
第五步:执行构造函数,按照我们的意愿赋值。
3.2、对象的内存布局
对象由对象头、实例数据、对齐填充三部分组成。
对象头:包括类型指针和对象自身的运行时数据。类型指针指向类的元数据,确定对象是哪个类的实例。自身的运行时数据包括哈希码、GC分代年龄、线程持有的锁等。如果是数组还要记录数组的长度。
实例数据:就是属性。包括父类和子类自身的属性。
对齐填充:不是必然存在的。就起一个占位符的作用。任何对象的大小都必须是8字节的整数倍,对象头是8字节的整数倍,所以如果实例数据部分不是8字节的整数倍,就需要占位符进行填充。