JVM的基本结构图:
由图可知,JVM的内存区域主要可以划分为5块:
- JVM栈 (Java Virtual Machine Stacks)
- 堆内存 (Heap Memory)
- 方法区 (Method Area)
- 本地方法栈 (Native Method Stacks)
- 程序计数器 (Program Counter (PC) Register)
一、JVM栈
- 程序是在栈内存中运行的,所以栈内存解决的是程序运行时的问题
- Java以栈帧为单位保存线程的运行状态,虚拟机只会对栈执行两种操作:以栈帧为单位的压栈或者出栈
- 一个线程独占一个Java栈(栈里的数据是线程私有的)
- 存储的是基本数据类型和堆中数据的引用(引用地址)
- 分为三个部分:基本类型变量区、执行环境上下文、操作指令区
- 异常:
java.lang.StackOverFlowError
二、堆
- 堆内存解决的是数据存储的问题
- 所有线程共享java堆
- 存储的是对象和数组(对象本身)
- 动态的分配内存(运行时分配),生命周期(不确定)不需要预先告诉编译器,Java的垃圾回收机制会自动收走不使用的数据
- 由于运行时动态分配内存,存储数据较慢
- 异常:
java.lang.OutOfMemoryError
三、方法区
- 又称静态区
- 存储每个类的信息(包括类名、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码
四、本地方法栈
和java栈的作用差不多,只不过是为JVM使用到的native方法(使用非Java语言实现的方法)服务的
五、程序计数器
- 用于保存当前线程执行的内存地址
- 由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的
- 注意这个区域是唯一一个不抛出
OutOfMemoryError
的运行时数据区
下面通过AppMain.java和Sample.java两块代码进一步说明
//运行时, jvm把AppMain的信息都放入方法区
public class AppMain {
//main 方法本身放入方法区
public static void main(String[] args) {
//test1是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面
Sample test1 = new Sample("测试1");
Sample test2 = new Sample("测试2");
test1.printName();
test2.printName();
}
}
//运行时, jvm把Sample的信息都放入方法区
public class Sample {
//new Sample实例后, name 引用放入栈区里, name 对象放入堆里
private String name;
/** 构造方法 */
public Sample(String name) {
this.name = name;
}
/** 输出 */
//print方法本身放入方法区里
public void printName() {
System.out.println(name);
}
}
-
启动虚拟机进程,程序从AppMain的开始,先从classpath中找到并读取AppMain.class二进制文件(编译后),然后把AppMain类的类信息和方法信息放入方法区,这个过程叫AppMain类的加载过程;
-
Java虚拟机定位到方法区AppMain类中的main()方法的字节码,开始执行它的指令,第一条语句是:
Sample test1 = new Sample("测试1");
-
接着Java虚拟机到方法区中查找Sample类的信息,没有找到,然后通过步骤1重新加载Sample类到方法区;
-
在堆中为Sample对象实例分配内存,这个实例持有指向方法区的Sample类的信息的引用(引用是指Sample类的信息在方法区中的内存地址)
-
每一个线程都有一个栈,栈里面的元素被称为栈帧,每当线程调用一个方法的时候就会往栈里压入一个新帧,这里的帧是用来存储方法的参数、局部变量和运算过程中的临时数据。位于**“=”前的test1是一个在
main()
方法中定义的变量,它是一个局部变量,因此,它被会添加到了执行main()
方法的主线程的java方法调用栈中,而“=”**将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用 -
接下来,JAVA虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后依次执行它们的
printName()
方法。当JAVA虚拟机执行test1.printName()
方法时,JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,再根据Sample实例持有的引用,定位到方法去中Sample类的类型信息,从而获得printName()
方法的字节码,接着执行printName()
方法包含的指令
六、疑问区
1、Q: Java中的参数传递(传值呢?还是传引用?)
A:
程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题,不会直接传递对象本身;
对象传递是引用值传递,原始类型数据传递是值传递; 实际上这个传入函数的值是对象引用的拷贝,即传递的是引用的地址值,所以还是按值传递。
2、Q: Java对象的大小如何计算?
A:
Object obj = new Object();
这样在程序中完成了一个java对象的声明,obj对象所占的空间为:
4byte(java栈中保存引用的所需要空间)+ 8byte(java堆中对象所需的空间) = 12byte
所有的java非基本类型的对象都需要默认继承Object对象,因此不论什么样的java对象,其大小都必须是大于8byte
同时java对象大小是8的整数倍,因此obj对象的大小至少为16byte
七、拓展
对象引用类型分为强引用、软引用、弱引用和虚引用
1、强引用:声明对象时虚拟机生成的引用
Sample sample = new Sample();
sample
为强引用,不会被垃圾回收
2、软引用:根据系统剩余内存来决定是否需要回收
换句话说,虚拟机在发生java.lang.OutOfMemoryError
时,肯定是没有软引用存在的
3、弱引用:弱引用与软引用类似,但在进行垃圾回收时,是一定会被回收掉的
因此其生命周期只存在于一个垃圾回收周期内
4、虚引用虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
虚引用主要用来跟踪对象被垃圾回收器回收的活动
参考地址: