运行时数据区域
1. 程序计数器
行号指示器,线程私有,本地方法计数器值为null。
2. java虚拟机栈
存储局部变量、方法等信息。每一个方法被调用至执行完毕的过程就对于着一个栈帧从入栈到出栈的过程,进入一个方法时,这个方法需要在栈帧中分配多少空间是完全确定的,运行期间不会改变。栈是线程私有的。
3. 本地方法栈
作用类似于Java虚拟机栈,主要用于存放本地方法的变量。线程私有。
4. java堆
所有线程共享,虚拟机启动时创建,唯一的目的就是存放对象实例,几乎所有对象实例都在这分配内存。堆是垃圾收集器管理的区域,空间可以是固定大小的,也可以是可扩展的。
5. 方法区
所有线程共享,主要用于存储被虚拟机加载的类型信息、储常量以及静态变量等,java6之后放弃永久代,逐步使用本地内存实现方法区。方法区较少发生垃圾收集行为。
6. 运行时常量池
是方法区的一部分,存放编译期生成的各种字面量和符号引用(常量池表),具有动态性,运行期间也可以将新的常量放入池中。
虚拟机对象
对象的创建
主要流程:new -> 类加载检查 -> 分配空间 -> 内存空间初始化 -> 构造函数 -> 创建完成
- 类加载检查:虚拟机遇到一条new命令时,会先检查这条命令的参数是否能在常量池定位到一个类的符号的引用。并且检查这个符号引用代表的类是否已经加载过,初始化过。
- 分配空间:类加载检查通过后,虚拟机为新生对象分配内存,对象所需的大小在加载后可以完全确定。分配空间的方式有两种:指针碰撞和空闲列表,暂时不展开说明。在划分空间时,还要考虑线程安全的问题,确保线程安全有两种方式:1.对分配内存空间对动作做同步处理--CAS+失败重试。2.把内存分配对动作按照线程活粉到不同空间处理--本地线程分配缓冲(TLAB),本地缓冲区用完后,分配新的缓冲区再进行同步锁定。
- 内存空间初始化:分配好内存空间后,虚拟机要将分配到对内存空间初始化为0值,如果使用TLAB方式分配内存,这一步也可以在分配空间时完成。
对象的内存布局
对象在堆内存中的存储布局可划分为三个部分:对象头/实例数据/对齐填充
- 对象头:对象头主要包含两类信息。1.运行时状态数据,与自身定义无关。2.类型指针,指向类型元数据,用于确定对象是哪个类的实例。32位虚拟机对象布局占位如下:哈希码-2位,年龄-4,固定0位-1,锁标志-2。
- 实例部分:对象真正存储的有效信息。
- 对齐填充:非必要,无实际意义。
对象的访问定位
java程序通过栈上的reference操作堆上的具体对象。对象的访问方式由虚拟机的实现而定。主流的访问方式有两种:句柄访问/直接指针。
- 句柄访问:堆中划分出一道区域作为句柄池,reference指向对象的句柄,而句柄指向对象的实例数据和对象的类型数据。优点是reference存储稳定的句柄的地址,在对象被移动后只需改变句柄的指针,无需改变reference。
- 直接指针:reference直接存储对象的地址,无需间接访问。优点是速度快,节省一次指针定位的时间。HotSpot主要使用第二种。
OOM异常
1. java堆溢出
/**
* java.lang.OutOfMemoryError: Java heap space
* java堆溢出
*/
class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
2. java栈溢出
/**
* java.lang.StackOverflowError
* java栈溢出
*/
class JavaVMStackSOF {
private int length = 1;
public void stackLeak() {
length++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF stackSOF = new JavaVMStackSOF();
try {
stackSOF.stackLeak();
} catch (Throwable e) {
System.out.println(stackSOF.length);
throw e;
}
}
}
3. 创建线程导致内存溢出
class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
private void stackLeakByThread() {
while (true) {
Thread thread = new Thread(() -> dontStop());
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM stackOOM = new JavaVMStackOOM();
stackOOM.stackLeakByThread();
}
}
4. 运行时常量池溢出
class RunTimeConstantPoolOOM {
public static void main(String[] args) {
/**
* java6运行会出现常量池OOM,7之后运行会出现堆溢出
* 因为7之后,存放在永久代的字符串常量池移至java堆中
*/
/*Set<String> set = new HashSet<>();
short i = 0;
while (true) {
set.add(String.valueOf(i).intern());
}*/
/**
* java6中,intern方法把首次遇到的字符串实例复制到永久代的字符串常量池,而StringBuilder创建的对象存储在堆上,不是同一个引用,返回false,false,false
* java7中,字符串常量池已经移至堆中,所以intern()返回的引用和StringBuilder创建的对象是同一个,返回true,false,false,
* 二三句返回false因为Builder.toString()之前常量池已经有它的引用
*/
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);
String str3 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str3.intern() == str3);
}
}