JVM自动内存管理机制总结

2.2运行时数据区域的布局

1. 程序计数器:当前线程所执行的字节码的行号指示器(选取下一条需要执行的字节码指令);

2. 虚拟机栈:存储局部变量表、操作数栈、动态链接、方法出口等信息;

3. 本地方法栈:与虚拟机栈作用类似,但虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为要用到的Native方法服务;

4. 堆:存放对象实例,它可以处于物理不连续的内存空间中,只要逻辑上连续;

5. 方法区(有些人习惯把它叫做“永久带”):存放虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码。其中,运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,它受方法区内存的限制,当常量池无法申请到内存时会抛出OOM异常。

(PS:直接内存:它并不是运行时数据区的一部分,它的分配不受Java堆大小的限制,但受到本机总内存的限制,在配置虚拟机参数时不可忽略直接内存,导致动态扩展时出现OOM异常。)

2.3创建对象的过程

当虚拟机遇到一条new指令时:

1. 检查该指令的参数是否能在常量池中定位到一个类的符号引用,检查这个类是否被加载,如果没有,先进行类加载;

2. 分配内存(把一块确定大小的内存从Java堆中划分出来)。两种分配方式:

1)指针碰撞(Java堆的内存规整时使用),用过的内存放一边,空闲的内存放另一边,指针往空闲方向挪动一段与对象大小相等的距离;

2)空闲列表(Java堆的内存不规整),虚拟机维护一个列表,记录哪些内存块是可用的,在分配时找到一块足够大的空间划分给对象实例,并更新列表。

3. 考虑线程安全问题。两种解决方案:

1)对分配内存空间的动作进行同步处理;

2)把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存就在该线程的TLAB上分配。

4. 分配完成之后,把分配到的内存空间初始化为零值(不包括对象头);

5. 设置对象。如设置对象是哪个类的实例、如何找到对象的哈希码、对象的GC分代年龄等。这些信息存放在对象头(Object Header)中;

6. 执行完new指令以后,所有字段还是零,所以接着执行<init>方法,对字段进行初始化赋值。

对象的内存布局,对象在内存中存储的布局可以分为3块区域:

1. 对象头

1)对象自身的运行时数据Mark Word(如哈希码、GC分带 年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向 时间戳等)

2)类型指针,虚拟机通过该指针确定这个对象是哪个类的 实例

2.实例数据

对象真正存储的有效信息,代码中定义的各类型的字段内容,包括父类和子类中定义的。

3. 对齐填充

不是必然存在,也没有特别含义,就是占位符 的作用,因为要求对象的大小必须是8字节的整数倍,对象头部分正好是8字节的1-2倍,当实例数据部分没有对齐时,要通过对齐补充来补全。

如何访问对象

Java程序通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有两种:

1. 使用句柄访问。reference中存的是对象的句柄地址。

2. 使用直接指针访问。reference中存的直接就是对象地址。

句柄访问的好处在于reference中存储的是稳定的句柄地址,在对象被移动时只需要改变句柄中的实例数据指针,reference本身不用修改。直接指针访问的好处在于速度更快,节省了一次指针定位的开销。

2.4 OutOfMemoryError异常

处理Java堆内存问题的简单思路:

出现了java.lang.OutOfMemoryError: Java heap space 的堆内存溢出异常时,

1. 通过内存映像分析工具(如Eclipse Memory Analyzer 或者IDEA的jprofiler等),分析是出现了内存泄漏还是内存溢出。

2. 如果是内存泄漏,进一步通过工具查看泄露对象到GC Roots的引用链,找出GC无法自动回收他们的原因,比较准确地定位出泄露代码的位置。

3. 如果不存在泄漏,即内存中的对象都存活着,那么就检查虚拟机的对参数-Xms和-Xmx,与机器物理内存对比看是否可以调大,并从代码上检查是否存在对象生命周期过长、持有状态时间过长的情况,减少程序运行期间的内存消耗。

猜你喜欢

转载自my.oschina.net/u/3342874/blog/1797275