java存不存在缓存区溢出_Java内存区域和内存异常溢出

本文详细介绍了Java虚拟机(JVM)的运行时数据区,包括方法区、堆、虚拟机栈、本地方法栈和程序计数器等核心概念。探讨了直接内存的使用及其可能导致的异常情况,并解释了对象创建过程、内存布局以及访问方式。
摘要由CSDN通过智能技术生成

java虚拟机概念模型(以下说法都是针对java虚拟机的概念模型,具体虚拟机有不同的体现)

一、java虚拟机运行时数据区(方法区,堆,虚拟机栈、本地方法栈、程序计数器)

37103691b4f77a834495dad04725a571.png

图片来源:https://www.cnblogs.com/fengbs/p/7029013.html

1. 程序计数器(Program Counter Register)(线程私有的,不会发生OutOfMemoryError):当前线程所执行的字节码行号指示器,字节码指示器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等基础功能)

java虚拟机的多线程是通过轮流切换并分配处理器执行时间实现的(每条线程都需要一个独立的程序计数器)

2. java虚拟机栈(Java Virtual Machine Stack)(线程私有的,为java方法服务):java方法执行的内存模型,java每个方法都是创建一个栈帧,方法从调用到执行完成,对应着一个栈帧在虚拟机中的入栈和出栈,

虚拟基栈最主要是局部变量表,存放了编译期可知的基本数据类型(int,shot,double等等),还有对象的引用(reference),局部变量表的空间是在编译器就分配好的,所以在进入一个方法的时候,这个方法在帧中需要的局部变量表的空间是确定的

3. 本地方法栈(Navite Method Stack):功能和java虚栈一样,但是本地方法栈是为虚拟机Native服务,HotSpot虚拟机java虚拟栈和本地方法栈是合二为一的

4. java堆(Java Heap)(所有线程共享):java虚拟机中内存最大的一块,存放所有的对象实例和数组。是垃圾收集器管理的主要区域,也被称为GC堆(Garbage Collected Heap)

收集器基本都使用分代收集算法,所以java堆可以细分为:新生代和老年代,再细一点有Eden区,From Survivor空间、 To Survivor空间。从内存分配的角度看,线程共享的Java堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。

5. 方法区(Method Area)(线程共享的):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

hotSpot一般把这个称谓“永久代”,是因为用永久代来实现方法区,只是为了像管理java堆一样管理这部分内存,但是这个并不是很好,因为比较容易有内存溢出的问题(永久代有-XX:MaxPermSize上限),所以在jdk7的HotSpot已经把原本放在永久代的字符串常量从这里移出去了。

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

运行时常量池相对于Class常量池的另一个重要特征是具备动态性,除了预置入Class文件中常量池的内容能进入运行常量池外,运行期间也可能将新的常量放入池中,这种特性利用的最多的就是String类的intern()方法

二、直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内训区域,但是这部分经常被使用,而且可能导致OutOfMemoryError

在JDK1.4之后引入了NIO(New Input/Output )类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中的来回复制数据。

内存不会受到java堆大小限制,但是既然是内存,肯定会受到本机的总内存(包括RAM和SWAP区或者分页文件)大小以及处理器寻址空间的限制。所以在使用-Xmx等参数分配内存的时候,根据机器内存情况,如果使用到直接内存,记得给人家留点,不然会内存溢出。

在机器上面,每个进程的内存大小是受操作系统限制的,当分配完Java堆内存后,还有一部分是直接内存,直接内存不会像新生代和老年代一样,发现空间不足了(比如有大量NOI操作)之后通知垃圾收集器进行垃圾回收,它只能等待老年代满了之后Full GC,然后“顺便的”帮他清理内存的废弃对象。某则他会一直等到内存溢出异常时,先catch掉,然后再catch中执行System.gc()。要是虚拟机开启了-XX:DisableExplicitGC开关,则直接内存不够会一直抛出异常,并不处理垃圾。

HotSpot虚拟机简单了解

一、对象的创建(Java普通对象,不包含数组和Class对象)

1. 对象创建过程:

第一步:对象对应类加载

虚拟机遇到new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,则执行相应类加载过程

第二步:在Java堆上分配内存

为新生对象分配内存(从Java堆上分一块确定大小内存),需分配内存大小在类加载完毕后已经确认。(具体分配方法参考第二节:对象分配内存方法)

第三步:内存空间初始化为零值

虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作是为了保证对象的实例字段在Java代码中可以不赋值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

第四步:设置对象头信息

虚拟机要对对象进行必要的设置,例如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)中

第五步:执行程序层面的方法

上面的步骤完成,从虚拟机的角度来看,一个新对象已经生成了,但是从java程序的角度来看,对象创建才刚刚开始----方法还没执行,所有字段的值还是0。所以一般执行完new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正的可用对象才算完全产生。

2. 对象分配内存算法

对象分配内存有两种算法,指针碰撞和空闲列表,具体使用哪种是Java堆决定的,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

1)指针碰撞(Bump the Pointer)

Java内存时规整的,用过的内存在一边,不用的在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲的那一边挪动一段与对象大小相等的距离,这种分配方式叫做指针碰撞

2)空闲列表(Free List)

Java内存是不规整的,已使用内存和空闲内存是相互交错的,虚拟机必须维护一个列表,记录上哪块内存时可用的,在分配的时候从列表上找一块足够大的空间划分给对象实例,并更新到列表上的记录,

这种分配方式叫空闲列表

