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

115 阅读6分钟


Java

内存区域

Heap

线程公有

存放实例对象

GC

主要管理区域,因此可以更细致的划分为:新生代、老年代

再细致一点划分:

Eden

区、
From Survivor
区、
To Survivor

内存空间:可以物理上不连续、逻辑上连续即可。

Method Area

线程公有

主要存储:类信息、常量、静态变量、编译后的代码

运行时常量池

主要存储:编译期的字面量以及符号引用

具有动态性,即可以在运行时将常量放入池中。

VM Stack

线程私有

主要包括:

局部变量表:存放编译期的各种基本数据类型、对象引用、

returnAddress

类型

操作数栈:每一个元素可以为任意的

java

类型,
32
位数据类型所占容量为
1
64
位数据类型所占容量为
2

动态连接:

class

文件的常量池中有大量的符号引用,这些符号引用有一部分是在类加载阶段或者在第一次使用的时候就转换为直接引用,这部分成为静态解析。另一部分是每一次运行的时候转化为直接引用,这部分即为动态连接。

方法出口:例如

A

方法中调用了
B
方法,
B
方法的返回值压入
A
方法的栈帧中。

Native Method Stack

线程私有

VM Stack

相似,唯一区别在于该栈为
Native
方法服务。

Hot Spot

VM Stack
Native Method Stack
合而为一。

Program Counter Register

线程私有

用于记录线程执行字节码的指令的地址。

Direct Memory

NIO

中使用直接内存,提高效率。

对象创建过程


首先当虚拟机遇到一条

new

指令时,先去检查该符号引用代表的类是否已经完成类加载,若未完成,则执行以下步骤

类加载

为对象分配内存

分配方式:指针碰撞

/

空闲列表

线程安全:

CAS

解决

虚拟机初始化内存空间

虚拟机对对象进行必要的设置

执行完成初始化

对象创建完成

对象内存布局


第一部分用于存储对象自身的运行时数据,如哈希码(

HashCode

)、
GC
分代年龄、锁状态标志、线程持有的锁、偏向线程
ID
、偏向时间戳、对象分代年龄,这部分信息称为“
Mark Word
”;
Mark Word
被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。

第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

如果对象是一个

Java

数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通
Java
对象的元数据信息确定
Java
对象的大小,但是从数组的元数据中无法确定数组的大小。

32

位系统下,存放
Class
指针的空间大小是
4
字节,
Mark Word
空间大小也是
4
字节,因此头部就是
8
字节,如果是数组就需要再加
4
字节表示数组的长度,如下表所示:


64

位系统及
64
JVM
下,开启指针压缩,那么头部存放
Class
指针的空间大小还是
4
字节,而
Mark Word
区域会变大,变成
8
字节,也就是头部最少为
12
字节,如下表所示:

压缩指针:开启指针压缩使用算法开销带来内存节约,

Java

对象都是以
8
字节对齐的,也就是以
8
字节为内存访问的基本单元,那么在地理处理上,就有
3
个位是空闲的,这
3
个位可以用来虚拟,利用
32
位的地址指针原本最多只能寻址
4GB
,但是加上
3
个位的
8
种内部运算,就可以变化出
32GB
的寻址。

对象访问定位

两种方式:

句柄池:引用中存储的是句柄地址,当实例对象移动时,只需要改变句柄对应的指针,不需要改变引用本身,比较稳定。

直接指针:速度快,节省了一次指针定位的开销。

常用指令

invokeinterface

:用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokevirtual

:指令用于调用对象的实例方法,根据对象的实际类型进行分派

invokestatic

:用以调用类方法

invokespecial

:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

invokedynamic

JDK1.7
新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在
JVM
内部,而
invokedynamic
则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断
,
从而达到动态语言的支持。

基于栈的指令集

&&

基于寄存器的指令集

java

采用的是基于栈的指令集,这种指令集依赖操作数栈进行工作

优点:

可移植:由于寄存器是由硬件直接提供的,所以程序如果依赖寄存器不可避免的会受到硬件的约束

程序代码紧凑

编译器实现简单

缺点:

速度慢

指令数量多:完成相同功能所需的指令比寄存器架构多,因为光是入栈、出栈就已经很多指令了

内存访问多:频繁的栈访问意味着频繁的内存访问,而对于处理器来说,内存始终是速度的瓶颈。

Java

内存溢出异常

内存溢出

堆上无内存可完成实例分配且堆无法扩展时:

java.lang.OutOfMemoryError: Java heap space

方法区以及内存的常量池无法满足内存分配需求时:

java.lang.OutOfMemoryError: PermGen space

栈扩展时无法申请足够内存:

java.lang.StackOverflowError

内存泄漏

代码设计引起的程序动态分配的内存没有释放,导致该部分内存不可用

内存溢出与内存泄漏的区别

内存泄漏是导致内存溢出的原因之一,内存泄漏积累起来就会导致内存溢出

内存泄漏可以通过完善代码来解决,内存溢出无法彻底避免,只能通过配置来减少发生的频率

内存泄漏内存溢出的检查

性能监测工具:

JProfiler

Optimizeit Profiler

Eclipse Memory Analyzer

EclipseMAT

JProbe