JVM定义了若干个程序执行期间使用的数据区域。这个区域里的一些数据在JVM启动的时候建立,在JVM退出的时候销毁。而其余的数据依赖于每个线程,在线程建立时建立,在线程退出时销毁。java
程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。程序员
因为Java 虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)只会执行一条线程中的指令。所以,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。算法
若是线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。编程
此内存区域是惟一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError状况的区域。数组
线程私有,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每一个方法被执行的时候都会同时建立一个栈帧(Stack Frame)用于存储局部变量表、操做栈、动态连接、方法出口等信息。缓存
动画是由一帧一帧图片连续切换结果的结果而产生的,其实虚拟机的运行和动画也相似,每一个在虚拟机中运行的程序也是由许多的帧的切换产生的结果,只是这些帧里面存放的是方法的局部变量,操做数栈,动态连接,方法返回地址和一些额外的附加信息组成。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。安全
对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的全部字节码指令都只针对当前栈帧进行操做。数据结构
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中肯定了该方法所须要分配的最大局部变量表的容量。多线程
局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot能够存放一个32位之内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。jvm
reference类型虚拟机规范没有明确说明它的长度,但通常来讲,虚拟机实现至少都应当能今后引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。
returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,若是是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中经过this访问。
Slot是能够重用的,当Slot中的变量超出了做用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
和局部变量区同样,操做数栈也是被组织成一个以字长为单位的数组。可是和前者不一样的是,它不是经过索引来访问,而是经过标准的栈操做——压栈和出栈—来访问的。好比,若是某个指令把一个值压入到操做数栈中,稍后另外一个指令就能够弹出这个值来使用。
虚拟机在操做数栈中存储数据的方式和在局部变量区中是同样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操做数栈以前,也会被转换为int。
虚拟机把操做数栈做为它的工做区——大多数指令都要从这里弹出数据,执行运算,而后把结果压回操做数栈。好比,iadd指令就要从操做数栈中弹出两个整数,执行加法运算,其结果又压回到操做数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操做数栈中,其后iadd指令从操做数栈中弹出那两个整数相加,再将结果压入操做数栈。第四条指令istore_2则从操做数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程当中局部变量和操做数栈的状态变化,图中没有使用的局部变量区和操做数栈区域以空白表示。
虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用能够当作是每一个方法的间接引用。若是表明栈帧A的方法想调用表明栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用做为参数,可是由于符号引用并非直接指向表明B方法的内存位置,因此在调用以前还必需要将符号引用转换为直接引用,而后经过直接引用才能够访问到真正的方法。
若是符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,若是是在运行期间转换为直接引用,那么这种转换就成为动态链接。
方法的返回分为两种状况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常致使的方法结束,这种状况是不会传返回值给上层的调用方法。
不过不管是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,若是方法是正常退出的,则调用者的PC计数器的值就能够做为返回地址,,果是由于异常退出的,则是须要经过异常处理表来肯定。
方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操做,所以方法退出时可能作的事情包括:恢复上层方法的局部变量表以及操做数栈,若是有返回值的话,就把返回值压入到调用者栈帧的操做数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。
在Java 虚拟机规范中,对虚拟机栈规定了两种异常情况:若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError 异常;若是虚拟机栈能够动态扩展(当前大部分的Java 虚拟机均可动态扩展,只不过Java 虚拟机规范中也容许固定长度的虚拟机栈),当扩展时没法申请到足够的内存时会抛出OutOfMemoryError 异常。
本地方法栈(Native MethodStacks)与虚拟机栈所发挥的做用是很是类似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并无强制规定,所以具体的虚拟机能够自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈同样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。可是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会致使一些微妙的变化发生,全部的对象都分配在堆上也渐渐变得不是那么“绝对”了。
堆是垃圾收集器管理的主要区域,所以不少时候也被称作“GC 堆”。
堆的大小能够经过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操做系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可经过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减少heap的大小到-Xms指定的大小,可经过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,一般-Xms与-Xmx的值设成同样。
若是从内存回收的角度看,因为如今收集器基本都是采用的分代收集算法,因此Java 堆中还能够细分为:新生代和老年代;
新生代:程序新建立的对象都是重新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(一般又称S0和S1或From和To)构成,可经过-Xmn参数来指定新生代的大小,也能够经过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小。
老年代:用于存放通过屡次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种状况:一、大对象,可经过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来表明超过多大时就不在新生代分配,而是直接在老年代分配。二、大的数组对象,且数组中无引用外部对象。
老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出OutOfMemoryError 异常。
方法区在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。
简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用以前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、链接(验证、准备、解析)和初始化。而装载(后的结果就是由.class文件转变为方法区中的一段特定的数据结构。这个数据结构会存储以下信息:
类型信息
这个类型的全限定名
这个类型的直接超类的全限定名
这个类型是类类型仍是接口类型
这个类型的访问修饰符
任何直接超接口的全限定名的有序列表
字段信息
字段名
字段类型
字段的修饰符
方法信息
方法名
方法返回类型
方法参数的数量和类型(按照顺序)
方法的修饰符
其余信息
除了常量之外的全部类(静态)变量
一个指向ClassLoader的指针
一个指向Class对象的指针
常量池(常量数据以及对其余类型的符号引用)
JVM为每一个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,integer,和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项同样,是经过索引访问的。
每一个类的这些元数据,不管是在构建这个类的实例仍是调用这个类某个对象的方法,都会访问方法区的这些元数据。
构建一个对象时,JVM会在堆中给对象分配空间,这些空间用来存储当前对象实例属性以及其父类的实例属性(而这些属性信息都是从方法区得到),注意,这里并非仅仅为当前对象的实例属性分配空间,还须要给父类的实例属性分配,到此其实咱们就能够回答第一个问题了,即实例化父类的某个子类时,JVM也会同时构建父类的一个对象。从另一个角度也能够印证这个问题:调用当前类的构造方法时,首先会调用其父类的构造方法直到Object,而构造方法的调用意味着实例的建立,因此子类实例化时,父类确定也会被实例化。
类变量被类的全部实例共享,即便没有类实例时你也能够访问它。这些变量只与类相关,因此在方法区中,它们成为类数据在逻辑上的一部分。在JVM使用一个类以前,它必须在方法区中为每一个non-final类变量分配空间。
方法区主要有如下几个特色:
一、方法区是线程安全的。因为全部的线程都共享方法区,因此,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类尚未被装入JVM,那么只容许一个线程去装载它,而其它线程必须等待
二、方法区的大小没必要是固定的,JVM可根据应用须要动态调整。同时,方法区也不必定是连续的,方法区能够在一个堆(甚至是JVM本身的堆)中自由分配。
三、方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集
能够经过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来讲,不少人愿意把方法区称为“永久代”(PermanentGeneration),本质上二者并不等价,仅仅是由于HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其余虚拟机(如BEA JRockit、IBM J9 等)来讲是不存在永久代的概念的。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并不是数据进入了方法区就如永久代的名字同样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
当方法区没法知足内存分配需求时,将抛出OutOfMemoryError异常。
名称 |
特征 |
做用 |
配置参数 |
异常 |
程序计数器 |
占用内存小,线程私有, 生命周期与线程相同 |
大体为字节码行号指示器 |
无 |
无 |
虚拟机栈 |
线程私有,生命周期与线程相同,使用连续的内存空间 |
Java 方法执行的内存模型,存储局部变量表、操做栈、动态连接、方法出口等信息 |
-Xss |
StackOverflowError OutOfMemoryError |
java堆 |
线程共享,生命周期与虚拟机相同,能够不使用连续的内存地址 |
保存对象实例,全部对象实例(包括数组)都要在堆上分配 |
-Xms -Xsx -Xmn |
OutOfMemoryError |
方法区 |
线程共享,生命周期与虚拟机相同,能够不使用连续的内存地址 |
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 |
-XX:PermSize: 16M -XX:MaxPermSize 64M |
OutOfMemoryError |
运行时常量池 |
方法区的一部分,具备动态性 |
存放字面量及符号引用 |
|
|
直接内存(Direct Memory)并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用,并且也可能致使OutOfMemoryError 异常出现,因此咱们放到这里一块儿讲解。
在JDK 1.4 中新加入了NIO(NewInput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可使用Native 函数库直接分配堆外内存,而后经过一个存储在Java 堆里面的DirectByteBuffer 对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在Java 堆和Native 堆中来回复制数据。
常常有人把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。
堆很灵活,可是不安全。对于对象,咱们要动态地建立、销毁,不能说后建立的对象没有销毁,先前建立的对象就不能销毁,那样的话咱们的程序就步履维艰,因此Java中用堆来存储对象。而一旦堆中的对象被销毁,咱们继续引用这个对象的话,就会出现著名的 NullPointerException,这就是堆的缺点——错误的引用逻辑只有在运行时才会被发现。
栈不灵活,可是很严格,是安全的,易于管理。由于只要上面的引用没有销毁,下面引用就必定还在,在大部分程序中,都是先定义的变量、引用先进栈,后定义的后进栈,同时,区块内部的变量、引用在进入区块时压栈,区块结束时出栈,理解了这种机制,咱们就能够很方便地理解各类编程语言的做用域的概念了,同时这也是栈的优势——错误的引用逻辑在编译时就能够被发现。
栈--主要存放引用和基本数据类型。
堆--用来存放 new 出来的对象实例。
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;好比申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,没法释放已申请的内存空间,一次内存泄露危害能够忽略,但内存泄露堆积后果很严重,不管多少内存,早晚会被占光。
memory leak会最终会致使out ofmemory。
Java 堆内存的OutOfMemoryError异常是实际应用中最多见的内存溢出异常状况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。
要解决这个区域的异常,通常的手段是首先经过内存映像分析工具(如Eclipse Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是不是必要的,也就是要先分清楚究竟是出现了内存泄漏(Memory Leak)仍是内存溢出(Memory Overflow)。
若是是内存泄漏,可进一步经过工具查看泄漏对象到GC Roots 的引用链。因而就能找到泄漏对象是经过怎样的路径与GC Roots 相关联并致使垃圾收集器没法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就能够比较准确地定位出泄漏代码的位置。
若是不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还能够调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的状况,尝试减小程序运行期的内存消耗。
一、JVM 会试图为相关Java对象在Eden Space中初始化一块内存区域。
二、当Eden空间足够时,内存申请结束;不然到下一步。
三、JVM 试图释放在Eden中全部不活跃的对象(这属于1或更高级的垃圾回收)。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。
四、Survivor区被用来做为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,不然会被保留在Survivor区。
五、当Old区空间不够时,JVM 会在Old区进行彻底的垃圾收集(0级)。
六、彻底垃圾收集后,若Survivor及Old区仍然没法存放从Eden复制过来的部分对象,致使JVM没法在Eden区为新对象建立内存区域,则出现“outofmemory”错误。
对象访问在Java 语言中无处不在,是最普通的程序行为,但即便是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,以下面的这句代码:
Object obj = newObject();
假设这句代码出如今方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,做为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,造成一块存储了Object 类型全部实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不一样,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
因为reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并无定义这个引用应该经过哪一种方式去定位,以及访问到Java 堆中的对象的具体位置,所以不一样虚拟机实现的对象访问方式会有所不一样,主流的访问方式有两种:使用句柄和直接指针。
若是使用句柄访问方式,Java 堆中将会划分出一块内存来做为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。