java内存管理之内存模型
1,运行时数据区域
1. 程序计数器 (program counter register)
2. Java虚拟机栈 (jvm stack)
3. 本地方法栈 (native method stack)
4. java堆 (heap)
5. 方法区(method area)
6. 运行时常量池
7. 直接内存
1. 程序计数器 (program counter register)
1.1 概念
程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。
注:如果当前线程正在执行的是一个本地方法,那么此时程序计数器为空。(这里的本地方法即非java语言编写的方法) 。
1.2 作用
1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
2)在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
1.3 特点
1)线程私有。每条线程都有一个程序计数器。
2)是唯一不会出现OutOfMemoryError的内存区域。(java.lang.OutOfMemoryError内存溢出,即说明jvm的内存不够用了)
3)生命周期随着线程的创建而创建,随着线程的结束而死亡。
2. Java虚拟机栈 (jvm stack)
2.1 概念
Java虚拟机栈是描述Java方法运行过程的内存模型。Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,这些信息包括:
1)局部变量表(基本数据类型、引用类型、returnAddress类型的变量(此变量为JVM原始数据类型,在java语言中不存在对应类型))
2)操作数栈
3)动态链接
4)方法出口信息等
当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。栈的大小和具体JVM的实现有关,通常在256K~756K之间。
2.2 特点
1)局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且,局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外,在方法运行的过程中局部变量表的大小是不会发生改变的。
2)Java虚拟机栈会出现两种异常:
a) StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
b) OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
3)Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
3. 本地方法栈 (native method stack)
1)本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法区是本地方法运行的内存模型。
2)本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
3)方法执行完毕后相应的栈帧也会出栈并释放内存空间。
4)也会抛出StackOverFlowError和OutOfMemoryError异常。
我们使用的HotSpot虚拟机,就干脆没有这块区域了,它和虚拟机栈是一起的。
4. java堆 (heap)
4.1概念
堆是用来存放对象实例的内存空间。
几乎所有的对象实例都存储在堆中。
4.2特点
1)线程共享 ,整个Java虚拟机只有一个堆,所有的线程都访问同一个堆;
2)在虚拟机启动时创建;
3)垃圾回收的主要场所;
4)可以进一步细分为:新生代、老年代。
新生代又可被分为:Eden、From Survior、To Survior。
不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,从而更高效。
5)堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError。
5. 方法区(method area)
5.1概念
虚拟机规范是把这块区域描述为堆的一个逻辑部分的,但实际它应该是要和堆区分开的。
方法区中存放已经被虚拟机加载的类信息、常量、静态变量(静态域)、即时编译器编译后的代码等。
5.2特点
1)线程共享
方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
2)永久代
方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,从上面提到的分代收集算法的角度看,HotSpot中,方法区≈永久代。不过JDK 7之后,我们使用的HotSpot应该就没有永久代这个概念了,会采用Native Memory来实现方法区的规划了。
3)内存回收效率低
方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。
对方法区的内存回收的主要目标是:对常量池的回收 和 对类型的卸载。
4)Java虚拟机规范对方法区的要求比较宽松
和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收
6. 运行时常量池(方法区的一部分)
运行时常量池目的是为了方便的创建某些对象而出现的。
相关的小实验;http://www.cnblogs.com/xianDan/p/4292814.html
理解常量池:https://blog.csdn.net/gcw1024/article/details/51026840/
Class文件中除了有类的版本信息、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,另外翻译出来的直接引用也会存储在这个区域中。这个区域另外一个特点就是动态性,Java并不要求常量就一定要在编译期间才能产生,运行期间也可以在这个区域放入新的内容,String.intern()方法就是这个特性的应用。
当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),
A)字面量相当于Java语言层面常量(是一个值,这个值本身(1,‘a’,“abc”,true);不可变的变量 final修饰的变量)
B)符号引用(就是一些标识性的字符串呗)则属于编译原理方面的概念,包括了如下三种类型的常量:
类和接口的全限定名; 字段名称和描述符; 方法名称和描述符
Java中八种基本类型的包装类的大部分都实现了常量池技术,它们是Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类(Float、Double)则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用对象池
7. 直接内存
直接内存是除Java虚拟机之外的内存,但也有可能被Java使用。
在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。
直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OutOfMemoryError。
2,hotspot虚拟机对象探秘
2.1 对象的创建过程
当虚拟机遇到一条含有new的指令时,会进行一系列对象创建的操作:
1,检查常量池中是否有即将要创建的这个对象所属的类的符号引用;
- 若常量池中没有这个类的符号引用,说明这个类还没有被定义!抛出ClassNotFoundException;Class.forname()时可能出现的错误
- 若常量池中有这个类的符号引用,则进行下一步工作;
2,进而检查这个符号引用所代表的类是否已经被JVM加载;
- 若该类还没有被加载,就找该类的class文件,并加载进方法区;
- 若该类已经被JVM加载,则准备为对象分配内存;
3,根据方法区中该类的信息确定该类所需的内存大小;
一个对象所需的内存大小是在这个对象所属类被定义完就能确定的!且一个类所生产的所有对象的内存大小是一样的!JVM在一个类被加载进方法区的时候就知道该类生产的每一个对象所需要的内存大小。
4,从堆中划分一块对应大小的内存空间给新的对象;
分配堆中内存有两种方式:
1)指针碰撞
如果内存是规整的,那么虚拟机将采用的是指针碰撞法来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。
2)空闲列表
如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。如果垃圾收集器选择的是CMS这种基于标记-清除算法的,虚拟机采用这种分配方式。
另外一个问题及时保证new对象时候的线程安全性。因为可能出现虚拟机正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。虚拟机采用了CAS配上失败重试的方式保证更新更新操作的原子性和TLAB两种方式来解决这个问题。
5,为对象中的成员变量赋上初始值(默认初始化);
6,设置对象头中的信息
例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息
7,调用对象的构造函数进行初始化
把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
2.2 对象的内存结构
一个对象从逻辑角度看,它由成员变量和成员函数构成,从物理角度来看,对象是存储在堆中的一串二进制数,这串二进制数的组织结构如下。
对象在内存中分为三个部分:
- 对象头
- 实例数据
- 对齐补充
1)对象头
- 对象在运行过程中所需要使用的一些数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 元数据指针(类(class)型信息),指向方法区中的目标类。
- 如果是数组,这里还有数组长度。
2)实例数据
实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。
3)对齐补充
用于确保对象的总长度为8字节的整数倍。
HotSpot要求对象的总长度必须是8字节的整数倍。由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的,因此需要对齐补充字段确保整个对象的总长度为8的整数倍。
2.3 对象访问过程
建立对象是为了使用对象,Java程序需要通过栈上的reference(引用)数据来操作堆上的具体对象。
例如:Object obj = new Object()
new Object()之后其实有两部分内容,一部分是类数据(比如代表类的Class对象)、一部分是实例数据。
由于reference在Java虚拟机规范中只是一个指向对象new Object()的引用obj,并没有规定obj应该通过何种方式去定位、访问堆中对象的具体位置,所以对象访问方式也是取决于虚拟机而定的。主流方式有两种:
1)句柄访问方式
Java堆中划分出一块句柄池,obj指向的是对象的句柄地址,句柄中则包含了类数据的地址和实例数据的地址。
2)直接指针访问方式
引用类型的变量直接存放对象的地址(obj指向的是这个对象),从而不需要句柄池。
但对象所在的内存空间中需要额外的策略存储对象所属的类信息的地址。即对象中存储所有的实例数据和类数据的地址
说明:
1)HotSpot采用直接指针方式访问对象,因为它只需一次寻址操作,从而性能比句柄访问方式快一倍。但它需要额外的策略存储对象在方法区中类信息的地址。
2)对象的访问过程取决于对象的在内存怎么存的(对象的内存分配) 取决于 采用何种垃圾回收机制。
《深入理解Java虚拟机》