《深入理解java虚拟机》读书笔记(一)java内存模型和对象探秘

《深入理解java虚拟机》读书笔记(一)java内存模型和对象探秘

在看书学习的过程中,我把书上的知识做了一下笔记,并且根据自己的理解总结一些知识点,方便以后复习,也希望能给一起学习的有需要的朋友们一些帮助。如果有写得不对的地方,欢迎大家指正。

1、概述

在java中,我们很少关注内存泄漏和内存溢出的问题,java虚拟机帮助我们自动进行内存管理。但是如果一旦出现问题,如果不了解虚拟机是怎么工作的,那么修正问题将会变得非常困难。

2、运行时数据区

java虚拟机在执行java程序的过程中把它所管理的内存分为若干个不同的数据区:

《深入理解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字节的整数倍,就需要占位符进行填充。