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中引入

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-12-18 00:37:21

JVM学习—内存方区域与内存溢出异常的相关文章

Java内存存放区域与内存溢出异常(一)

**Java内存存放区域与内存溢出异常(一)** Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有着各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖于用户进程的启动和结束而建立和销毁,java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图一. 1.在这里先介绍程序计数器 程序计数器(Program Counter Register)是一块内存较小的内存空间,它的作用可以看作是当 前线程所执行的字节码的

JVM学习笔记(四)------内存调优【转】

转自:http://blog.csdn.net/cutesource/article/details/5907418 版权声明:本文为博主原创文章,未经博主允许不得转载. 首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM. 对JVM内存的系统级的调优主要的目的是减少GC的频率和Fu

JVM学习与问题总结——java内存区域与内存溢出异常

java虚拟机将内存分为哪些区域? 根据Java SE7版本的Java虚拟机规范,虚拟机管理的内存包括5个运行时数据区域: 程序计数器 虚拟机栈 本地方法栈 方法区 堆 运行时数据区各部分的作用? 程序计数器 一个线程所执行的字节码的行号指示器. 字节码解释器会通过改变计数器的值来选取下一条将要执行的指令,那么分支.循环.跳转.异常处理.线程恢复都需要依赖计数器来完成.而Java虚拟机当中的多线程是通过争取CPU时间片来切换线程执行任务,当一个线程重新获取CPU时间片的时候就需要恢复上一次任务执

JVM学习笔记(三)------内存管理和垃圾回收【转】

转自:http://blog.csdn.net/cutesource/article/details/5906705 版权声明:本文为博主原创文章,未经博主允许不得转载. JVM内存组成结构 JVM栈由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Space和To Space组成,结构图

Java内存溢出异常(上)

上一篇文章我们讲了JVM运行时数据区域与内存溢出异常,其中对于内存溢出异常这部分将的不够详细,这篇文章将着重讲解Java内存溢出异常的相关知识.如果有没看过上一篇文章的小伙伴们,请点击Java内存区域与内存溢出异常. Java的内存溢出异常主要分为两类:分别是内存溢出和栈溢出.在以下几种情况,会抛出内存异常:Java堆溢出.虚拟机栈和本地方法栈溢出.方法区和运行时常量池溢出.以及本机直接内存溢出,下面讲一一介绍这几类异常. Java堆溢出 在Java内存区域与内存溢出异常中讲过,Java堆主要是

JVM学习系列(一) JAVA内存区域和内存溢出异常

JAVA内存区域介绍 程序计数器: 线程私有,很小的内存空间,可以看做是当前线程所执行的字节码的行号指示器: 每个线程都有一个独立的程序计数器,各个线程之间的计数器相互不影响,独立存储: 如果线程执行的是Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果是一个Native方法,那么这个计数器的值则为undefined: 该内存区域不会发生任何的OutOfMemoryError的情况(JAVA虚拟机规范中未规范). goto 保留字(Java当前版本暂且不用,也不让别人用),

JVM高级特性与实践(一):Java内存区域 与 内存溢出异常

对于从事C.C++的开发人员而言,在内存管理领域,他们具有绝对的“权利”——拥有每个对象的控制权,并担负着每个对象生命周期的维护责任.而对于Java开发人员而言,在虚拟机自动内存管理机制的帮助下,无需为每一个创建new操作去配对 delete/free 代码,减少内存泄漏和内存溢出的问题,这些都交给了Java虚拟机去进行内存控制,但是正因如此,当出现相关问题时,若不了解JVM使用内存规则,就难以排查错误.接下来以此篇文章记录学习Java虚拟机内存各个区域概念.作用.服务对象以及可能产生的问题.

深入理解JVM读书笔记一: Java内存区域与内存溢出异常

Java虚拟机管理的内存包括几个运行时数据内存:方法区.虚拟机栈.本地方法栈.堆.程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区. 2.2 运行时数据区域 2.2.1程序计数器 程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器.字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支.循环.跳转.异常处理.线程恢复等基础功能都需要依赖这个计数器来完成.如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚

深入理解jvm之内存区域与内存溢出

文章目录 1. Java内存区域与内存溢出异常 1.1. 运行时数据区域 1.1.1. 程序计数器 1.1.2. java虚拟机栈 1.1.3. 本地方法栈 1.1.4. Java堆(Java Heap) 1.1.5. 方法区 1.1.6. 运行时常量池 1.1.7. 直接内存 1.2. HotSpot虚拟机 1.2.1. 对象的创建 1.2.2. 对象的访问定位 1.3. OOM异常的解决思路 1.4. 参考 Java内存区域与内存溢出异常 运行时数据区域 程序计数器 当前线程所执行的字节码的