java虚拟机在执行java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。这些区域都有各自的用途和建立、销毁时间,有的区域伴随虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而创建和销毁。java
程序计数器(Program Counter Register)是一块较小的内存空间,它能够当作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工做就是经过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都依赖与这个计数器完成。程序员
java虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式实现的,因此在任什么时候刻一个处理器只能执行一条线程的指令。所以,为了线程切换以后能恢复到正确的执行位置,每条线程都须要一个独立的程序计数器,各条线程之间程序计数器互不影响,独立存储。这类内存区域被称为“线程私有”内存。算法
若是线程正在执行的是java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址。若是线程正在执行的是Native方法,计数器值为空。此区域是java虚拟机规范中惟一一个没有规定任何“OutOfMemoryError”状况的区域。api
与程序计数器同样,java虚拟机栈也是线程私有的,他的声明周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每一个方法执行的同时都会建立一个栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每一个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机中从入栈到出栈的过程。数组
局部变量表存放了编译期可知的各类基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型。其中long和double数据会占用两个局部变量空间,其余的占用一个局部变量空间。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时须要在栈中分配多大的局部变量空间是彻底肯定的,在方法运行期间不会改变局部变量表的大小。安全
在java虚拟机规范中对虚拟机栈规定了两种异常情况:若是线程请求深度大于虚拟机运行的请求深度,抛出StackOverflowError异常;若是虚拟栈能够扩展,当扩展时没法申请到足够的内存空间,抛出OutOfMemoryError异常。多线程
本地方法栈与java虚拟栈很是类似,他们之间的区别只不过是java虚拟机栈是为虚拟机执行java方法服务的,而本地方法栈是为虚拟机执行Native方法服务的。ide
对于大多数应用来讲,java堆是java虚拟机所管理的内存中最大的一块。java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。次内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分片内存。函数
java堆也是垃圾收集器管理的主要内存区域,因为如今的收集器基本都采用分代收集算法,因此java堆能够再划分为新生代和老年代;再细致一点能够划分为Eden空间、From Survivor空间、To Survivor空间等。oop
在实现时java堆既能够是固定大小的内存区域,也能够是可扩展的,当前主流的虚拟机都是可扩展的(经过-Xmx和-Xms控制)。若是堆中内存用完,且堆也没法再扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area)与java堆同样,也是一个各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
不一样的虚拟机在这块的实现不同。
在java虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分。主要用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。java语言并不要求常量必定只有在编译期才能产生,也就是说并不是预置入Class文件中的常量池内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。这种特性被开发人员利用的比较多的是String的intern()方法。
当运行时常量池在没法申请到内存时也会抛出OutOfMemoryError异常。
直接内存(Direct Memory)并非虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。可是这块内存也被频繁的使用,并且也可能致使OutOfMemoryError异常出现。
被使用的例子:NIO类引入了一种基于通道与缓冲区的I/O方式。它可使用Native函数库直接分配堆外内存,而后经过存储在java堆中的一个DirectByteBuffer对象做为这块内存的引用进行操做。这样就避免了java堆和Native堆之间来回复制数据。
以HotSpot虚拟机为例介绍java堆内存对象的建立、内存布局以及访问定位。
在java语言中对象的建立一般是经过new关键字进行建立,但在虚拟机中时一个什么过程?
1.虚拟机接受到new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用表明的类是否已被加载、解析和初始化。(若是没有先进行类加载)
2.虚拟机将为新对象分配内存,在类加载以后已经肯定对象须要的内存大小。这个过程等同于从堆内存中为对象划分出一块肯定大小的内存空间。
分配方式
指针碰撞:若是java堆中内存时绝对规整的,全部用过的内存存放在一边,空闲内存在另外一边,中间放着一个指针做为分界点指示器。那分配内存就是把指针向空闲内存方向移动对象大小相等的距离。
空闲列表:若是java堆中内存并不规整,虚拟机就必须维护一个列表,在列表上记录那些内存时可用的,在分配的时候就从列表中找一块足够大的空间分给对象实例,并更新列表上的记录。
分配时线程安全问题也有两种方案
一种是在分配内存空间的动做进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操做的原子性,
另外一种是为每一个线程预先在java堆中分配一小块内存空间(线程分配缓冲)哪一个线程须要分配内存就在该线程事先分好的内存空间进行。
3.内存分配完成以后,虚拟机须要将分配到的内存空间都初始化为零值。
4.虚拟机对对象进行必要的设置,会在对象头中设置例如该对象是哪一个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
5.对象进行初始化,通常会执行<init>方法按照程序员的意愿为对象初始化。这样一个真正可用的对象才算建立出来。
在HotSpot虚拟机中对象在内存中的布局能够划分为3块区域:对象头、实例数据、对齐填充。
对象头:对象头又分为两部分信息,第一部分存储对象自身运行时数据如哈希码、GC分代年龄、所状态标志、线程所持有的锁、偏向线程ID、偏向时间戳等;第二部分是类型指针,对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。(不是全部虚拟机实现都须要保留类型指针,固然若是是数组类型在对象头中还要有一块用于存储数组长度)。
实例数据:对象真正存储的有效信息,程序代码中定义的各类类型字段的内容。
对齐填充:这部分并非必然存在的,由于HotSpot虚拟机要去对象起始地址必须是8字节的整数倍,换句话说也就是对象大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数,所以当对象实例数据内容没有对齐时,就须要经过对齐填充来补全。
创建对象是为了使用对象,java程序须要经过栈上的reference数据来操做堆上具体对象。对象的访问方式根据虚拟机实现主要有两种:使用句柄访问和指针访问。
句柄访问:若是使用句柄访问那么java堆中就必须划分出一块内存来做为句柄池,reference中存储的就是句柄池中对象句柄的地址。而句柄中就要保存对象实例数据地址和对象类型数据地址。
指针访问:若是使用直接指针访问,在java堆中对象的内存布局就要考虑如何放置对象类型数据的相关信息。
使用句柄的优点就是reference中存储的是稳定的句柄地址,在对象被移动是只会改变句柄中实例数据指针,而reference自己不须要修改。
使用直接指针访问方式的好处就是速度更快,它节省了一次指针定位的时间开销。
java堆用于存储对象实例,只要不断建立对象实例,而且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制以后就会出现内存溢出的异常。
咱们能够经过实例来演示:首先限制java堆内存大小,不可扩展而后不停建立对象。
虚拟机运行参数配置:
Dfile.encoding=UTF-8 -verbose.gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\data -XX:SurvivorRatio=8
代码:
public class VmTest { static class OOMObject{ } 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 D:\data\java_pid8168.hprof ... Heap dump file created [28070894 bytes in 0.145 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 com.sean.esapi.client.VmTest.main(VmTest.java:15)
java堆的内存OOM异常是实际应用中常见的内存溢出异常状况。当出现java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
要解决这个区的异常主要就是确认内存中的对象是否必要的,也就是要分清楚是内存泄露仍是内存溢出。若是是内存泄露就要找到泄露对象到GC Roots的引用链,因而就能找到泄露对象是经过怎样的路径与GC Roots相关联并致使垃圾收集器没法回收他们的。
若是不存内存泄露就要一方面根据物理机内存对比看看是否能够扩充java堆内存空间。另外一方面从代码层面看看是否有对象生命周期过长或持有状态时间过长的状况,尝试减小程序运行期的内存消耗。
因为在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,所以对于HotSpot来讲虽然-Xoss参数(设置本地方法栈大小)存在但实际上无效。虚拟机提供了为单个线程设置栈容量的参数设置-Xss。
java虚拟机规范中描述有两种异常:
StackOverflowError:若是线程请求的栈深度大于虚拟机容许的最大深度,抛出StackOverflowError异常。
OutOfMemoryError:若是虚拟机在扩展栈时没法申请到足够的内存空间,抛出OutOfMemoryError异常。
在单线程模式下经过递归方法测试设置-Xss参数减少栈内存的容量,出现StackOverflowError异常,减少-Xss参数以后再次运行发现出现StackOverflowError异常时递归的深度变小。说明当栈内存缩小时虚拟机容许的最大深度相应缩小。
设置:
-Xss128k
运行:
public class VMStackTest { private int stackLength = 1; public void stackLeak() { stackLength ++; System.out.println(stackLength); stackLeak(); } public static void main(String[] args) { VMStackTest vmst = new VMStackTest(); try { vmst.stackLeak(); } catch (Exception e) { e.printStackTrace(); } } }
结果:
1 ... 974 975 976 977 Exception in thread "main" java.lang.StackOverflowError at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77) at sun.nio.cs.UTF_8.access$200(UTF_8.java:57) at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636) at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691) at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579) at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271) at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125) at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207) at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129) at java.io.PrintStream.write(PrintStream.java:526) at java.io.PrintStream.print(PrintStream.java:597) at java.io.PrintStream.println(PrintStream.java:736)
从新设置-Xss参数:
-Xss512k
一样运行上面代码
结果:
1 ... 5064 5065 5066 5067 Exception in thread "main" java.lang.StackOverflowError at java.io.FileOutputStream.write(FileOutputStream.java:326) at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82) at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140) at java.io.PrintStream.write(PrintStream.java:482) at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221) at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291) at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104) at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185) at java.io.PrintStream.write(PrintStream.java:527) at java.io.PrintStream.print(PrintStream.java:597)
当定义大量的本地变量,增大方法帧中本地变量表的长度,在较小的调用深度就会出现抛出StackOverflowError异常。
JVM未提供设置整个虚拟机栈占用内存的配置参数。虚拟机栈的最大内存大体上等于“JVM进程能占用的最大内存(依赖于具体操做系统) - 最大堆内存 - 最大方法区内存 - 程序计数器内存(能够忽略不计) - JVM进程自己消耗内存”。当虚拟机栈可以使用的最大内存被耗尽后,便会抛出OutOfMemoryError,能够经过不断开启新的线程来模拟这种异常(这种方式容易耗尽操做系统资源致使宕机)
配置:
-Xss128k
运行以下代码没有获得OutOfMemoryError异常:
public class VMStackTest { public void stackLeak() { String name; while (true) { name = "nihao"; try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void stackLeakByThread(){ int count = 0; while (true) { count ++; Thread t = new Thread(new Runnable(){ @Override public void run() { stackLeak(); } }); t.start(); System.out.println(count); } } public static void main(String[] args) { VMStackTest vmst = new VMStackTest(); vmst.stackLeakByThread(); } }
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字符描述、方法描述等。对于这些区域的测试,基本思路就是运行时产生大量的类去填满方法区,直到溢出。这里经过CGLib直接操做字节码运行时生成了大量的动态类。
注意:JVM8中把运行时常量池、静态变量也移到堆区进行存储。方法区方法区主要是存储类的元数据的,如虚拟机加载的类信息、编译后的代码等。JDK8以前方法区的实现是被称为一种“永久代”的区域,这部分区域使用JVM内存,可是JDK8的时候便移除了“永久代(Per Gen)”,转而使用“元空间(MetaSpace)”的实现,并且很大的不一样就是元空间不在共用JVM内存,而是使用的系统内存。
DirectMemory容量可经过-XX:MaxDirectMemorySize指定,若是不指定,则默认为与java堆最大值(-Xmx指定)同样,咱们测试时直接越过DirectByteBuffer类,直接经过反射获取Unsafe实例进行内存分配。由于虽然使用DirectByteBuffer分配内存也会出现内存溢出异常,但它抛出异常时并无去向操做系统申请内存,而是经过计算得知内存没法分配手动抛出内存溢出异常。真正申请分配内存的方法是unsafe.allocateMemory()。
配置:
-Xmx20M -XX:MaxDirectMemorySize=10M
运行:
public class DirectMeoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { //经过反射获取到unsafe实例,再经过unsafe实例申请内存 Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe)unsafeField.get(null); while(true){ unsafe.allocateMemory(_1MB); } } }
执行结果:
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at com.sean.esapi.client.DirectMeoryOOM.main(DirectMeoryOOM.java:16)
由DirectMemory致使内存溢出的一个明显特征就是在Heap Dump文件中不会看到明显的异常,若是发现OOM以后Dump文件有很小,程序直接或间接又使用了NIO,能够考虑这方面缘由。