Java虚拟机历史
Sun Classic
Sun Classic VM–世界上第一款商用的Java虚拟机。只能使用纯解释器方式来执行Java代码。
- 使用java -version命令查看当前版本Java使用的虚拟机(Java8使用的是HotSpot VM)
Classic VM在JDK1.2之前是Sun JDK中唯一的虚拟机,在JDK1.2时,它与HotSpot VM并存,但默认使用的是Classic VM(用户可用java -hotspot参数切换至HotSpot VM),而在JDK1.3时,HotSpot VM成为默认虚拟机,但Classic VM仍作为虚拟机的“备用选择”发布(使用java -classic参数切换),直到JDK1.4时,Classic VM才完全退出商业虚拟机的历史舞台,与Exact VM一起进入了Sun Labs Resarch VM之中。
Sun HotSpot VM
目前使用最广的Java虚拟机
- 热点代码探测技术:HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。通过编译器和解释器恰当的协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无需等待本地代码输出才能执行程序,即时编译的时间压力也相对较小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
内存区域
运行时数据区域
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 线程私有:每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储;
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果正在执行的是Native方法,这个计数器的值则为空(Undefined);
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域;
Java虚拟机栈
- 线程私有:生命周期与线程相同;
- 描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程;
- 局部变量表:存放了编译期可知的各种基本数据类型(long和double类型会占用2个局部变量空间,其余只占用1个)、对象引用类型(reference类型)和returnAddress类型(执行一条字节码指令的地址)。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小;
- StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度时;
- OutOfMemoryError:如果虚拟机栈可以动态扩展,并且在扩展时无法申请到足够的内存时;
本地方法栈
- 与虚拟机栈发挥的作用非常相似;
- 虚拟机栈为虚拟机执行Java方法(字节码)服务;本地方法栈则为虚拟机使用到的Native方法服务;
- 在虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构没有强制规定;
- HotSpot直接把本地方法栈和虚拟机栈合二为一;
- 也会抛出StackOverflowError和OutOfMemoryError;
Java堆
- 对于大多数应用来说,是Java虚拟机所管理的内存中最大的一块;
- 所有线程共享的,在虚拟机启动时创建;
- 存在的唯一目的就是存放对象实例。在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也逐渐变得不是那么“绝对”了;
- Java堆是垃圾收集器的主要区域,因此会根据不同的垃圾收集器进行不同的细分方法;
- 可以处于物理上不连续的内存空间中,只要逻辑上连续就可以;
- OutOfMemoryError:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时;
方法区
- 线程共享的,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
- Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来;
运行时常量池
- 是方法区的一部分。用于存放编译期生成的字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放;
- 动态性:运行期间也可以将新的常量放入池中,这种特性被开发人员利用的比较多的是String类的intern()方法;
直接内存
- 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但也会被频繁调用,例如NIO;
- 直接内存得分配不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器物理寻址空间的限制,导致OutOfMemoryError;
HotSpot虚拟机
以下内容限于普通Java对象。
对象的创建
- 类加载检查:虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的引用符号,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载机制。
- 分配内存:类检查通过后,虚拟机会为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定。
+ 指针碰撞(Bump the Pointer):假设Java堆中的内存是绝对规整的,所有用过的内存都放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,所以分配内存就是指把指针向空闲空间移动一段与对象大小相同的距离。
+ 空闲列表(Free List):假设Java堆中的内存是不规整的,已使用的内存和空闲内存相互交错,这种情况下,虚拟机就必须维护一个列表,记录哪块内存可用,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新记录表上的数据。
+ 选择分配方式取决于Java堆是否规整,Java堆是否规整又取决于采用的垃圾收集器是否带有压缩整理功能。在使用Serial、ParNew等带Compact过程的收集器时,系统采用的是指针碰撞;而采用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
+ 为了解决在并发情况下安全的分配内存,通常有两种做法,第一种:对分配内存空间的动作进行同步处理(实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性);第二种:把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。 - 初始化零值:虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步操作保证了对象的实例字段在Java代码中可以不赋值就直接使用,程序能访问到这些字段的数据类型多对应的零值。
- 进行必要的设置:虚拟机根据对象的对象头中存储的信息对对象进行必要的设置。
以上工作完成后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的角度来看,对象的创建才刚刚开始(init方法还没有执行,多有的字段值都还是零)
对象的内存布局
- 对象头(Header):包含两部分数据,第一部分:用于存储对象自身的运行时数据;第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。注意:如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。
- 实例数据(Instance Data):是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。HotSpot虚拟机默认的分配策略为longs\doubles、ints、shorts\chars、bytes\booleans、oops(Ordinary Object Pointers),从分配策略中可以看出。相同宽度的字段总是被分配到一起。
- 对其填充(Padding):并不是必然存在,仅仅起着占位符的作用,保证对象的长度必须是8的倍数。
对象的访问定位
建立对象是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也取决于虚拟机实现而定的。
- 句柄:Java堆中将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与数据类型各自的具体地址信息。优点:存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要改变。
- 直接指针:reference中存储的直接就是对象地址。优点:速度快。HotSpot使用。