JVM内存区域划分
一、JVM运行时数据区划分
根据《Java虚拟机规范》JVM会把它管理的内存划分为若干个不同的数据区域,如下图所示:方法区、堆、栈(虚拟机栈、本地方法栈)、程序计数器。线程私有的意思是指,JVM每遇到一个新的线程就会为他们分配栈和程序计数器。
PS:
(1)非线程共享区域的生命周期与所属线程相同,而线程共享区域与JAVA程序运行生命周期相同,GC只发生在线程共享的区域。
(2)程序计数器无内存溢出异常,其他四个区域会抛出OutofMemoryRrror异常。
二、程序计数器
程序计数器的功能类似于计算机组成原理中的PC寄存器,用于存放下一条指令所在单元的地址。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。虽然JVM的程序计数器跟PC有所区别,但是在概念上是等同的,JVM中的PC存放的是程序正在执行的字节码的行号,字节码解释器的工作就是通过改变程序计数器的值来选择下一条需要执行的字节码指令。
在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复到切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
如果线程执行的是一个Java方法,那么寄存器里面记录的就是正在执行的虚拟机字节码指令的地址,如果线程执行的是一个native方法,那么寄存器记录的值为undefined。
由于程序计数器是固定宽度的存储空间,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
三、虚拟机栈
虚拟机栈中存放每个方法执行时创建的栈帧,对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存放局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的 Code 属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表存放了各种编译器已知的各种基本数据类型,对象引用等;程序员关注的栈内存一般是指的局部变量表的内存,局部变量表所需的内存空间在编译时期完成分配,在方法运行期间不会改变局部变量表的大小。
操作数栈:程序中的计算是通过操作数栈来完成的,操作数栈的最大深度也是在编译的时候就确定了。Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称 Java 虚拟机是基于栈的,这点不同于 Android 虚拟机,Android 虚拟机是基于寄存器的。基于栈的优点是可移植性强,缺点是速度相对较慢。
动态链接:每个栈帧都包含一个指向运行时常量池的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
方法返回地址:方法正常退出时,调用者的 PC 计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令。
-Xss128k:设置每个线程的栈大小,jdk1.5以后每个线程的栈大小为1M,减小这个值能生成更多的线程,但同时可能会带来OutOfMemoryError。
在Java虚拟机规范中针对这个区域规定了两种异常:1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,2)如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
四、本地方法栈
该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
五、方法区
在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
在方法区中有一个非常重要的部分就是运行时常量池,常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如:int、long等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用(#类和接口的全限定名#字段的名称的描述符#方法和名称的描述符).虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集合,包括直接常量(string,integer和floating常量)和对其他类型,字段和方法的符号引用。
方法区和持久代的关系如下:
“持久代”仅仅是HotSpot存在的一个概念,并且将其置于方法区,JRocket与IBM的VM都不存在这个“持久代”,最新的HotSpot也计划将其移除。在已经发布的Oracle JDK7 RC(JDK7 build 147)里,HotSpot VM仍然有PermGen,但许多原本存储在PermGen里的东西已经挪到了别的地方。 方法区物理上存在于堆里,而且是在堆的持久代里面;但在逻辑上,方法区和堆是独立的。
六、堆区
堆内存由年轻代和老年代组成,其中年轻代又分为一个Eden区和两个Survivor区(使用复制收集算法);所有新创建的Object 一般都会存储在新生代中,如果新生代数据在一次或多次GC后存活下来,那么将被转移到Old Generation中。
新建的对象也有可能在老年代上直接分配内存,这主要有两种情况:一种为大对象,可以通过启动参数设置-XX:PretenureSizeThreshold=1024
,表示超过多大时就不在年轻代分配,而是直接在老年代分配,此参数在年轻代采用Parallel Scavenge GC时无效,因为其会根据运行情况自己决定什么对象直接在老年代上分配内存;另一种为大的数组对象,且数组对象中无引用外部对象。
当老年代满了就需要对老年代进行回收,老年代的垃圾回收称为Full GC。
对象访问会涉及到Java栈、Java堆、方法区这三个内存区域,Object obj = new Object(); Object obj 这部分将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。而“new Object()”这部分将会反映到Java堆中,形成一块存储Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定。另外,在java堆中还必须包括能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些数据类型存储在方法区中。根据reference去访问实例对象有两种访问方式:句柄访问方式、指针访问方式。
内存泄露:指程序中一些对象不会被GC所回收,它始终占用内存,即被分配的对象引用链可达但已无用。(可用内存减少)。内存溢出:程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。内存泄露是内存溢出的一种诱因,不是唯一因素。
-Xms参数和-Xmx参数可以控制堆内存,默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。
JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
在GC算法进行垃圾回收时首先要进行对象存活性判断,一般有两种方法:引用计数、可达性分析(在java语言中可作为GC Roots进行可达性分析的对象包括:虚拟机栈中引用的对象,方法区中类静态属性实体引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。)。垃圾收集算法分:标记清除算法、复制算法、标记整理算法;JVM的收集器采用分代收集,分代收集是在不同的区使用上面三种算法,以及是否可并行收集。
七、总结
Java内存划分为几个区?
根据虚拟机规范一般划分为五个区域: 程序计数器、方法区、虚拟机栈、本地方法栈、堆;其中程序计数器类似于计算机组成原理的PC寄存器,主要作用是用来标记程序正在运行的字节码地址;方法区主要每个类的基本信息,运行时常量池,编译后的代码等;栈区主要是存方法调用的栈帧,每个栈帧下又存储很多信息,局部变量表返回地址等;堆区就是所谓的GC堆,一般分为新生代、老年代,新生代又分为Eden区和Survivor区;PS:如果对GC算法比较熟悉,在面试中可以着重强调这一部分,说的多一些,可能面试官就会顺着往下问GC算法有哪些。
参考地址:
http://wiki.jikexueyuan.com/project/java-vm/storage.html