[深入理解Java虚拟机] 第2章 Java内存区域与内存溢出异常

运行时数据区域

1. 程序计数器:

当前线程所执行的字节码的行号指示器,每个线程一个(“线程私有”),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;

如果正在执行的是Native方法,这个计数器值为空(Undefined)。[像System.currentTimeMillis() 这样的方法,大多通过C实现,没有编译成需要执行的字节码指令]

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。[程序运行过程中,计数器改变的只是值,而不会随着程序的运行需要更大的空间,也就不会发生溢出情况]

2. Java虚拟机栈:

线程私有,生命周期与线程相同,描述了Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean, byte, char, short, int, float, long, double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。

局部变量表所需内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

3. 本地方法栈:

与虚拟机栈的不同在于,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机用到的Native方法服务。

4. Java堆:

被所有线程共享,在虚拟机启动时创建,用于存放对象实例:所有的对象实例以及数组都要在堆上分配(现在并不绝对)。

是垃圾收集器管理的主要区域——“GC堆”;
可以处于物理上不连续的内存空间中,只要逻辑上式连续的即可。

5. 方法区:

线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

和Java堆一样不需要梁旭的内存和可以选择固定大小或者可扩展,还可以选择不实现垃圾收集。

6. 运行时常量池:

方法区的一部分,保存Class文件中描述的符号引用,以及翻译出来的直接引用

动态性——运行期间也可能将新的常量放入池中,例如,String类的intern()方法。

7. 直接内存:

不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。

JDK1.4新加入的NIO类,引入了一种基于通道与缓冲区的I/O方法,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。能提高性能,避免在Java堆和Native堆中来回复制数据。

内存的分配不受Java堆大小限制,受本机总内存大小以及处理器寻址空间的限制。

HotSpot虚拟机对象

1. 对象创建:

虚拟机遇到一条new指令时,

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

  • 类加载检查通过后,为新生对象分配内存:
    若内存规整——“指针碰撞”;若内存不规整——“空闲列表”。
    对象创建过程在并发情况下不是“线程安全”的——对分配内存空间的动作进行同步处理;或者把内存分配的动作按照线程划分在不同的空间之中进行(TLAB,本地线程分配缓冲)。

  • 内存分配完成后,将分配到的内存空间都初始化为零(不包括对象头),TLAB可以将这一步提前至TLAB分配时进行。

  • 对对象那个进行必要的设置,例如,这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希值等,存放在对象头中。

  • 执行new指令之后会接着执行 init 方法,按照程序员的意愿初始化对象。

2. 对象的内存布局:

对象头:

第一部分用于存储对象自身运行时数据(Mark Word),非固定的数据结构,根据对象的状态复用自己的存储空间。

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

如果对象是一个数组,对象头还必须有一块用于记录数组长度的数据。

实例数据:

程序代码中所定义的各种类型的字段内容,存贮顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。

对齐填充:

不是必然存在的,仅起到占位符的作用,内存管理系统要求对象起始地址必须是8字节的整数倍。

3. 对象的访问定位:

  • 使用句柄:

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

    好处是reference存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。

  • 直接指针:

    reference中存贮的直接就是对象地址。好处是速度更快,节省了一次指针定位的时间开销。

OutOfMemoryError异常

1. Java堆溢出:

不断创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制;

(-Xms 堆的最小值; -Xmx 堆的最大值)

2. 虚拟机栈和本地方法栈溢出:

实验证明:单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,虚拟机抛出的都是StackOverflowError异常。

可以通过不断建立线程的方法产生内存溢出异常,但这与栈内存是否足够大没有任何联系。
(栈容量只由 -Xss 参数设定)

3. 方法区和运行时常量池溢出:

JDK1.6之前,常量池分配在永久代内,通过 -XX:PermiSize和 -XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。但JDK1.7不会了。

可以借助CGLib直接操作字节码运行时生成大量动态类导致方法区溢出。

4. 本机直接内存溢出:

DirectMemory容量可通过 -XX:MaxDirectMemorySize指定,若不指定,则默认与Java堆最大值(-Xmx指定)一样。

可以越过DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(unsafe.allocateMemory())。

猜你喜欢

转载自blog.csdn.net/Blanchedingding/article/details/80848629
今日推荐