JVM学习笔记(一)内存区域,对象创建,对象内存布局

前置
1、 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
1) 符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中
2)在Java中,一个java类将会编译成一个class文件。 在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替
2、直接引用:直接引用可以是
1)直接指向目标的指针(比如,指向"类型"【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。
如果有了直接引用,那引用的目标必定已经被加载入内存中了


一、运行时数据区
1、程序计数器
线程私有 指向正在执行的字节码地址 、修改计数器的值来选取下一条执行指令、多线程切换后恢复通过计数器找到位置
1)程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。
2)虚拟机的字节码解释器工作时,就是通过修改程序计数器的值,来选取下一条需要执行的字节码指令;
3)分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖程序计数器来完成。
4)Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,在一个时刻一个处理器(多核处理器的一个内核)
都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,
各条线程之间计数器互不影响,独立存储,是线程私有的内存。
5)如果线程正在执行的是一个Java方法,它的程序计数器记录的是正在执行的虚拟机字节码指令的地址;
 如果正在执行的是Native方法,它的程序计数器值为空(undefined),此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。

2、java虚拟机栈(执行Java方法)
线程私有、方法执行内存模型(栈帧的入栈出栈)、栈帧(局部变量表、操作数栈、动态链接、方法出口)、
局部变量表(基本类型、对象引用、returnAddress)

1)Java虚拟机栈是线程私有的,它的生命周期与线程相同。它描述的是Java方法执行的内存模型。
2)方法执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
 方法调用到执行完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
3)虚拟机栈的局部变量表,存放了编译器可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、
 对象引用、returnAddress(指向一条字节码指令的地址)类型。
4)long、double类型长度64位,会占2个局部变量空间,其余的数据类型只占用1个局部变量空间。
5)局部变量表所需空间在编译期完成分配,运行期间不会改变局部变量表的大小。
6)如果线程请求的栈深入大于虚拟机允许的深度,抛出StackOverflowError;
 或者可动态扩展的栈,如果扩展时无法申请到足够内存,会抛出OOM异常。

3、本地方法栈(执行Native方法)
1)与Java虚拟机栈类似,区别在于Java虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机执行Native方法服务。
2)Sun 的 HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样,本地方法栈区域也会抛出StackOverflow/OOM异常。
4、Java堆
线程共享、存放对象和数组、垃圾回收的主要区域、回收方式分:新生代/老生代、可处于逻辑上连续物理上不连续的空间中
1)Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。
2)用于存放对象实例、数组。(随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙
 的变化发生,所有的对象都分配在堆上渐渐变得不那么绝对了)
3)Java堆是垃圾收集器管理的主要区域
a:从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代、老年代;
 再细致一点的有:Eden空间、From Survivor空间、To Survivor空间等。
b:从内存分配的角度看,线程共享的Java堆中能划分出多个线程私有的分配缓冲区,存储的仍然是对象实例。
 进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
4)Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
5)如果堆中没有内完成实例分配,且堆无法再扩展时,抛出OOM异常。

5、方法区
线程共享、可处于逻辑上连续物理上不连续的空间中、运行时常量池属于方法区
1)线程共享的内存区域,存储已经加载的类信息、常量、静态变量、即时编译后的代码等数据
2)对于HotSpot虚拟机称为永久代,把GC分代收集扩展至方法区,省去专门为方法区编写内存管理代码的工资。
 对于其他虚拟机不存在永久代的概念。
3)可以处于物理上不连续的内存空间中,只要逻辑上是连续即可。
4)这个区域垃圾收集行为比较少出现,该区域内存回收目标主要针对常量池的回收和对类型的卸载,但是回收和卸载条件苛刻。
5)方法区无法满足内存分配需求时,抛出OOM异常。

5.1、运行时常量池 【Class文件中的常量池、翻译出来的常量、动态添加的常量(String.intern())】
1)运行时常量池是方法区的一部分,Class文件中存放有常量池信息,用于存放编译期生成的各种字面量和符号引用,
在类加载后进入方法区的运行时常量池中存放。
2)运行时常量池相对于Class文件常量池的特征:
a:除了保持Class文件中描述的符号引用,还会把翻译出来的直接引用也存入运行时常量池中。
b:具有动态性,除了编译期预置入Class文件中的常量池,还可以将新的常量放入池中,比如String.intern();
3)常量池无法再申请到内存时,会抛出OOM


6、直接内存(不属于Java虚拟机规范的内存区域)
【NIO包引入的Channel、Buffer的I/O方式,直接使用Native函数,不受Java堆大小限制,但是受本机总内存大小限制,会抛OOM】
1)不属于虚拟机运行时数据区,非Java虚拟机规范定义的内存区域。但是也可能导致OOM
2)NIO(New Input/Output)中引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,该方式可以使用Native函数
 直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆
 和Native堆中来回复制数据,提高性能。
