JVM学习—内存方区域与内存溢出异常


Java与C++中存在的一堵高墙就是又内存动态分配和垃圾回收技术所围成的,墙外面的人想进去,墙里面的人想出去。
Java将内存控制权交给了JVM,所以程序员无需为每个对象手动释放空间,所以不容易出现内存泄露与溢出。但是一旦出现内存泄露或溢出,如果不了解虚拟机怎样分配内存的,那么排查将非常困难。Java虚拟机将执行java程序过程中管理的内存划分成若干个区域,每个区域有各自的用途以及创建和销毁的时间。这些区域有的是随着JVM进程启动而存在,有的则是通过用户线程的启动和结束而建立和销毁。

程序计数器(Program Counter Register)
程序计数器是一块很小的内存区域,可以看做是当前线程执行字节码的行数指示器,字节码在解释过程中通过改变这个计数器的值来选取下一条要执行的指令(分支
、循环、跳转、异常处理等基础功能都是依赖这个计数器完成的),实际是记录的字节码指令的地址。由于Java虚拟机的多线程是通过线程轮流切换来执行的,为了确保每次线程切换后恢复到正确的位置,所以每个线程都应该有自己的程序计数器,各个线程之间的程序计数器互不影响,独立存储。这里内存区域称为“线程私有”的内存。这块区域并没有任何OutOfMemoryError情况。

Java虚拟机栈(Java Virtual Machiner Stacks)
Java虚拟机栈也是线程私有的,与线程的生命周期相同。虚拟机栈是描述Java执行方法的内存模型:每个方法被执行都会在虚拟机栈内创建一个栈帧,每个方法从调用到执行完毕,就对应着栈帧的入栈与出栈。栈帧是用来存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表用来存放编译器可以知道的基本数据类型、引用类型、returnAddress类型。long和double占用两个局部变量空间,其他数据类型占用一个局部变量空间,当进入一个方法的时候局部变量空间是被确定的,方法运行过程中,这个变量表不会被改变。在虚拟机规范中这块区域规定了两种异常情况:如果如果线程请求栈的深度大于虚拟机规定的深度,将抛出StackOverfloweError异常;如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够内存空间,将会抛出OutMemoryError异常。

本地方法栈(Native Method Stack)
其作用与虚拟机栈一样,只不过虚拟机栈是为执行java方法(字节码)服务的,而本地方法栈是为虚拟机用到的Native方法服务的,他的实现比较自由,所以在HotSpot中将本地方法栈和虚拟机栈合二为一。他的异常抛出与虚拟机栈一样。

Java堆(Java Heap)
堆是JVM管理的最大一块内存区域,他是被所有线程所共享的。在虚拟机启动时候创建。Heap的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配(JIT编译器发展与逃逸分析技术成熟)。堆分为新生代和旧生代,新生代又由Eden space和Survivor space组成(Survivor由From space和Tospace组成),对于新创建的对象由新生代分配内存,当Eden区域不足的时候会将存活的对象注意到Survivor中,旧生代中用于存放经过多次GC还存在的对象。同时Heap也是垃圾收集器主要区域,所以也称为GC堆。堆可以在物理内存上不是连续的,只要在逻辑上是连续的即可。他也允许扩展,当内存不够分配时候会抛OutOfMemoryError异常。

方法区(Method Area)
方法区与Heap一样被所有线程所共享,他用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。java虚拟机规范将他划分为堆的一部分。在HotSpot上,方法区也称为“永久代”,这样做的目的就是为了把GC扩展到方法区,这样HotSpot的垃圾回收器就像管理Java堆一样管理方法区(像JRockit、J9JVM并不存在永久代概念),当方法区无法满足内存需求时会抛出OutOfMemoryError。

运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口描述等信息外还有就是常量池,用来存放编译期生成的字面量和符号引用。运行时常量池对与Class常量具备动态性,即不要求一定只有编译期的常量进入运行时常量池,对于运行时新的常量也可以放入常量池的。

直接内存
直接内存并不是虚拟机运行时的内存区域,但这部分内存会被频繁使用,也会导致OutOfMemoryError。显然直接内存不会受到Java堆的大小限制,但是会受到本机内存限制,如果服务器管理员在分配内存过程中忽略掉直接内存,使得总内存大于物理内存会产生OutOfMemoryError。

深入了解对象创建过程(针对HostSpot中堆内存研究)
当我们需要创建一个对象的时候,只需使用new关键词即可,但是JVM中是如何进行创建对象的呢?
虚拟机接收到new指令后,首先会去运行时常量池中定位到这个类的符号,并且检查这个符号代表的类是否被加载、解析和初始化过。如果没有,那么必须执行相应的类加载过程。执行完类加载后,接下来虚拟机需要为这个对象分配内存。对象所需的内存大小,经过类加载后就能被完全确定,为对象分配内存就是在Java堆中为这个对象划分一块内存。有两种划分方式,根据当前这个堆是否是绝对完整的,如果是绝对的完整的(即用过的在一边,没有用过的在另一边,之间使用指针进行划分)为其划分一块内存只需将指针向未使用区划分一块对象所需内存大小区域即可,这个方式称之为“指针碰撞”。如果堆内存不是绝对完整的则需要维护一个列表,这个列表中记录着空闲区域地址,当分配内存的时候只需在列表中找到一块内存即可,这中方式称之为“空闲列表”。堆是否绝对完整,取决于所采用垃圾回收器是否带有压缩整理功能。

