【0】README
0.1)本文转自 深入理解jvm, 旨在学习 java内存区域 的基础知识;
【1】运行时数据区域
1)jvm 所管理的内存将会包括以下几个运行时数据区域
1.1)方法区;(线程共享)
1.2)虚拟机栈;(线程私有)
1.3)本地方法栈;(线程私有)
1.4)java 堆;(线程共享)
1.5)程序计数器;(线程私有)
Attention) 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError(内存溢出) 异常的可能;
0.1)本文转自 深入理解jvm, 旨在学习 java内存区域 的基础知识;
【1】运行时数据区域
1)jvm 所管理的内存将会包括以下几个运行时数据区域
1.1)方法区;(线程共享)
1.2)虚拟机栈;(线程私有)
1.3)本地方法栈;(线程私有)
1.4)java 堆;(线程共享)
1.5)程序计数器;(线程私有)
Attention) 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError(内存溢出) 异常的可能;
【1.1】程序计数器(线程私有)
1)程序计数器: 是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器;(干货——程序计数器==行号指示器)
2)其作用: 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存;
3)此内存区域:是唯一一个在 jvm 规范中 没有规定任何 OutOfMemoryError 情况的区域;
【1.2】java 虚拟机栈(线程私有,用于存储局部变量表,操作数栈,动态链接,方法出口等信息)
1)虚拟机栈的作用: 描述的是 java 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息;
2)局部变量表: 存放了编译器可知的基本数据类型,对象引用 和 return address 类型(指向了一跳字节码指令的地址);
3)局部变量空间(Slot): 其中64 位长度的long 和 double 类型数据都会占用两个局部变量空间(slot),其余的数据类型占用1个;(干货——引入slot==局部变量空间)
4)jvm 在该区域规定了两种异常:
Exception1)如果线程请求的栈深度大于jvm 所允许的深度,将抛出 StackOverFlowError 异常;
Exception2)如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常;
【1.3】本地方法栈(线程私有)
1)本地方法栈的作用于 虚拟机栈的作用相似;
2)他们作用的区别在于: 虚拟机栈为虚拟机执行 java(字节码)服务,而本地方法栈为 虚拟机使用到的 Native 方法(本地方法)服务;
【1.4】java 堆(存放对象实例,线程共有)
1)java 堆的唯一作用:存放对象实例,几乎所有的对象实例都在这里分配内存;
2)GC堆(垃圾收集堆): java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为 GC堆(Garbage Collected Heap);
3)从内存回收的角度看:由于现在收集器基本都采用分代收集算法,所以 java 堆还可以分为: 新生代 和 老年代;(干货——在java堆中引入新生代和老年代)
4)从内存分配的角度来看:线程共享的 java 堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB);
5)java堆 可以处理物理上不连续的内存空间中, 只要逻辑上是连续的即可;
6) java 堆是可扩展的,不过当前主流的jvm 都是按照可扩展来实现的(通过 Xmx 和 -Xms 控制), 如果在堆中没有内存完成实例分配,并且堆也无法再扩展,将会抛出 OutOfMemoryError 异常;
【1.5】方法区(用于存放 class 的相关信息,线程共有)
1)方法区的作用:用于存储被虚拟机加载的类信息,常量,静态变量,即时编译器(JIT)编译后的代码等数据;
2)方法区除了和 java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集;
3) 方法区的内存回收目标: 主要是针对常量池的回收和对类型的卸载;
【1.6】运行时常量池
1)运行时常量池是方法区的一部分;
2)Class 文件中除了有类的版本,字段,方法,接口等描述信息外,还有一个信息——常量池;(干货——引入常量池)
3)常量池的作用:用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放;
4)运行时常量池的一个重要特征:具备动态性;(干货——运行时常量池的具备动态性)
看个荔枝): 并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中, 这种特性被开发人员利用的多的便是 String类的 intern 方法;
【1.7】直接内存
0)直接内存: 分配在本机直接内存上,不是 jvm 运行时数据区的一部分,但被频繁地使用;
1)jdk1.4 中新加入了NIO(new input/output)类: 引入了一种基于通道与缓冲区(buffer)的IO方式,他可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作,这样能在一些场景中提高性能,因为避免了在 java 堆 和 Native 堆中来回复制数据;
2)显然,本机直接内存的分配不会受到 java 堆大小的限制;
【2】HotSpot 虚拟机对象探秘
1)对象的创建steps:
step1)是否执行类加载过程:虚拟机遇到一条new指令,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那必须执行相应的类加载过程;
step2)分配内存: 接下来 虚拟机将为新生对象分配内存。(内存分配方式有: 指针碰撞 和 空闲列表)
problem):对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针指向的位置, 在并发情况下也并不是线程安全的。如,可能出现正在给对象A分配内存,指针还未来得及修改,对象B 又同时使用了原来指针来分配内存的情况;
solution1):对分配内存空间的动作进行同步处理——实际上采用 CAS 配上失败重试的方式保证更新操作的原子性;
solution2):吧内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有TLAB用完并分配新的TLAB时,才需要同步锁定;(干货——每个线程在 java堆中预先分配一小块内存,称为本地线程分配缓冲)
step3)初始化为零: 内存分配完成后,虚拟机需要将分配到 的内存空间都初始化为零值;
step4)虚拟机对对象进行必要的设置: 如这个对象的类是什么,如果才能找到类的元数据信息,对象的哈希码,对象的GC 分代年龄等信息。这些信息存放在对象的对象头中;(干货——引入了对象头)
step5)执行 <init>方法: 执行完new 执行后,会执行 init 方法,把对象按照程序员的意愿进行初始化;
【2.1】对象的内存布局
1)对象的内存布局分为3个区域: 对象头, 实例数据 和 对齐填充;
2)HotSpot 虚拟机的对象头包括两部分数据:
2.1)对象头第一部分(Mark Word):用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,这部分数据的长度在 32位 和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64 bit, 官方称为 “Mark Word”;(干货——引入对象头的Mark Word);对象头信息是与对象自身定义 的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成 非固定的数据结构以便在 极小的空间内存储尽量多的信息;(干货——对象头信息是与对象自身定义 的数据无关的额外存储成本)
Complementary)HotSpot 虚拟机对象头 Mark Word:
1)程序计数器: 是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器;(干货——程序计数器==行号指示器)
2)其作用: 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存;
3)此内存区域:是唯一一个在 jvm 规范中 没有规定任何 OutOfMemoryError 情况的区域;
【1.2】java 虚拟机栈(线程私有,用于存储局部变量表,操作数栈,动态链接,方法出口等信息)
1)虚拟机栈的作用: 描述的是 java 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息;
2)局部变量表: 存放了编译器可知的基本数据类型,对象引用 和 return address 类型(指向了一跳字节码指令的地址);
3)局部变量空间(Slot): 其中64 位长度的long 和 double 类型数据都会占用两个局部变量空间(slot),其余的数据类型占用1个;(干货——引入slot==局部变量空间)
4)jvm 在该区域规定了两种异常:
Exception1)如果线程请求的栈深度大于jvm 所允许的深度,将抛出 StackOverFlowError 异常;
Exception2)如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常;
【1.3】本地方法栈(线程私有)
1)本地方法栈的作用于 虚拟机栈的作用相似;
2)他们作用的区别在于: 虚拟机栈为虚拟机执行 java(字节码)服务,而本地方法栈为 虚拟机使用到的 Native 方法(本地方法)服务;
【1.4】java 堆(存放对象实例,线程共有)
1)java 堆的唯一作用:存放对象实例,几乎所有的对象实例都在这里分配内存;
2)GC堆(垃圾收集堆): java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为 GC堆(Garbage Collected Heap);
3)从内存回收的角度看:由于现在收集器基本都采用分代收集算法,所以 java 堆还可以分为: 新生代 和 老年代;(干货——在java堆中引入新生代和老年代)
4)从内存分配的角度来看:线程共享的 java 堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB);
5)java堆 可以处理物理上不连续的内存空间中, 只要逻辑上是连续的即可;
6) java 堆是可扩展的,不过当前主流的jvm 都是按照可扩展来实现的(通过 Xmx 和 -Xms 控制), 如果在堆中没有内存完成实例分配,并且堆也无法再扩展,将会抛出 OutOfMemoryError 异常;
【1.5】方法区(用于存放 class 的相关信息,线程共有)
1)方法区的作用:用于存储被虚拟机加载的类信息,常量,静态变量,即时编译器(JIT)编译后的代码等数据;
2)方法区除了和 java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集;
3) 方法区的内存回收目标: 主要是针对常量池的回收和对类型的卸载;
【1.6】运行时常量池
1)运行时常量池是方法区的一部分;
2)Class 文件中除了有类的版本,字段,方法,接口等描述信息外,还有一个信息——常量池;(干货——引入常量池)
3)常量池的作用:用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放;
4)运行时常量池的一个重要特征:具备动态性;(干货——运行时常量池的具备动态性)
看个荔枝): 并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中, 这种特性被开发人员利用的多的便是 String类的 intern 方法;
【1.7】直接内存
0)直接内存: 分配在本机直接内存上,不是 jvm 运行时数据区的一部分,但被频繁地使用;
1)jdk1.4 中新加入了NIO(new input/output)类: 引入了一种基于通道与缓冲区(buffer)的IO方式,他可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作,这样能在一些场景中提高性能,因为避免了在 java 堆 和 Native 堆中来回复制数据;
2)显然,本机直接内存的分配不会受到 java 堆大小的限制;
【2】HotSpot 虚拟机对象探秘
1)对象的创建steps:
step1)是否执行类加载过程:虚拟机遇到一条new指令,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那必须执行相应的类加载过程;
step2)分配内存: 接下来 虚拟机将为新生对象分配内存。(内存分配方式有: 指针碰撞 和 空闲列表)
problem):对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针指向的位置, 在并发情况下也并不是线程安全的。如,可能出现正在给对象A分配内存,指针还未来得及修改,对象B 又同时使用了原来指针来分配内存的情况;
solution1):对分配内存空间的动作进行同步处理——实际上采用 CAS 配上失败重试的方式保证更新操作的原子性;
solution2):吧内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有TLAB用完并分配新的TLAB时,才需要同步锁定;(干货——每个线程在 java堆中预先分配一小块内存,称为本地线程分配缓冲)
step3)初始化为零: 内存分配完成后,虚拟机需要将分配到 的内存空间都初始化为零值;
step4)虚拟机对对象进行必要的设置: 如这个对象的类是什么,如果才能找到类的元数据信息,对象的哈希码,对象的GC 分代年龄等信息。这些信息存放在对象的对象头中;(干货——引入了对象头)
step5)执行 <init>方法: 执行完new 执行后,会执行 init 方法,把对象按照程序员的意愿进行初始化;
【2.1】对象的内存布局
1)对象的内存布局分为3个区域: 对象头, 实例数据 和 对齐填充;
2)HotSpot 虚拟机的对象头包括两部分数据:
2.1)对象头第一部分(Mark Word):用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,这部分数据的长度在 32位 和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64 bit, 官方称为 “Mark Word”;(干货——引入对象头的Mark Word);对象头信息是与对象自身定义 的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成 非固定的数据结构以便在 极小的空间内存储尽量多的信息;(干货——对象头信息是与对象自身定义 的数据无关的额外存储成本)
Complementary)HotSpot 虚拟机对象头 Mark Word:
2.2)对象头第二部分(类型指针):类型指针, 即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
3)对象的实例数据区域:是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容;
4)对象的对齐填充区域: 没有什么特别的含义,它仅仅起着占位符的作用, 换句话说,就是对象的大小必须是 8字节 的整数倍;
【2.3】对象的访问定位
1)java 程序需要通过 栈上的 引用数据来操作堆上的具体对象。
2)引用应该通过何种方式去定位,访问堆中的对象的具体位置,目前主流的访问方式有 使用句柄 和 直接指针两种;(干货——栈上引用访问堆上对象的主流访问方式)
2.1)使用句柄访问方式: 那么java 堆中将会划分出一块内存来作为句柄池,引用数据中存储的就是对象的句柄地址,而句柄地址包含了对象实例数据和类型数据各自的具体地址;
3)对象的实例数据区域:是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容;
4)对象的对齐填充区域: 没有什么特别的含义,它仅仅起着占位符的作用, 换句话说,就是对象的大小必须是 8字节 的整数倍;
【2.3】对象的访问定位
1)java 程序需要通过 栈上的 引用数据来操作堆上的具体对象。
2)引用应该通过何种方式去定位,访问堆中的对象的具体位置,目前主流的访问方式有 使用句柄 和 直接指针两种;(干货——栈上引用访问堆上对象的主流访问方式)
2.1)使用句柄访问方式: 那么java 堆中将会划分出一块内存来作为句柄池,引用数据中存储的就是对象的句柄地址,而句柄地址包含了对象实例数据和类型数据各自的具体地址;
2.2)使用直接指针访问(Sun HotSpot 使用这种方式访问)