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,与机器物理内存对比看是否可以调大,并从代码上检查是否存在对象生命周期过长、持有状态时间过长的情况,减少程序运行期间的内存消耗。