需要注意即便移动小小的指针,在并发情况也不是线程安全的。为了解决这个问题,一种是对内存分配采用线程同步处理;另一种方式是在之前现将堆内存区域为每个线程分配一小块内存,这一小块内存称为本地线程分配缓冲(Thread Local Allocation Buffer)TLAB。只有当TLAB用完并重新分配才需要同步锁定(TLAB位于新生代的Eden区域)。所以建议使用第二种,因为第一种在内存分配时候就需要加锁,这样new开销比较大,使用TLAB其性能和C差不多,但是如果创建大对象,超过了为每个线程分配的内存空间,仍然会使用同步锁,这样效率也不是特别高了,所以小对象比大对象在java中更高效。

分配完内存空间之后JVM会为每个内存空间初始化为本类型的零值,这样就可以在java字段没有初始化时也能访问本字段类型的零值了。

接下来JVM对对象进行必要的设置,列如这个对象是哪个类的实例、如何才能找到这个类的元数据信息、对象的哈希码、对象的GC分代。

上面工作工作做完之后,在jvm角度来说已经创建完对象了,但是从java程序来说并没有因为还没有对字段初始化,所有字段现在都是零值呢。所以需要执行<init>方法,这样才算完成对象的初始化。

对象在内存中的布局
在Java堆中为对象分配完内存空间之后,那么这个对象在内存区域中具体存储什么东西呢?
在HotSpot中,主要分为三部分:对象头、实例数据、对齐填充。
对象头中又分为两部分一部分是存储着对象自身运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID等。另一部份是类型指针,即这个对象是哪个类的实例。
实例区域用于存储真正的有效信息,在程序代码中所定义各种类型类型字段内容。无论父类还是子类中定义的,都需要记录起来。
对其填充不是必然存在的,他的作用就是当实例数据区没有和对象头对齐的时候,需要通过对其填充来补充。

对象的访问
建立完对象之后目的就是为了访问对象,访问方式取决于虚拟机的实现。主流的访问方式有句柄访问方式和直接访问方式。HotSpot采用的是直接访问方式,这种访问方式更加迅速。

OutOfMemoryError异常
在JVM运行区域中,处理程序计数器不会出现OOM,其他的区域都有可能出现OOM异常。
java堆溢出
/**
 * jvm args:-Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError
 * @author colin
 *
 */
public class HeapOOM {

	public static void main(String[] args){
		List<HeapOOM> list = new ArrayList<HeapOOM>();
		while(true){
			list.add(new HeapOOM());
		}
	}
}

设置堆中最大最小内存空间为10M,这样就不会内存扩展了

-XX:+HeapDumpOnOutOfMemoryError让虚拟机出现内存异常时能够Dump出当前的内存堆转存储快照,以便事后分析。

运行打印:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid27154.hprof ...
Heap dump file created [12009563 bytes in 0.155 secs]

有这个能够知道是堆内存出现OOM,要解决这个问题需要分析是出现内存泄露还是内存溢出,可以使用Eclipse的MAT插件打开堆转存储快照文件(MAT:Memory Analyzer Tool是基于Heap Dumps来进行分析的)。

如果是内存泄露的话通过工具查看内存泄露对象到GC Roots的引用链,这样就能找到泄露对象是通过怎么样的路径与GC Roots相关联导致垃圾收集器无法自动回收他们,掌握了泄露对象类型信息即GC Roots引用链的信息,就可以比较清除的定位泄露代码位置。

如果是内存溢出的化,就说明内存中的对象还存活着,那么应该设置堆参数-Xmx 与-Xms,与实际物理内存对比看是否可以调整堆大小,从代码上查看某些对象是否生命周期过长、持有状态过长、尝试减少程序运行期的内存消耗。


测试栈,HotSpot中不区分虚拟机栈和本地栈,所以设置本地栈大小的-Xoss不会起作用,使用-Xss设虚拟栈的大小

/**
 * jvm Args:Xss2M		设置栈大小
 * @author colin
 *
 */
public class JavaVMStackSOF{
	private int stackLength = 1;
	
	public void stackLeak(){
		stackLength++;
		stackLeak();
	}
	
	public static void main(String[] args) {
	
		JavaVMStackSOF stack = new JavaVMStackSOF();
		stack.stackLeak();
	}
}

总结一下内存设置参数:

-Xmx10M:设置堆内存最大值

-Xms10M:设置初始分配内存大小(最小值)

-Xss2M:设置栈内存大小

-XX:PermSize=64M:非堆内存大小,默认是物理内存的1/64(永久代初始内存大小)

-XX:MaxPermSize=128M:最大非堆内存大小,默认是物理内存的1/4(永久代最大内存大小)

-XX:MaxDirectMemorySize=10M:直接内存大小,在JNIO中引入

猜你喜欢

转载自blog.csdn.net/colin_yjz/article/details/48101665