一 Java内存区域与HotSpot虚拟机对象
1 Java技术体系、JDK、JRE?
Java技术体系包括:
· Java程序设计语言;
· 各种硬件平台上的Java虚拟机;
· Class文件格式;
· Java API类库;
· 来自商业机构或开源社区的第三方Java类库。
JDK,即Java开发工具包,包括Java程序设计语言、Java虚拟机、Java API类库,是用于支持Java程序开发的最小环境;不仅提供了Java程序运行所需要的JRE,还提供了一系列的编译、调试工具,如javac、jconsole等。
JRE,即Java运行时环境,包括Java API类库中的Java SE API子集和Java虚拟机两部分,是用于支持Java程序运行的标准环境。
2 Java内存区域如何划分?
Java内存区域分为五部分:程序计数器、虚拟机栈、本地方法栈、方法区、堆。
(1)程序计数器:
---当前线程所执行的字节码的行号指示。若线程正在执行的是一个java方法,则计数器记录的是正在执行的虚拟机字节码指令;若线程正在执行的是一个Native方法(本地方法),则计数器值为空;
---线程私有的;
---唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
(2)虚拟机栈:
---描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息;每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
---线程私有的,生命周期与线程相同;
---使用连续的内存空间;
---两种异常:若线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;若虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,抛出OutOfMemoryError异常。
(3)本地方法栈:
---本地方法栈和虚拟机栈的唯一区别是虚拟机栈是为虚拟机执行java方法服务,而本地方法栈为虚拟机执行Native方法服务,其他与虚拟机栈一样。
(4)Java堆:
---虚拟机启动时创建,用于存放对象实例,几乎所有的对象实例都在Java堆中分配内存(随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术可能导致对使得实例在栈上直接分配);
---所有线程共享的;
---是垃圾收集器管理的主要区域。从内存回收角度,可分为:新生代和老年代;从内存分配的角度,Java堆中可分出多个线程私有的分配缓冲区(TLAB)。
---可以处于物理上不连续的内存空间、可以选择固定大小或者可扩展。可能抛出OutOfMemoryError异常。
(5)方法区:(HotSpot: 永久代)
---用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
---线程共享的;
---此区域内存回收的主要目标是常量池的回收和类型的卸载;
---不需要连续内存、可以选择固定大小或者可扩展、还可以选择不进行垃圾收集。当无法满足内存分配需求时,将抛出OutOfMemoryError异常。
3 运行时常量池
---运行时常量池是方法区的一部分;
---Class文件中的常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放;
---Java虚拟机规范没有对运行时常量池做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域;
---具备动态性,运行期间也可能将新的常量放入池中,如String类的intern()方法。
4 对象的创建过程
普通Java对象(不包括数组和Class对象)的创建过程为:
(1)首先检查new指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,则先执行相应的类加载过程。
(2)在类加载检查通过后,虚拟机将为新生对象分配内存。
分配方式有:
· “指针碰撞”:当Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。此时,分配内存仅需要把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
· “空闲列表”:当Java堆中的内存不是规整的,此时,虚拟机必须维护一个列表,记录上哪些内存块是可用的,分配的时候从列表中找到一块足够大的空间划分给对象实例,并且更新列表上的记录。
选择哪种分配方式是由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配方式为指针碰撞;使用CMS这种基于标记-清除算法的收集器,系统采用的分配方式为空闲列表。
分配内存的操作在并发情况下并不是线程安全的,解决方法有:
· 对分配内存空间的动作进行同步处理,而实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
· 把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。
(3)虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。
(4)虚拟机对对象进行必要的设置,如这个对象是哪个类的实例、对象的哈希码、对象的GC分代年龄等。
(5)一般来说,执行new指令之后还会接着执行<init>方法,把对象按照程序员的意愿进行初始化。
5 对象的内存布局
对象在内存中存储的布局可分为3块区域:对象头、实例数据、对齐填充。
对象头包括两部分:
· 存储对象自身的运行时数据(Mark Word),如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。考虑到虚拟机的空间效率,Mark Word被设计成根据对象的状态复用自己的存储空间。
· 类型指针,即对象指向它的类元数据的指针。若对象是一个Java数组,则还必须有一块用于记录数组长度的数据。
实例数据:在程序代码中所定义的各种类型的字段内容,包括父类继承下来的和子类中所定义的。存储顺序受虚拟机分配策略参数和字段在Java源码中定义顺序的影响。规则:相同宽度的字段总是被分配到一起,在父类中定义的变量会出现在子类之前。
对齐填充:起占位符的作用。对象的大小必须是8字节的整数倍。
6 对象的访问方式
(1)使用句柄访问
方式:Java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
优点:reference中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改。
(2)使用直接指针访问
方式:reference中存储的是对象地址。
优点:节省了一次指针定位的时间开销,速度更快。