0. 前言
本文主要想阐述的问题如下:
- 什么是JVM(Java Virtual Machine)和GC(Garbage Collection)?
- HotSpot虚拟机架构
- JVM内存区域介绍
- Java对象的访问方式
面试常问的问题:
- 变量和实例存在哪?
- java栈的作用?
- java的堆存什么?
- 方法区存什么?
- 各种内存溢出的情况及其原因?
1. 什么是JVM?
- VM(Virtual Machine):一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
- JVM(Java Virtual Machine):提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提供很好的灵活性,同时也确保Java代码可在符合该规范的任何系统上运行
关于JVM,需要说明一下的是,目前使用最多的Sun公司的JDK中,自从1999年的JDK1.2开始直至现在仍在广泛使用的JDK6,其中默认的虚拟机都是HotSpot。2009年,Oracle收购Sun,加上之前收购 的EBA公司,Oracle拥有3大虚拟机中的两个:JRockit和HotSpot,Oracle也表明了想要整合两大虚拟机的意图,但是目前在新发布的JDK8中,默认的虚拟机仍然是HotSpot,因此本文中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。
2. HotSpot虚拟机架构
这个架构可以分成三层看:
- 最上层:javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域
- 中间层:称为Runtime Data Area,主要是在Java代码运行时用于存放数据的,从左至右为方法区(持久代也叫非堆)、堆(共享,GC回收对象区域)、栈、程序计数器和寄存器、本地栈(私有)
- 最下层:JVM最核心两块 JIT(just in time)即时编译器 和 GC(Garbage Collection,垃圾回收器)
3. JVM内存区域
在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:
3.1 Method area:
又叫永久代(Permanent Generation)或Non-Heap(非堆)。方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。
特点:
- 与Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。
- 相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是不会发生GC,这里的GC主要是对常量池的回收和对类的卸载,
- 回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。
3.2 Heap:
堆区(Heap):创建实例对象,几乎所有对象实例都在这里分配内存。Java堆是内存回收的主要区域,所以也有人称他为GC堆。
特点:
- 堆区由所有线程共享,在虚拟机启动时创建。
- 堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存。
- 堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。
- 一个接口的多个实现类需要的内存大小可能不一样,一个方法中多个分支需要的内存也不一样,所以内存分配回收是动态的。
3.3 Java Thread:
虚拟机栈(JVM Stack):Java方法被执行的时候的内存模型,一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
特点:
- 局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。
- 局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中,是完全确定的,在方法的生命周期内都不会改变。
- 虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。
- 每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。
3.4 Program Counter Registers
每个JAVA线程都有一个单独的PC Register,他是一个指针,由Execution Engine读取下一条指令。对于非Native方法,这个区域记录的是正在执行的VM原语的地址,如果正在执行的是Natvie方法,这个区域则为空(undefined)。由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,所以此内存区域是唯一一个在VM内存区域中,没有规定任何OutOfMemoryError情况的区域。
特点:
- 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的
3.5 Native Method Stack
供本地方法(非java)使用的栈。每个线程持有一个Native Method Stack。本地方法栈在作用、运行机制、异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。
特点:
- 本地方法栈也是线程私有的。
4 Java对象的访问方式
JAVA是面向对象的语言,那么在JAVA虚拟机中,存在非常多的对象,对象访问是无处不在的。即时是最简单的访问,也会涉及到JAVA栈、JAVA堆、方法区这三个非常重要的内存区域之间的关联关系。
比如:
Object obj = new Object();
其中,Object obj
这部分语义作为一个reference类型数据出现,将存储到JAVA栈
的本地变量表中。new Object()将生成一个实体对象,存储在JAVA堆中,包含了Object类型的所有实例数据值(对象中各个字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定的。另外,在JAVA堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现接口、方法等)的地址信息,这些类型数据存储在方法区中。不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:句柄和直接指针。
- 句柄访问方式
JAVA堆中将会划分出来一块内存作为句柄池,reference中就是存储了对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。- 好处:
reference中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要被修改。
- 好处:
-
直接指针
相比较句柄的访问方式,JAVA堆中不会单独划分内存,reference中直接存储了对象地址,而对象中包含了对象类型数据的地址信息。- 好处:
速度更快,节省了一次指针定位需要的时间开销,由于JAVA对象访问十分频繁,这类开销积小成多后也是一项非常可观的执行成本。Sun HotSpot虚拟机使用的就是这种访问方式。
- 好处:
可能上述文字比较抽象,我们举个例子,比如有这样一个实体类,名为Stu:
public class Stu extends Object{
private String name;
private int age;
public Stu(String name,String age){
this.name = name;
this.age = age;
}
public String getName(){
return this.name;
}
...
}
创建Stu对象:
Stu kevin = new Stu(“kevin”,15);
这样根据上文解释如下:
kevin作为一个reference类型的变量存储在本地变量表中,在Hotspot虚拟机中,存储的是具体对象(kevin)的直接地址;new Stu(“kevin”,15)就是实例化了一个对象,JAVA堆中保存了Stu实体类的所有的字段信息,比如name=”kevin”,age=15。此时,JAVA堆中还存储了Stu对象的类型数据的地址信息,通过这个地址在方法区中可以查找对象的类型、父类、实现的接口、方法等信息。
备注:
- Execution Engine :是执行引擎。Class文件被加载后,即时编译器(JIT compiler,just-in-time compiler)会把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。
- Native Method :一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。