文章目录
一、JVM内存结构
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8 和之前的版本略有不同。
JDK 1.8 之前:
JDK 1.8 :
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
1.1 程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
从上面的介绍中我们知道程序计数器主要有两个作用:
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
1.2 Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
1.3 本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
1.4 堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
1.5 方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
1.6 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
1.7 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
二、实例代码
1.创建一个类、包含各个属性和方法
代码如下(示例):
package student;
public class Student {
int id;
String sname;
int age;
Computer comp; //计算机
void study(){
System.out.println("我在认真学习!!,使用电脑:"+comp.brand);
System.out.println("上面调用了方法,存在方法区");
}
void play(){
System.out.println("我在玩游戏!王者农药!");
System.out.println("上面调用了方法,存在方法区");
}
//构造方法。用于创建这个类的对象。无参的构造方法可以由系统自动创建。
Student(){
System.out.println("调用了无参的构造方法!");
System.out.println("上面调用了方法,存在方法区");
}
public static void main(String[] args) {
Student stu = new Student(); //创建一个对象
System.out.println("新建了对象,存在堆中;stu指向该对象,stu存在方法栈帧中");
stu.id=1001;
stu.sname= "高淇";
stu.age = 18;
System.out.println("设置了属性,在常量池创建了");
System.out.println("指向了运行时常量池");
Computer c1 = new Computer();
System.out.println("新建了对象,存在堆中;c1指向该对象,c1存在方法栈帧中");
c1.brand = "联想";
System.out.println("设置了属性,在常量池创建了");
stu.comp = c1;
System.out.println("指向了堆中的对象");
stu.play();
stu.study();
}
}
class Computer {
String brand;
}
2.结果分析
输出结果:
调用了无参的构造方法!
上面调用了方法,存在方法区
新建了对象,存在堆中;stu指向该对象,stu存在方法栈帧中
设置了属性,在常量池创建了
指向了运行时常量池
新建了对象,存在堆中;c1指向该对象,c1存在方法栈帧中
设置了属性,在常量池创建了
指向了堆中的对象
我在玩游戏!王者农药!
上面调用了方法,存在方法区
我在认真学习!!,使用电脑:联想
上面调用了方法,存在方法区