深入java内存区域

Posted qven

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入java内存区域相关的知识,希望对你有一定的参考价值。

 

  第一次发布随笔,有点小兴奋,还是进入正题吧。

  java代码运行一般分为编译期和运行期。编译期负责将.java(如Student.java)编译成.class文件,然后在运行期通过类加载过程(加载,连接(验证,准备,解析),初始化,使用,卸载),类加载阶段将在下一章节进行分析,此处就不在赘述。

  先来看看java虚拟机运行时数据区图:

    技术分享图片

     一.线程隔离的内存空间

      1.程序计数器 

     程序计数器是是一块较小的内存空间。它在虚拟机中充当行号指示器的角色。无论是顺序,循环,分支等执行顺序都需要行号指示器来决定当前运行的指令地址,字节码解释器就是通过改变计数器的值来决定下一条字节码指令。在多线程中,一个处理器虽然看似是多条线程同时在执行,但事实上是由于cpu调度器时间片的切换(每个时间片都较短)。若线程A执行一部分未执行完,CPU调度器切换分配时间片给线程B,此时就需要程序计数器来记录线程A最后执行的指令地址以保证再次切换到线程A时能执行正确的指令位置。每个线程都会有一个自己的程序计数器,且程序计数器之间互不影响。

      2.java虚拟机栈

      栈和线程的生命周期是一致的,随着线程的创建而创建,销毁而销毁。栈中主要存储着局部变量表,操作数栈,动态链接和方法出口等,局部变量表中存放着8种基本数据类型以及对象引用,其中long和double类型是64位的,所以在局部变量内存中占2个slot槽位(相当于两个局部变量的内存),对象引用不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。线程执行每一个方法的时候都会创建一个栈帧,进入方法即入栈,跳出方法即出栈,方法执行结束后栈帧销毁。

      3.本地方法栈

      本地方法栈和虚拟机栈其实是类似的,区别在于虚拟机栈是针对于java方法而服务的,而本地方法栈是为native方法而服务的(被native修饰的方法一般与平台有关,可移植性不高)。

     

    .线程共享的内存空间

      1.堆

       堆是所有线程都能够共享的区域,几乎所有的对象和数组都存在此内存空间,它随着虚拟机启动而创建,它也是GC垃圾收集器主要回收的内存。

       由于主要存储的是对象,先来探索一下对象是如何创建的,当虚拟机接收到一条new指令的时候,先回检查指令的参数是否能在常量池中找到对应的符号引用,并且检查该符号引用是否有类加载,解析,初始化等。如果没有,执行类加载过程,执行完之后再为该创建好的对象分配内存。

      再看看对象的内存布局,它分为对象头,实例数据以及对其填充。其中对象头又分为运行时数据和类型指针。运行时数据包括哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID以及偏向时间戳。类型指针是指向类元数据的指针,类元数据是存放在方法区中的类信息(method,field等),但不是所有的对象都会有类型指针。另外数组是一种特殊的对象,它在对象投中还存有记录数组的长度的数据,这与一般的对象不同,一般的对象在类型指针中类元数据记录着对象长度,而数组的类元数据中没有记录长度信息。实例数据才是真正存储对象的有效信息。最后一部分是对其填充,由于HOTSPOT vm自动管理内存中对象起始地址必须是以8位的整数倍,而对象头刚好是8位的整数倍,但实例数据则不一定是8位的整数倍,而对其填充则就是对实例数据的填充,将其填充为8位的整数倍。

      对象的访问定位。java中是通过栈中的reference对象引用来操作具体的对象,但reference是如何区定位到具体的对象的,目前是有两种方式来进行定位:

      第一种是句柄。栈中的reference指向堆中的分配内存的句柄池,其中句柄池中包含类元数据的地址信息和实例对象的地址信息,分别指堆中的具体对象和方法区中的类元数据。

      技术分享图片  

    另外一种是直接指针。直接指针是reference指向堆中的对象,但对象中的对象头中的类型类型指针指向方法区中的类元数据。

    技术分享图片

    两种方法各有优势,第一种主要是稳定,由于在虚拟机中对象的迁移是很普遍的,了解GC内存回收应该知道,复制算法将堆分为两块其中一块用于存储对象,当需要进行内存回收时,将未回收的对象复制到另一块内存。但对象的迁移对reference是没有影响的,只会改变句柄池中实例数据的地址。第二种主要是性能效率的提升,由于堆中实例数据的地址指针的消失,是对象调用的性能提升,虽然一个不够明显,当堆中对象的访问非常频繁,性能提升还是很明显的。目前采用比较多的还是第二种,但也不是绝对的。

   2.方法区

    方法区和堆一样是线程共享的区域,方法区主要存储类的类元数据,静态变量,常量以及即时编译器编译代码等。这个区域一般都称之为永久代,主要是由于很少被full-gc清理。运行时常量池也是方法区的一部分,class文件有部分是常量池,它包括字面量和符号引用(字面量一般是被final修饰在编译期就被转化为的常量,符号引用是直接引用的前生,在连接中解析阶段就是将符号引用解析为直接引用),符号引用转化之后的直接引用以及常量池一起放到运行时常量池。运行时常量池有一个很明显的特征即为动态性,常量池中的常量并不一定只有在编译期才能产生,运行期也有可能会产生常量并将其放入常量池,比如String中的intern()方法。

  

  本文仅仅是对学习过程中的笔记整理,欢迎大家对文中不当之处进行点评。转载请注明地址:http://www.cnblogs.com/qven/p/8734534.html

  参考文献:深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)

    

    

  

 

以上是关于深入java内存区域的主要内容,如果未能解决你的问题,请参考以下文章

《深入理解Java虚拟机》笔记02:Java内存区域与内存溢出异常

Java内存管理:深入Java内存区域

Java内存区域 - 深入Java虚拟机读后总结

Java内存区域划分内存分配原理(深入理解JVM一)

Java内存区域 - 深入Java虚拟机读后总结

深入理解Java内存区域