1. 运行时数据区域
1. 程序计数器(线程私有) 当前线程所执行的字节码的行号指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都要考程序计数器。(记住程序当前走到的位置,下次还回来)。线程私有。
2. Java虚拟机栈(线程私有) 和方法相关联,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程 线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(常见的递归层数过多导致”爆栈“)
3. 本地方法栈(线程私有) 类似于Java虚拟机栈。区别在于:
- 虚拟机栈为虚拟机执行Java方法
- 本地方法栈则为虚拟机使用到的Native方法服务
Java开头通过Java Native Interface来调用本地方法(一般用C语言编写)
4. Java堆(线程共享)
- Java虚拟机所管理的内存中最大的一块
- new出来的对象就存在于堆上
- 垃圾收集器管理的主要区域
5. 方法区(线程共享)
6. 运行时常量池
- 方法区的一部分
- 用于存放编译期生成的各种字面量和符号引用
2. 对象是如何创建的?
Object obj=new Object() : 分析这行代码的执行过程
- 使用了new关键字,检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。没有的话先加载类
- 类加载检查通过后,虚拟机将为新生对象分配内存
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值
- 虚拟机要对对象进行必要的设置,例如将对象的哈希码、元数据、GC分代年龄、是否使用偏向锁等数据存放在对象头中
- 执行init方法,把对象按照程序员的意愿进行初始化(给成员变量赋的值)
3. 对象的内存布局
4. 对象的访问定位
1. 句柄访问
Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
2. 直接指针访问
Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
二者比较:
- 使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,即使对象被移动(GC过程),只需要改变句柄中的示例指针,无需变动refrence
- 直接指针访问方式的最大好处就是速度更快, refrence直接指向实例数据,减少了一次指针访问
HotSpot虚拟机使用直接指针方式进行对象访问的
5. OutOfMemoryError异常代码演示
1. Java堆溢出
package jvm;
import java.util.ArrayList;
import java.util.List;
public class OutOfMemoryErrorDemo {
static class MyObject{
}
public static void main(String[] args) {
List<MyObject> list=new ArrayList<>();
int i=0;
while(true)
{
System.out.println(i++);
list.add(new MyObject());
}
}
}
限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析
2. 虚拟机栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
package jvm;
import java.util.ArrayList;
import java.util.List;
public class OutOfMemoryErrorDemo {
static int i=0;
public static void main(String[] args) {
f();
}
public static void f() {
System.out.println(i++);
f();
}
}
相同的Xss(线程的堆栈大小)下,如果栈中的本地数据较多,那么相应的可以递归的次数就越少
还有1种栈溢出会报OutOfMemoryError异常,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常,比如将上面的递归改成多线程版就会出现这种问题
参考:《深入理解Java虚拟机》:周志明
|