您的位置:首页 > 编程语言 > Java开发

深入理解Java虚拟机JVM高级特性与最佳实践阅读总结——第二章 Java内存区域与内存溢出异常

2017-02-28 22:50 931 查看
程序计数器program counter register:字节码解释器所需要执行的下一条字节码地址,由于虚拟机的多线程机制,每个线程都有一个独立的程序计数器,线程私有,各计数器之间独立存储;如果执行的是Java方法,计数器记录的是正在执行的虚拟机字节码地址;如果执行的是native方法,计数器为空(undefined);此内存区域是唯一没有规定OutOfMemoryError情况的区域

Java虚拟机栈Java virtual machine stacks、本地方法栈Native Method Stack:线程私有,其生命周期和线程相同;两种异常:如果线程请求的栈深度大于虚拟机允许深度,则抛出StackOverFlow;对于栈可动态扩展的虚拟机,如果扩展时无法申请到足够内存,抛出OutOfMemoryError;本地方法栈和Java虚拟机栈类似,区别在于一个执行Java方法,一个执行本地方法

局部变量表:存放在编译期可以知道的各种基本数据类型、对象引用、returnAddress类型;局部变量表所需要的内存空间在编译期间完成分配,进入一个方法需要在帧中分配多大局部变量空间是完全确定的,运行期间不会改变局部变量表大小

Java堆:也称GC堆,是Java虚拟机管理的最大一块内存空间,被所有线程共享,在虚拟机启动时创建;作用是存放对象实例;堆空间在物理上可以是不连续的,但逻辑上是连续的;堆的大小可以是固定的,也可以是动态扩展的;如果堆无法完成实例分配或无法再扩展,则抛出OutOfMemoryError

方法区method area:是堆的一个逻辑部分,存储虚拟机加载的类信息、常量、静态变量、编译后代码;方法区GC和堆中的GC可以不一致,该区GC的主要针对常量池的回收和对类型的卸载;其余和堆类似

运行时常量池Runtime Constant Pool:方法区的一部分,存放编译期间成成的各种字面量、符号引用,该信息在类加载后进入;该区除了保存Class文件中的符号引用外,还会将翻译出来的直接引用也存储在运行时常量池;运行时常量池具有动态性,常量不一定在编译期产生,即并非预置入class文件中常量池的内容才可以进入运行时常量池,运行期间也可以,如String类的intern()方法

直接内存:不是规范的一部分,但也可能导致内存溢出异常,1.4中引入了NIO类,引入了基于通道(channel)和缓冲区(buffer)的io方式,可以使用native库直接分配对外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为该内存的引用,从而避免在Java堆和native堆之间频繁复制,直接内存不受Java堆大小的限制,但是受物理内存的限制

虚拟机角度解释对象的创建
1、在常量池定位该类的符号引用,并检查该类是否加载、解析、初始化,不成立则进入类加载阶段
2、分配内存,对象所需的内存在类加载阶段是完全已知的。分配方式主要有两种
          1、指针碰撞法,适用于规整的内存空间
          2、空闲列表法,适用于不规整的空间
而空间是否规整,则取决于GC是否具有内存压缩整理功能,多线程情况下,为线程安全,主要有两种方式:
          1、同步,虚拟机就是采用这种方式,CAS+失败重试保证更新操作完整性
          2、内存分配按照线程规划放置到线程内部,即通过本地线程分配缓冲TLAB(Thread Local Alloaction Buffer),此时只有在重新分配TLAB时才需要同步
3、将分配的空间初始化为零值,如果使用了TLAB,也可以在TLAB分配时进行
4、对象设头置,如元数据、哈希码、GC分代信息等
至此,虚拟机对象创建完成,对象初始化操作(调用invokespecial指令)由程序决定

对象内存布局
1、对象头header,包含两部分信息
          1、存储对象运行时数据,如哈希码、GC分代、锁、偏向线程ID,偏向时间戳等
          2、类型指针,即指向元数据的指针,以此确定对象属于哪个类,对于数组对象,还放有记录数据长度的数据(普通对象可以通过访问元数据获得对象大小)
2、实例数据instance,存储真实数据,存储顺序 long/double  int  short/char  byte/boolean  oop(ordinary object pointer),即相同宽度字段分配在一起,且父类数据在子类之前,如果CompactFiled为true,子类中较窄变量可能插入父     类变量空间的空隙
3、对齐填充padding,HotSpot要求对象起始地址必须为8的倍数

对象的定位。主要有两种
1、句柄式,堆中划分出句柄池,栈中的reference存储对象的句柄池地址,句柄池中存储对象实例数据(实例池中)和各数据类型的地址信息(方法区内),优点是reference中的地址稳定,对象移动仅需要改变句柄池中内容
2、直接指针式,reference中直接存放对象的地址,此时需要考虑对象内存布局中如何保存访问类型数据的指针,优点是速度快,节省了一次指针定位的开销,HotSpot就是采用这种方式

补充
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        public static void main(String[] args) {
        String str1 = new StringBuilder("中国").append("钓鱼岛").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }    }
}
Sting的intern()方法是native方法,如果常量池中已经包含一个等于String对象的字符串,则返回常量池中的对象引用,否则将该对象添加到常量池并返回该String的引用
jdk1.6中返回两个FALSE,因为1.6采用的复制的方式将对象复制到永久代,因此一个是指向永久代中的引用,一个是堆上的引用,是两个不同的引用,则返回FALSE
jdk1.7中,第一个返回true,第二个返回FALSE,因为1.7中不再采用复制的方式,只是在常量池记录“首次出现”的实例引用,因此第一个返回true,第二个Java已经在常量池中出现过,则不满足条件,返回false
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java jvm
相关文章推荐