Java内存区域与内存溢出异常

程序运行时,存储数据的五个地方:

1)寄存器 这是最快的存储区,位于处理器内部。但是,寄存器数量极其有限,所以寄存器根据需求进行分配。你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象(C和C++允许向编译器建议寄存器的分配方式)。
寄存器是存在在cpu上的。而内存是挂在数据总线的,数据总线就是用来决定传输数据的大小。而就是通过在寄存器上的地址来寻找相应内存。总的来说,寄存器和内存是两个东西,程序是无法来控制寄存器,所以这里了解一下就可以了。主要涉及到运行程序涉及到的就是下面这些栈(stack)、堆(heap)、静态域、常量池。

2)堆栈 即“栈”,位于通用RAM(随机访问存储器)中,但通过堆栈指针可以从处理器那里获得直接支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些Java数据存储于堆栈中—–特别是对象引用,但是Java对象并不存储于其中。
栈中主要存放一些基本类型的变量( int, short, long, byte,float, double, boolean, char )和对象引用。 对象是不会放置在里面的。
3) 一种通用的内存池(位于RAM区),用于存放所有的Java对象,以及动态生成的对象(包括数组)和程序运行时生成的一些数据(包括对象的定义和变量的定义)。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。new一个对象,会自动在堆里进行存储分配。 当然,为这种灵活性必须付出相应的代价:用堆进行存储分配和清理可能比堆栈进行存储分配需要更多的时间。
4)常量存储。常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。
5)非RAM存储 比如流对象和持久化对象。在流对象中,对象转化成字节流,通常被发送给另一台机器。在“持久化对象”中,对象被存放磁盘中。这种存储方式的技巧在于:把对象转化成可以存放在其他媒介上的事物,在需要时,可以恢复成常规的、基于RAM的对象。

其他

数据共享 这个数据共享主要也是由于引用的是地址来决定的,举个例子:char str1=”str1”;char str2=”str1”;这时候再次声明Str2,同时指定两个不同的引用而相同的变量;这时候并不需要重新开辟另外一份内存,只需要两者都指向相同的地址就可以了。这样数据共享带来的就是内存上的节省。
定义和声明 这里需要对这两个动词进行一些说明。因为在平时过程中,我是对这两个概念比较模糊。一说就是定义声明了一个变量。但是事实上确实不一样的。声明就只是定义这个变量的名字,告诉编译器会有这么一个变量。而定义就不同了,定义就是在声明之后对变量进行初始化、设置一个初始值的过程。如:int i;int i=1;就是这个区别。
而在java变量的声明过程中,是不允许没有初始化变量的。
Data segment 这个包括静态域和常量池。
静态域 这个就是咱们存放在对象中的静态变量。
常量池 这个主要是在编译完成后,存放在.class文件中(code segment)。包括一些基本的数据类型和相应的类的接口和声明。换言之就是在编译后,程序中经常使用的不会改变的。例如:基本数据类型(这个是
规定,肯定没法改)。接口的命名:这个你肯定不会闲到改改这个来解闷的。

内存分析,java程序执行的过程,一般变量的内存粗存放过程。


实例:
下面通过分析一个例子来说明java变量是怎么存放在内存中的
Code segment:arraylistlist[]=new arraylist[2];
Arraylist[0]=2;arraylist[1]=3;arraylist[2]=4;
因为list[]是一个变量,这是一个声明我们放到栈中。
而后面每个数组实例化出来的变量,所以放到堆中。
而实实在在存在的变量的值都是常量,所以放在常量池中,也就是上图中的datasegment。

运行时数据区域

Java虚拟机在执行Java程序的过程中,会把所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。下图是Java虚拟机运行时数据区:
Java虚拟机运行时数据区

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
在Java虚拟机中,多线程是通过线程 轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对多核处理器来说是一个内核)都只会执行一条线程中的命令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈(栈)

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
两种异常,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

Java堆

对大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

方法区

Method Area,Non-Heap(非堆)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池

运行时常量池(RuntiMe ConStant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用的,这部分内容将在类加载后进入方法区的运行时常量池中存放。

直接内存(Direct Memory)

NIO类引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以 使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提供性能,因为避免了在Java堆和Native堆中来回复制数据。
服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致动态扩展时出现OutOfMemoryError异常。

在虚拟机中,对象创建对象的过程????
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。从虚拟机的视角来看,一个新的对象已经差生了,但从Java程序的视角来看,对象创建才刚刚开始—方法还没有执行,所有的字段都还为零。 所以,一般来说,执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

0%