JVM内存结构详解

JVM内存结构详解

程序计数器

程序计数器(progaram counter register)是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型中(仅概念模型,各种虚拟机可能会通过一些高效的方法去实现),字节码解释器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于java虚拟机的多线程是通过线程间轮流切换并分配处理器的执行时间来实现的。在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们成这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个java操作,这个计数器记录的是正在你执行的虚拟机字节码指令的地址:如果正在执行的是Native方法,这个计数器的值为空(undifined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

小结:程序计数器是一小块内存空间,每个线程都拥有独立的程序计数器,互不影响,java操作中用来记录正在执行的的字节码指令的地址,如果是本地方法操作,则计数器的值为空,并且计数器没有内存溢出的情况

java 虚拟机栈

与程程序计数器一样,java虚拟机栈(java vertual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(stack frame),用于存储局部变量表、操作数栈、动态链接、方法出口信息等。每个方法从调用到执行完成的过程,就对应这一个栈帧在虚拟机栈中入栈到出栈的过程。一般,我们将内存分为栈内存和堆内存,这种分类不太专业,这里的栈内存就是虚拟机栈,或者说是虚拟机栈中的局部变量
局部变量表存放了编译期可知的各种基本类型、对象引用类型(他不等同于对象本身,可能是一个指向对象其实地址的引用指针,也可能死指向一个代表对象的句柄或者其他与对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
其中64位长度的long和double类型的数据占据2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。在方法运行期间不会改变局部变量表的大小。
在java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error(栈溢出)异常;如果虚拟机栈可以动态扩展(当前大部分java虚拟机都可以动态扩展,追过java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请足够的内存,就会抛出OutOfMemoryEroor(内存溢出)异常

小结:java虚拟机栈也是线程私有的,进入方法时会创建一个栈帧,储存方法的内部信息,一个方法的执行过程就代表这个栈帧从虚拟机栈入栈到出栈的过程。
以及这个区域有两种异常:栈溢出,内存溢出
栈溢出:当线程请求的栈深度超过虚拟机允许的深度时,抛出异常。
内存溢出:大多数虚拟机支持动态扩展,如果扩展是无法申请到足够的内存,就会抛出异常

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法和数据结构并没有强制规定,因此具体的虚拟机可以*实现它。甚至有的虚拟机(如sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机合二为一。与虚拟机栈一样,本地方法栈区域也会抛出栈溢出异常和内存溢出异常。
什么是native方法
简单地讲,一个Native Method就是一个java调用非java代码的接口。
native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

小结:本地方法栈是为虚拟机用到的Native 方法服务的,Native 方法简单的来说是java调用非java代码的接口,接口中Native修饰的方法时原生态方法,对应的实现也是在其他语言实现的文件中,java可以通过JNI接口调用其他语言对底层的访问

java 堆

java堆,对于大多数应用来说,java堆(java Heap)是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,它在虚拟机启动的时候创建。此内存区域的位置目的就是存放对象实例,几乎所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的成熟,栈上分配、标量分配、优化技术将会导致一些微妙的变化发生,所有的对象都分配在对上也渐渐变得不那么“绝对”了。
java堆是垃圾收集器管理的主要区域,因此很多时候也被成为“GC堆”(Collected Heap:收集堆,不是垃圾堆的意思)。从内存回首的角度来看,由于现在的收集器基本上都采用分代收集算法,所以java堆中还可以细分为:新生代和老年代:再细致一点的有Eden空间、From Survivor空间、To Servivor空间等。从内存分配的角度来看,线程共享的java对中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),不过无论如何划分,都与内存内容无关,无论在哪个区域,存储的都仍然是对象实例,进一步划分的目的是为更好的回收内存,或者更快的分配内存。
根据java虚拟机规范的规定,java对可以处于物理不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样,在实现时,既可以实现固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryErro异常。

小结:java堆是内存结构中最大的一块区域,也是GC的主要区域,主要是存储对象实例,对象实例也分为年轻代与老年代,年轻代也分为3个空间:Eden空间、From Survivor空间、To Survivor空间,堆也会抛出内存溢出的异常,当堆中没有内存完成实例时,也无法完成扩展时,将会抛出异常

方法区

方法区(Method Area)与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态方法、即时编译器编译后的代码等书库。虽然java虚拟机规范把方法区描述为为堆的一个逻辑部分,但是他却又一个别名叫做Non-Heap(非堆),目的是与java堆区分开。
对于习惯在hotSpot虚拟机上开发,部署程序的开发者来说,很多人都愿意把方法区成为“永久代”(permanent generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择吧GC分代收集扩展到方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾回收期可以向管理java对一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他的虚拟机(如BEA ,JRockit,IBM J9)来说是不存在永久代的概念的。原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,现在看来并不是一个好主意,因为这样更容易遇到内存溢出问题(永久代-XX:MaxPermSize的上限,J9和JRockit,只要没有触碰进程可用内存的上限,例如32位系统中的4GB,就不会出现问题),而且只有极少数方法(例如String.intern())会因这个原因导致不同的虚拟机下有不同的表现。因此,对于HotSpot虚拟机,根据官方发布的路线图信息,现在也有放弃永久代并逐步改为采用Native Memory来实现方法区的规定了,从JDK1.7的Host中,已经把放在永久代的字符串常量池移除。
java虚拟机规范对于方法区的限制非常宽松,除了和java堆一样需要连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区就如永久代的名字一样“永久”存在。这区域的内存回收目标主要是针对常量池的回收和类型的卸载,一般来说,这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的,在sun公司的BUG列表中,曾出现过若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
根据java虚拟机的规范,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

小结:方法区是线程共享的一块内存区域,它主要储存,虚拟机编译后的类信息,常量,静态方法,这块内存区域的GC回收主要针对常量池,与类型,当方法区无法满足内存分配,将抛出内存溢出异常

常量池

常量池是方法区的一部分,用于存放编译期生成的各中字面值和符号引用,这部分内容将在类加载后进入常量池中存放
运行时常量池并不一定只能在编译期间存放常量值,它具有动态性,如:String类的internal()方法可以向串池中存放字符串常量
常量池无法申请到内存,会抛出OutOfMemoryError

小结:常量池存在于方法区内,存放编译后的常量值与符号引用,当常量池无法申请到内存时将抛出内存溢出异常

直接内存区

直接内存是一个基于缓冲区和IO,通过Native方法分配的堆外内存,用于解决java堆和Native堆数据频繁复制。
这个内存通常被忽略,但是他任然基于内存,因此也会有内存溢出异常。

小结:直接内存区基于缓存区域与IO,通过Native方法分配的堆外内存,用于解决堆与本地方法栈【Native Method Stack】的数据复制问题,直接内存区也存在内存溢出的问题

JVM普通对象创建

单线程创建对象的空间分配
指针碰撞【内存规整】:
如果堆内存规整,每次为对象分配内存,采用移动指针的方式,指针移动距离等于对象的大小
空闲列表【内存不规整】:
如果内存不规整,已使用内存和空闲内存交叉在一起,虚拟机就会找到一块能够存放创建对象的空闲内存,并且在内存列表中更新。

多线程并发创建对象原理
对内存空间进行同步处理,虚拟机实际采用的CAS算法加失重匹配的方式保证更新操作的原子性
本地线程分配缓冲(Thread Local Annotation Buffer,TLAB),为每一个线程预先分配一小块内存,在TLAB用完并分配新的TLAB时,需要同步。

JVM对象的内存布局

对象在内存中的布局可以分为三个部分:对象头、实例数据、对齐填充

对象头
对象头分为两部分:Mark Word类型指针
MarkWord:用于存放对象的哈希码、GC分代年龄、锁状态标记、线程持有的 锁、线程偏向ID、偏向时间戳等
②即对象指向它的类元数据的指针,虚拟机通过这个指针来确认对象属于哪一个 类的实例。
③数组类型,对象头还需要记录数组的长度信息
实例数据
存储对象被定义的基本信息,包括继承自父类的信息。不同的虚拟机的存储策略肯恩不同,通常按照数据类型所占空间从大到小如:double、long…char,相同大小放一起,父类在子类之前。
对齐填充
这一部分不是必须的。它起着占位符的作用,当一个兑现的大小不是8bit的整数倍,对其补充将填充部分空间来补齐缺少的部分。

内存溢出

java堆内存溢出
Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达到的路劲来避免垃圾回收机制清除这些对象,name在对象数量到达最大堆容量后就会产生内存溢出问题
堆可能产生内存溢出的原因:对象生命周期过长、持有时间过长、GC Roots引用链错误
·虚拟机栈或者方法栈溢出
①如果线程请求的栈深度大于虚拟机所允许的最大深度,则抛出*Error
②如果虚拟机在扩展栈是无法申请到足够的内存空间,则抛出OutOfMemoryError
场景分析:
单线程时,不论是栈帧太大还是虚拟机栈容量太小,当内存无法分配时,虚拟机抛出 的都是*Error。
多线程时,如果栈空间太大,势必会发生内存溢出。这种溢出通常是OutOfMemoryError
在我们不降低线程数和更改更大的操作系统的情况下,可以通过减少最大堆和减少最大栈容量的方式来换取更多的线程
方法区和运行时常量池溢出
对于常量池的对象引用,jdk1.7和jdk1.6有不同的表现,jdk1.7后,将常量池移除到永久代外面,而且,常量池新加入的对象也不再是存储的对象本身,而是存储了对象的引用。
如:JVM内存结构详解
代码执行的结果:jdk1.6 连个false
Jdk1.7的话是一个true和一个false。
原因是jdk1.6往常量池添加对象,如果对象首次出现,则对对象直接复制。而jdk1.7,如果添加的对象首次出现,则将对象的引用复制到常量池中,所以先是true,而java在常量池中本身就存在,不属于第一次出现,所以结果为false
方法区用于保存Class的相关信息,如:类名,访问修饰符,常量池,字段描述,方法描述等,所以我们在用例如spring,hibernate等支持cglib,或者虚拟机上的动态语言(如groove)等持续创建类来实现语言的动态性,就很容易遇到方法区和常量池的溢出。