(一)学习JVM ——运行时数据区域java
JVM是整个Java平台的基石,是Java实现与硬件无关与操做系统无关的关键部分,是Java生成出极小体积的编译代码的运行平台,是保障用户机器免于恶意代码损害的屏障。——《Java虚拟机规范》数据结构
JVM在执行Java程序时,会把它所管理的内存氛围几个不一样的区域。其中有一些区域会随着JVM启动而建立,随着JVM退出而销毁。另一些则是与线程一一对应的,这些与线程对应的数据区域随着线程的开始和结束而建立和销毁。less
程序计数器(Program Counter),也可成为PC寄存器,它是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。eclipse
每一条JVM线程都有本身的PC寄存器。在虚拟机的概念模型里,字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令的。ide
任意时刻,一条JVM线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法(current method)。多个线程则是经过线程轮流切换并分配处理器执行时间的方式来实现的。函数
若是当前方法不是本地方法,那么PC寄存器中就保存JVM正在执行的字节码指令的地址,若是该方法是本地方法,那PC寄存器的值就是undefined。PC寄存器的容量至少应当能保存一个returnAddress类型的数据,或者保存一个与平台相关的本地指针的值。工具
该内存区域是惟一一个在JVM规范中没有规定任何OutOfMemoryError状况的区域。
与程序计数器同样,JVM栈也是线程私有的,它的生命周期与JVM线程相同。每一条JVM线程都有本身私有的JVM栈(Java Virtual Machine stack),这个栈与线程同时建立,用于存储栈帧(Stack Frame)。
每一个方法在执行的同时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态链接、方法出口等信息。每个方法从调用知道执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
除了栈帧入栈和出栈,JVM栈不会在受其余因素的影响,因此栈帧能够在堆中分配,JVM栈所使用的内存不须要保证是连续的。
JVM规范既容许JVM栈被设置为固定大小,也容许根据计算动态扩展和收缩。若是采用固定大小,那么每个线程的JVM栈容量能够在线程建立的时候独立选定。
JVM栈可能发生的异常状况有两种:若是线程请求分配的栈容量超过JVM栈容许的最大容量,会抛出:StackOverflowError异常。若是JVM栈能够动态扩展,而且在尝试扩展的时候没法申请到足够的内存,或者在建立新的线程的时候没有足够的内存去建立对应的JVM栈,会抛出:OutOfMemoryError异常。
JVM提供了-Xss参数,对于设置JVM栈的最大值,下面的代码会产生StackOverflowError。
package lesson1.test; public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } // -verbose: -Xss128k public static void main(String[] args) { JavaVMStackSOF sof = new JavaVMStackSOF(); try { sof.stackLeak(); } catch (Throwable e) { System.out.println("stack length: " + sof.stackLength); throw e; } } }
上面代码设置-Xss的最大值为128k后,运行程序,结果返回:
tack length: 988 Exception in thread "main" java.lang.StackOverflowError at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8) at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9) at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9) at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9) at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9) at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9) at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9) ……后续异常堆栈信息省略
实验结果代表:在单个线程下,不管是因为栈帧太大仍是JVM栈容量过小,当内存没法分配时,会抛出StackOverflowError异常。
本地方法栈(Native Method Stack)与JVM栈所发挥的做用是很是类似的,不一样之处在于JVM栈执行Java方法,而本地方法栈执行Native方法。例如,当JVM虚拟机使用其余语言(好比C语言)来实现执行的解释器时,就可使用本地方法栈。
JVM规范容许本地方法栈实现成固定大小或者根据计算动态扩展和收缩。若是采用固定大小,那么每个线程的本地方法栈容量能够在建立栈的时候独立选定。
本地方法栈可能出现的异常状况有两种,若是线程请求分配的栈容量超过本地方法栈容许的最大容量,JVM会抛出一个StackOverflowError异常。若是本地方法栈能够动态扩展,而且在尝试扩展的时候没法申请到足够的内存,或者在建立新的线城时没有足够的内存区建立对应的本地方法栈,JVM会抛出一个OutOfMemoryError异常。
HotSpot虚拟机并不区分本地方法栈和JVM栈,所以,对于HotSpot来讲,虽然-Xoss参数存在,可是无效,占容量只由-Xss参数设置。
堆(Heap)是可供各个线程共享的运行时内存区域,也是供全部类实例和数组对象分配内存的区域。对于大多数应用来讲,堆是JVM所管理的内存中最大的一块。堆在JVM启动的时候就被建立了,它存储了被自动内存管理系统(automatic storage management system),也就是垃圾回收器(GC)所管理的各类对象,这些受到管理的对象无需也没法显示地销毁。
堆能够处于物理上不连续的内存中,只要逻辑上是连续便可,就像磁盘空间同样。堆的容量能够是固定的,也能够随着程序执行的需求动态扩展,并在不须要过多空间时自动收缩。
堆可能发生的异常状况是,若是实际所需的堆超过了自动内存管理系统能提供的最大容量,那JVM将抛出一个OutOfMemoryError异常。
JVM提供了参数-Xmx设置堆的最大空间,-Xms设置堆的最小值。
import java.util.ArrayList; import java.util.List; public class HeapOOM { static class OOMObject { } // -verbose: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); while (true) { list.add(new OOMObject()); } } }
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid6252.hprof ... Heap dump file created [27974147 bytes in 0.083 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at cn.net.bysoft.lesson1.HeapOOM.main(HeapOOM.java:15)
上面代码将堆的最小值Xms设置为20M,最大值Xmx也设置为20M,即为不扩展堆大小。经过参数-XX:HeapDumpOnOutOfMemoryError可让JVM在内存溢出时Dump出当前的内存堆快照,以便分析。
可使用Eclipse Memory Analyzer打开快照, http://archive.eclipse.org/mat/1.4/update-site/
分析若是是内存泄漏,可进一步经过工具查看泄露对象到GC Roots的引用链,若是不存在泄漏,就是内存中的对象都存活着,那就须要修改JVM的堆参数。或者从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的状况。
方法区(Method Area)是可供各个线程共享的运行时内存区域,它与传统语言中的编译代码存储区(storage area for compiled code)或者操做系统的正文段(text segment)的做用很是的相似,它存储了每个类的结构信息,例如,运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括类、实例、接口初始化时用到的特殊方法。
方法区是堆的逻辑组成部分,它有一个别名叫作Non-Heap(非堆),目的是与堆区分开来。
方法区的容量能够是固定的,也能够随着程序执行的需求动态扩展,并在不须要过多空间的时候自动收缩。方法区在实际内存中能够是不连续的。
方法区可能发生一个异常,若是方法区的内存空间不能知足内存分配需求,那么JVM将抛出一个OutOfMemoryError异常。
JVM提供了参数-PermSize设置方法区的最小值,-MaxPermSize设置方法区的最大值。
import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class RuntimeConstantPoolOOM { // -XX:PermSize=10M -XX:MaxPermSize=10M public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable { return arg3.invoke(arg0, arg2); } }); } } static class OOMObject { } }
上面代码借助CGLib直接操做字节码,运行时生成大量的动态类来使方法区OOM。
方法区用于存放Class的相关信息,如类名称、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的作法就是运行时产生大量的类去填满方法区,直到溢出。
上述代码在JDK1.8后就无效了,由于JDK1.8彻底移除来永久带,取而代之的是Metaspace(元数据空间),一样的,它也提供了几个参数来设置其大小, -XX:MetaspaceSize 初始空间大小 , -XX:MaxMetaspaceSize 最大空间, -XX:MinMetaspaceFreeRatio 在GC以后,最小的Metaspace剩余空间容量的百分比 , -XX:MaxMetaspaceFreeRatio 在GC以后,最大的Metaspace剩余空间容量的百分比 。
JDK8测试Metaspace溢出的代码以下:
import java.util.ArrayList; import java.util.List; public class RuntimeConstantPoolOOM { static String base = "string"; //-XX:MetaspaceSize=1M //-XX:MaxMetaspaceSize=1M public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < Integer.MAX_VALUE; i++) { String str = base + base; base = str; list.add(str.intern()); } } }
OutOfMemoryError: Metaspace
运行时常量池(runtime constant pool)是方法区的一部分,是class文件中每个类或接口的常量池表(constant_pool table)的运行时表示形式,它包括了若干种不一样的常量,从编译期可知的数值字面量到必须在运行期解析后才能得到的方法或字段的引用。它相似于传统语言中的符号表(symbol table),不过它存储数据的范围更普遍。
每一个运行时常量池都在JVM的方法区中分配,在加载类和接口到JVM后,就建立对应的运行时常量池。
建立运行时常量池时可能会发生一个异常,若是构造运行时常量池所需的内存超过了方法区所能提供的最大值,那么JVM将会抛出一个OutOfMemoryError异常。
在语言层面上,建立对象(例如克隆,反序列化)一般仅仅经过一个new关键字而已,但在JVM中,对象的建立却更加细致。
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已被加载、解析和初始化过。若是没有,那必须先执行相应的类加载过程(类加载需单独说明)。
在类加载检查经过后,JVM将为新对象分配内存。对象所需的内存大小在类加载后即可彻底肯定,为对象分配空间的任务等同于把一块肯定大小的内存从堆中划分出来。
划份内存的方式通常有两种:
指针碰撞(Bump the Pointer):假设堆中的内存是绝对规整的,全部用过的内存都放在一边,空闲的内存放在一边,中间放着一个指针做为分界点的指示器,那所分配内存就仅仅是把分界点指针往空闲空间端,移动一段与对象大小相等的距离。
空闲列表(Free List):若是堆中的内存并非规整的,已使用的内存和空闲内存相互交错,JVM就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表上的记录。
选择哪一种分配方式取决于堆内存是否规整,而堆内存是否规整由取决与采用的GC是否带有压缩整理功能。
除此以外,分配内存的过程也不是线程安全的,解决这个问题有两种方案:
一种是堆分配内存的空间的动做进行同步处理,实际上JVM采用CAS配上失败重试的方式保证更新操做的原子性;
另外一种是把内存分配的动做按照线程划分在不一样的空间中进行,即每一个线程在堆中预先分配一小块内存,成为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。
JVM是否采用TLAB,能够经过-XX:+/-UseTLAB参数来设定。
分配好内存空间后,JVM要堆对象进行必要的设置,例如这个对象是那个类的实例、如何采用找到类的元数据信息、对象的HashCode、对象的GC分代年龄等信息。这些都在对象的Object Header中。
从JVM的视角看,一个新的对象已经产生了,但从Java程序的视角看,对象建立才刚刚开始,<init>方法尚未执行,全部的字段仍是默认零值。
在HotSpot JVM中,对象在内存中存储的布局有3块区域:
对象头(Object Header);
实例数据(Instance Data);
对齐填充(Padding);
HotSpot JVM的对象头包括两部分信息:
第一部分用于存储对象自身的运行时数据,例如HashCode、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等。着部分数据的长度在32位和64位的JVM中分别为32bit和64bit,官方称之为Mark Word;
例如,在32位的JVM中,若是对象处于未锁定状态下,那么Mark Word的32bit中,25bit用于存储对象HashCode,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0。
第二部分是类型指针,即对象指向它的类元数据的指针,JVM经过这个指针来肯定这个对象是哪一个类的实例。若是对象是一个Java数组,那么在对象头中还必须有一块用于记录数组长度的数据,由于JVM能够经过普通Java对象的元数据信息肯定Java对象的大小,可是从数组的元数据中却没法肯定其大小。
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各类类型的字段内容。不管是从父类继承下来的,仍是在子类中定义的,都须要记录起来。着部分的存储顺序会受到JVM分配策略参数和字段在Java源码中定义顺序的影响。
HotSpot默认分配为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),宽度相同的字段老是被分配到一块儿。知足这个前提下,父类中定义的变量会出如今子类以前。若是CompactFields参数值为true(default),那么子类之中较窄的变量也能够会加入到父类变量的空隙。
第三部分对齐填充并非必然的,也没有特别的含义,它仅仅起到占位符的做用。因为HotSpot自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象大小必须是8字节的倍数。而对象头部分正好是8字节的倍数,所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。
Java程序须要经过栈上的reference数据来操做堆上的具体对象。目前主流的访问方式有两种:
句柄访问:堆中会划分出一块内存来做为句柄池,reference中存储的是对象的句柄地址,而句柄中包含来对象实例数据与类型数据各自的具体地址信息;
直接指针访问:堆对象的布局中,必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址;
两种对象访问方式各有优点,句柄的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只要改变句柄中的实例数据指针,二reference不须要改变。
使用直接指针访问的好处就是速度够快,节省来一次指针定位的时间开销,频繁的访问对象,指针定位时间聚沙成塔后也是很是可观的。
栈中是用栈帧(frame)来存储数据和部分过程结果的数据结构,同时也用来处理动态链接(dynamic linking)、方法返回值和异常分派(dispatch exception)。
它随着方法调用而建立,随着方法结束而销毁——不管方法是正常结束仍是异常结束都算做方法结束。栈帧的存储空间由建立它的线程分配在JVM栈之中,每个栈帧都有本身的本地变量表(local variable)、操做数栈(operand stack)和指向当前方法所属的类的运行时常量池的引用。
本地方法表和操做数栈的容量在编译期肯定,并经过相关方法的code属性保存及提供给栈帧使用。
某条线程执行过程当中的某个时间点上,只有目前正在执行的那个方法的栈帧的活动的,称为当前栈帧(current frame),对应的方法称为当前方法(current method),对应的类成为当前类(current class)。若是当前方法调用了其余方法,或者当前方法执行结束,那么这个方法的栈帧就再也不是当前栈帧了。调用新方法,栈帧会随着建立,并成为新的当前栈帧。返回时,会回传该方法的执行结果给前一个栈帧,而后丢弃当前栈帧,是的前一个栈帧成为当前栈帧。
每一个栈帧内部都包含一组称为局部变量表的变量列表。栈帧中局部变量表的长度由编译期决定,而且存储于类或接口的二进制表示之中。
一个局部变量能够保存一个类型为boolean、byte、char、short、int、float、reference或returnAddress的数据。两个局部变量能够保存一个类型为long或double的数据。
每一个栈帧内部都包含一个成为操做数栈的后进先出(LIFO)栈。栈帧中操做数栈的最大深度由编译期决定。
栈帧在刚刚建立时,操做数栈是空的。操做数栈在每个位置上能够保存一个JVM中定义的任意数据类型的值,包括long和double数据,在操做数栈中的数据必须正确地操做。在任什么时候刻,操做数栈都会有一个栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其余数据类型则占用一个。
每一个栈帧内部都包含一个指向当前方法所在类型的运行时常量池引用,以便对当前方法的代码实现动态链接。在class文件里,一个方法若要调用其余方法,或者访问成员变量,则须要经过符号引用来表示,动态链接的做用就是将这些以符号引用所表示的方法转换为对实际方法的直接引用。
因为对其余类型中的方法和变量进行了晚期绑定(late binding),因此即使那些类发生变化,也不会影响调用他们的方法。
方法调用正常完成是指在方法的执行过程当中,没有抛出任何异常——包括直接从JVM中抛出的异常以及在执行时经过throw语句显示抛出的异常。若是当前方法调用正常完成,它极可能会返回一个值给调用它的方法。
在这种场景下,当前栈帧承担着恢复调用者状态的责任,包括恢复调用者的局部变量表和操做数栈,以及正确递增程序计数器,以跳过刚才执行的方法调用指令等。
方法调用异常完成是指在方法的执行过程当中,某些指令致使了JVM会抛出异常,而且JVM抛出的异常在该方法中没有办法处理,或者在执行过程当中遇到athrow字节码指令并显示抛出异常,同时在该方法内部没有捕获异常。若是方法是异常调用完成的,那必定不会有方法返回值返回给其调用者。
(一)学习JVM ——运行时数据区域