HotSpot虚拟机对象探秘
对象的创建
- 虚拟机遇到new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,检查这个符号引用代表的类是否被加载,解析和初始化。
- 类加载通过后,虚拟机将为对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。(假设Java堆中内存是绝对规整的,使用“指针碰撞”方法,如果不是规则的,就使用“空闲列表”分配方式)
指针碰撞:所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,所分配的内存就仅仅是把那个指针向空闲空间挪动一段与对象大小相等的距离。
空闲列表:虚拟机必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够的空间划分给对象实例,并更新列表上的记录。
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。(这一步保证了对象的实例字段在java代码中可以不赋初值就可直接使用)
- 虚拟机对对象进行必要的设置。(例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息)这些信息存放在对象的对象头。
- 从虚拟机的角度,一个新的对象已经产生,但对于Java程序视角而言,还需要执行init方法。
对象的内存布局
对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头包括两部分信息:
第一部分用于存储对象自身运行时数据(哈希码,GC分代年龄,锁状态标志等)
第二部分是类型指针对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。(如果对象是一个java数组,那在对象头中还必须有一块用于记录数组长度的数据,虚拟机无法从数组的元数据中确定数组大小)
实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
第三部分对齐填充,不是必然存在的,起着占位符的作用。对象的大小必须是8字节的整数倍。(对象头部分是8字节的倍数),因此当实例数据的大小不是8 的整数倍时,就需要用填充对齐来调整。
对象的访问定位
java程序通过栈上的reference数据来操作堆上的具体对象。对象访问方式取决于虚拟机实现而定的。访问方式有使用句柄和直接指针两种。
句柄访问:
Java堆中会划分出一块内存来作为句柄池,reference中内存的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
使用直接指针访问:
Java堆对象中就必须如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
句柄
由于reference中存储的是稳定的句柄地址,在对象被移动时(如GC过程中的对象移动),只需改变句柄中实例数据指针,而reference本身不用动。
直接指针
速度快,节省了一次指针定位的时间开销。HotSpot采用此方式
OutOfMemory异常
java堆溢出
java堆是用来存放对象实例以及数组的,使java堆发生内存溢出的要旨是:
不断创建对象
保证对象存活,不会被垃圾收集器
jvm虚拟机启动参数设置
/**
* java 堆溢出
* VM Args:-Xms20m -Xmx20m(限制堆的大小不可扩展)
* @author XIA
*
*/
public class HeapOutOfMemoryError {
public static class OOMObject{
}
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while(true){
list.add(new OOMObject());
System.out.println(System.currentTimeMillis());
}
}
}
运行结果中java heap space明确的指出了异常发生的区域:堆。
虚拟机栈和本地方法栈溢出
使虚拟机栈发生内存溢出异常的情形有两种:
线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常
虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常
/**
* 虚拟机栈溢出(线程请求的栈深度大于虚拟机所允许的最大深度将抛出StackOverflowError异常)
* VM Args:-Xss256k
* @author XIA
*
*/
public class StackOverflowError {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackOverflowError oomError = new StackOverflowError();
try {
oomError.stackLeak();
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println("栈深度为:"+oomError.stackLength);
e.printStackTrace();
}
}
}
package com.NioSocket;
/**
* 通过不断创建活跃线程,消耗虚拟机栈资源
* 虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常
* VM Args:-Xss256k
* @author XIA
*
*/
public class StackOutOfMemoryError {
//线程任务,每个任务一直在运行
private void wontStop(){
while(true){
System.out.println(System.currentTimeMillis());
}
}
//不断地创建线程
public void stackLeadByThread(){
while(true){
Thread thread = new Thread(
new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
wontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
StackOutOfMemoryError oomError = new
StackOutOfMemoryError();
oomError.stackLeadByThread();
}
}
方法区和运行时常量池溢出
使方法区发生内存溢出的要旨:
1.程序运行时创建的大量类,导致方法区内存空间不足
2.程序中存有大量字面量等导致常量区内存不足
String.intern()是一个Native方法,它的作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回常量池中这个字符串的String 对象;否则,将次对象包含的字符串添加到常量池中,并返回此String对象的引用。
}/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize =10M
* @author XIA
*
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
1.7版本开始字符串常量和类引用被移出到Java Heap中。
动态产生大量的类产生方法区内存溢出