3)本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制,动态扩展超出限制时
 会抛出OOM

二、hotspot虚拟机对象
1、虚拟机中对象的创建过程
new 指令 -> 常量池中能否定位到一个类的符号引用 -> 该符号引用代表的类是否已加载、解析、初始化 
-> 没有,则先执行响应的类加载 -> 为新生对象分配内存(对象所需内存大小在类加载完成后可以完全确定) 
-> 分配的内存空间初始化为零值(不包括对象头),如果使用线程分配缓冲,初始化为零的操作提前至分配缓冲时进行
(也就是赋初始值,保证对象实例字段在Java代码中可以不赋初始值就能直接使用)
-> 对象的设置(对象的类、类的元数据、对象的哈希码、对象的GC分代年龄、是否启用偏向锁,这些存放在对象的对象头(Object Header)中)
-> 执行<init>方法(由字节码中是否跟随invokespecial指令决定),把对象按照程序员的意愿初始化 -> 完
------------
1)内存分配方式:
a:指针碰撞:Java堆中内存绝对规整,所有用过的内存都放在一边,中间放着一个指针作为分界点的指示器,分配内存就是把指针向空闲那边挪动一段与对象大小相等的距离。
b:空闲列表:Java堆中内存不规整,已使用的内存与空闲内存相互交错,虚拟机通过维护空闲列表(记录哪些内存块可用),分配内存时从列表中找到一块足够大的空间分给对象实例,并更新空闲列表记录。
2)Java堆是否规整由虚拟机采用的垃圾收集器是否带有压缩整理功能决定。
3)分配内存操作并发解决方案:
a:对分配内存空间的动作进行同步处理CAS+失败重试,保证更新操作的原子性。(CAS参考AQS源码解析相关博客)
b:把内存分配的动作按照线程划分在不同的空间中进行。每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲。
 线程要分配内存时,从线程的缓冲上分配,只有缓冲用完并分配新的缓冲时,才需要同步锁定。
 线程分配缓冲设置:-XX:+/-UseTLAB 来设定。

2、对象的内存布局 【对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)】
1)对象头:包括两部分信息
a:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
这部分数据长度在32位-64位(未开启压缩指针)的虚拟机分别为32bit和64bit。
b:存储类型指针,即对象指向它的类元数据的指针。虚拟机通过这个指针来确定这个对象是哪个类的实例。
(但不是所有的虚拟机实现都必须在对象数据上保留类型指针,查找对象元数据不一定要经过对象本身)
ps:如果对象是一个Java数组,对象头中还必须有一块用于记录数组长度的数据。
虚拟机可以通过普通Java对象的元数据确定对象的大小,但是从数组的元数据无法确定数组大小。
2)实例数据:对象真正存储的有效信息,是代码中定义的各种类型的字段内容(无论父类继承的还是子类定义的,都需要记录)
   a:这部分的存储会受虚拟机分配策略参数和字段在Java源码重定义顺序的影响
b:HotSpot分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
 首先,相同宽度的字段总是被分配到一起;
 其次,父类中定义的变量会出现在子类之前;
 最后,如果CompatFields参数值为true(默认为true),那么子类中较窄的变量可能会插入父类变量的空隙中。
3)对齐填充:不是必然存在的,没有特别的含义,仅仅起占位符的作用。
HotSpot虚拟机的自动内存管理系统,要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。而对象头部正好是8字节的整数倍,因此当对象实例数据部分没有对齐时,需要通过对齐填充来补全。

3、对象的访问定位
1)通过栈里的reference数据来操作堆上的具体对象。
2)对象的访问方式取决于虚拟机的实现。主流访问堆中对象方式:使用句柄、直接指针。
 a:使用句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,
   句柄中包含对象实例数据与类型数据各自的具体地址信息。
 b:直接指针:Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中存储的是对象地址。
3)使用句柄访问的好处是,reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象)时,
 只会改变句柄中的实例数据指针,而reference本身不需要修改。

     直接指针访问的好处是,速度更快,节省了一次指针定位的时间开销。HotSpot使用的是直接指针访问对象。

    
        类的符号引用,常量,放在方法区的常量池中。
        数组,对象,放在堆里。
        静态变量,类信息,放在方法区。
        基本类型数据,对象的引用,放在栈的局部变量表中。
        堆和方法区是线程共享,程序计数器和栈是线程私有。

猜你喜欢

转载自blog.csdn.net/u010577768/article/details/80085489