概述Java运行时数据区域
程序计数器(Program Counter Register)虚拟机栈(VM Stack)本地方法栈(Native Method Stack)堆(Heap)方法区(Method Area)
运行时常量池(Runtime Constant Pool)直接内存(DIrect Memory) 对象
对象的创建对象的内存布局对象的访问定位 部分JVM参数参考
概述Java运行时数据区域包括如下部分:
线程独享
- 程序计数器(Program Counter Register)虚拟机栈(VM Stack)本地方法栈(Native Method Stack)
线程共享
- 方法区(Method Area)堆(Heap)
- 一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,为了线程切换后能恢复倒正确的执行位置,每条线程都需要有一个独立的程序计数器如果线程执行Java方法,则该计数器记录的是虚拟机字节码指令的地址,如果正在执行Native方法,则计数器的值为空此区域是唯一一个在《Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域
- Java虚拟机栈(Java Virtual Machine Stack) 也是线程私有的,它的生命周期与线程相同每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack frame)用于存储局部变量表、 *** 作数栈、动态连接、方法出口等信息,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程局部变量表存放了编译器可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他于此对象相关的位置) 和 returnAddress类型(指向了一条字节码指令的地址)局部变量表中的数据类型所占的空间以局部变量槽(Slot)来表示,其中64位长度的 long 和 double类型的数据会占用两个变量槽,其余数据类型占用一个变量槽局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小在 《Java 虚拟机规范》中对这个内存区域规定了两类异常情况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常,如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError异常
- 本地方法栈与虚拟机栈(VM Stack)所发挥的作用是非常相似的,只是本地方法栈服务的是Native方法,虚拟机栈服务的是Java方法本地方法栈也会在栈深度溢出时抛出StackOverflowError异常,在栈扩展失败时分别抛出 OutOfMemoryError异常
- 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例堆是垃圾收集器管理的内存区域所有线程共享的堆可以划分出多个线程私有的分配缓冲区TLAB(Thread Local Allocation Buffer),以提升为对象分配内存时的效率,免得多个线程互相影响,还有一种解决方案就是在创建对象时加锁,可以使用CAS锁,这样就可以不必单独为一个线程划分一个分配缓冲区,但是多线程创建对象时,可能由于锁而等待堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的堆既可以是固定大小的,也可以是可扩展的,当前主流的Java虚拟机都是按照可扩展来实现的,如果堆中没有足够内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常这里的堆是Java堆,而不是数据结构所指的堆结构,这里的堆可以理解为就是一块逻辑上连续,并且用于存放对象实例的内存空间,与数据结构的堆没有关系。早期有些一些高级语言使用数据结构的堆Heap或者叫优先队列Priority Queue来找到最小可用的内存块,所以这就是堆叫法的由来方法中的局部变量存放在虚拟机栈(VM Stack)中,类的成员变量不管是基本数据类型还是引用类型(这里的引用类型不是指对象实例,对象实例是肯定存放在堆中的,引用会随着对象实例一起存放在堆中)都存放在堆中,因为多个线程对对象的基本数据类型和引用成员变量进行写 *** 作会有线程安全问题,而只有堆是共享的才会有线程安全问题,而虚拟机栈是独享的没有线程安全问题
- 方法区是线程共享的内存区域,用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据在JDK8以前方法区存放在堆中的永久代(Permanent Generation),也被垃圾收集器GC(Garbage Collected)管理,在JDK8以后方法区存放在元空间(meta Space),元空间是在堆外内存分配的一块空间在JDK6开始HotSpot虚拟机开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划,在JDK7时将字符串常量池移出了永久代,放入堆中其他区域,永久代本来就在堆中,在JDK8时把方法区剩余内容(主要是类型信息)全部移到元空间中,虽然常量池和类型信息处于不同的两个空间,但它们在逻辑上仍然属于方法区在JDK8时出现的情况,字符串常量池在堆中,而方法区中其他的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据都存放在元空间中根据《 Java 虚拟机规范 》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
- 运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池表(Constant Pool Table),用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池,如 int a = 10,10即是字面量,a即使符号引用运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说并非预置入Class文件中常量池的内存才能进入方法区运行时常量池,如String类的intern()方法,不过运行时常量池在元空间,而字符串常量池在堆中,但字符串常量池逻辑上仍然属于运行时常量池运行时常量池逻辑上也是方法区的一部分,自然受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常
- 直接内存并不是虚拟机运行时数据区(运行时数据区包括虚拟机栈、本地方法栈、程序计数器、堆、方法区)的一部分,但是这部分内存也频繁地被使用,而且也可能导致OutOfMemoryError异常出现JDK1.4中新加入的NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个储存在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行 *** 作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据直接内存由于是在堆外,所以直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制所以在设置虚拟机参数时,不止需要考虑堆的大小还要考虑直接内存,否则会使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常
- 当Java虚拟机遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有则必须先执行相应的类加载过程在类加载检查通过后,接下来虚拟机将为新生对象分配内存,然后修改指针指向该内存,在并发情况下修改指针所指向的位置也不是线程安全的,第一种解决方法是使用锁进行同步处理如CAS配上重试的方式保证更新 *** 作的原子性,第二种解决方法是每个线程在Java堆中预先分配一块小内存,称为本地线程分配缓冲(Thread Local Allocation Buffer),哪个线程要分配内存,就在自己的本地线程分配缓冲中分配,只有本地缓冲区用完了,分配新的缓冲区时才需要同步锁定内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,这样保证了对象的成员变量可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值Java虚拟机还要对对象头进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都储存在对象头中,根据虚拟机的当前运行状态的不同,如是否启用偏向锁,对象头会有不同的设置方式设置完对象头后,需要调用构造函数初始化,一般遇到new 代码会生成字节码流中的new指令和invokespecial指令,invokespecial指令就是指向构造函数的意思,如果是克隆过程则不会产生invokespecial指令则不会执行构造函数
- 对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)第一部分对象头包括两类信息,一类是用于存储对象自身的运行时数据,一类是类型指针。对象自身的运行时数据指的是哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据被称为Mark Word,由于Mark Word要存储的数据非常多,所以被设计成有着动态定义的数据结构,以便在有限的空间内存储更多的信息。类型指针即对象指向它的类型元数据的指针,即通过这个指针可以确定对象是哪个类的实例。如果对象是一个Java数组,则对象头还必须有一块用于记录数组长度的数据
Mark Word存储内容(根据对象的状态不同,存储的内容不同)
- 第二部分实例数据是对象真正存储的有效信息,即程序代码中定义的各种类型的字段内容,从父类继承下来和子类中定义的字段都必须记录起来。实例数据中各个字段的存储顺序为long/double、int、short/char、byte/boolean、oop(Ordinary Object Pointer)普通对象指针,即先存储基本类型、再存储引用指针,先存储较长的数据,再存储较短的数据,相同宽度的字段总是被分配到一起存放,满足上述条件后,父类中定义的变量会存放再子类中定义的变量前面第三部分对齐填充并不是必然存在的,也没有上面特别的含义,仅仅起着占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即任何对象的大小都必须是8字节的整数倍,对齐也可以提高CPU的处理速率,因为CPU访问内存的粒度不是一个字节一个字节访问的,而是只能从特定地址开始访问,如果读取8字节数据,如果对齐则CPU只要访问内存一次即可读取到,如果不对齐则要分两次,第一次访问一部分,第二次访问剩余部分。对象头部分已经被精心设计成正好是8字节的倍数(1倍或2倍),如果实例数据部分没有对齐,则需要对齐填充来补全
- 对象访问方式主要有两种:使用句柄和直接指针使用句柄访问,Java堆中将划分出一块内存作为句柄池,栈中的引用reference存储的就是对象的句柄地址,句柄中含有两部分,一部分是对象实例数据指针(对象实例数据包括对象头、实例数据、填充),第二部分是对象类型数据指针,指针也就是地址,根据地址可以去堆中其他地方访问对象实例数据和对象类型数据,这种方式的好处是引用reference存储的是句柄地址,在对象移动的时候无须改变引用中的句柄地址,只需改变堆中的句柄中的对象实例数据地址,缺点是访问对象时先得访问句柄获取对象的存储地址,增加一次访问开销使用直接指针访问,栈中引用reference存储的是对象地址,直接可以通过该地址访问堆中的对象,但是在对象中必须存储类型数据指针,因为没有句柄存储类型数据指针了,好处是访问开销小,但是移动对象的时候需要改变栈中引用reference的地址。Java中使用直接指针访问,上面描述对象内存布局时也可以看出Java使用的是直接指针,因为在对象头存储着类型指针,用于执行存放类信息的内存块
《深入理解 Java 虚拟机》
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)