3. 分配内存存在的问题

虚拟机中对象创建时非常频繁的,即使是仅仅修改一个指针指向的位置,在并发情况下也不是线程安全的,比如:正在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。

解决这个问题有两种方式:

1)对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证操作的原子性

2)分配内存的动作按照线程划分在不同的空间之中执行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程TLAB上分配,只有TLAB用完需要重新分配的时,才需要使用同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来决定

二、对象的内存布局

对象分为三部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

1.  对象头

对象头分为两部分:Mark Word、类型指针

如果对象是个Java数组,那对象头中还有一块记录数组长度的数据,因为虚拟机不能从数组的元数据确定数组的大小,如果是普通Java对象,虚拟机可以通过元数据确定对象大小

1)Mark Word

用于存储对象自身的运行数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit。

对象需要的运行时数据很多,其实已经超过了32位、64位Bitmap结构所能记录的成本,但是对象头信息是与对象自身定义的数据无关的额外存放成本。

2)类型指针(并不是所有虚拟机都有类型指针)

对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例

2.实例数据

对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容

3.对齐填充

不是必然存在的,也没什么特别含义,仅有占位符的作用,HotSpot VM的自动内存管理系统要求对象的大小必须是8字节的整数倍,对象头正好是8字节的整数倍(1或2倍),因此对齐填充是用来补全对象实例数据没有对齐的部分

三、对象的访问定位

访问对象是通过栈上的reference数据来操作堆上的具体对象,reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用怎么定位,访问对象堆中的对象的具体位置,所以对象访问的方式是通过虚拟机决定的,主流的访问方式有句柄和直接指针

1. 句柄

Java堆中会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与数据类型数据的具体地址

优势:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象很频繁)时只会改变句柄中的实例数据的指针,而reference本身不需要更改

42b441c989e92d7653aa45c3cae6757d.png图片来源:https://blog.csdn.net/java2000_wl/article/details/8015105

2. 直接指针(HotSpot使用的是这种方式访问对象)

Java堆对象要存放访问类型数据的相关信息,而reference中存储的直接就是对象地址

优势:速度比句柄方式更快,节省了一次指针定位的时间开销,因为对象访问在虚拟机中非常频繁,积少成多后,这也是一项非常可观的执行成本

cf17be9babed3285a13aa01377643a7f.png图片来源:https://blog.csdn.net/java2000_wl/article/details/8015105

异常

发生在虚拟机中常见异常

1.OutOfMemoryError:需要内存时,内存无法扩展,则会抛出此异常

2.StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度(栈的大小),则会抛出此异常

一、Java堆溢出(一般会OutOfMemoryError)

可以通过不停的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后,就会产生内存溢出异常

内存溢出先通过工具确认内存中的对象是否是有必要的,也就是先要分清楚是内存泄露(没必要的),还是内存溢出(必要的)

内存泄露(Memory Leak):

原因:对象有路径和GC Roots相关联导致垃圾收集器无法自动回收他们

解决思路:可以通过工具查看泄露对象到GC Roots的引用链,定位到泄露内存的代码

内存溢出(Memory Overflow):

原因:内存超过设定内存大小

解决思路:确定不是内存泄漏,检查虚拟机的堆参数-Xmx和-Xms,与机器物理内存对比看看是不是还能调大一些,从代码检查是不是有对象生命周期太长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗

二、虚拟机栈和本地方法栈溢出

通常有两种异常

1. StackOverflowError

线程请求的栈深度大于虚拟机所允许的最大深度,会抛出此异常

2. OutOfMemoryError

虚拟机在扩展栈时无法申请到足够的内存空间,则抛出此异常

模拟结果:

单线程的时候模拟不管是减少栈容量还是(定义了大量的本地变量,增大此方法栈本地变量表的长度)都是StackOverflowError

多线程的情况下,通过不断建立线程会出现OutOfMemoryError,这种溢出与栈空间是否足够大没关系,这种情况下为每线程的栈分配的内存越大,反而更容易产生内存溢出

多线程内存溢出的情况如果不能减少线程数或更换64位虚拟机,就只能通过减少最大堆和减少栈容量来换取更多的线程

三、方法区和运行时常量溢出

类太多可能出现方法区的OutOfMemoryError

四、本机直接内存溢出

内存溢出表现:Head Dump文件看不见明显的异常,Dump文件很小,程序中又直接或间接访问了NIO,那可以查一查是不是本机直接内存溢出

常见命令

1. -Xmx :动态扩展虚拟机内存

2.-Xms:动态扩展虚拟机内存,如果与-Xmx 设置成相同值,可以避免堆自动扩展

3.-XX:MaxPermSize:最大的方法区容量

4.-XX:+/-UseTLAB:虚拟机是否使用TLAB

5.-XX:+HeapDumpOnOutOfMemoryError:虚拟机出现内存溢出异常时Dump出当前的内存堆转储快照

6.-Xoss:设置本地方法栈的大小(对HotSpot虚拟机无效,因为HotSpot虚拟机是Java虚拟栈和本地方法栈合二为一的),一般有-Xss设定

7.-Xss:设置每个线程的堆栈大小

8.-XX:PermSize和-XX:MaxPermSize:jdk1.6及之前版本可以通过限制方法区的大小

9.-XX:MaxDirectMemorySize :本机直接内存最大容量,如果不指定,则默认和Java堆最大值(-Xmx)一样

String.intern() :如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用

68f724bad5a33d9a9f6bd2ca7369ecb7.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值