java虚拟机(JVM)堆、栈、方法区的介绍

1,037 阅读6分钟

JVM的基本结构图:

由图可知,JVM的内存区域主要可以划分为5块:

  1. JVM栈 (Java Virtual Machine Stacks)
  2. 堆内存 (Heap Memory)
  3. 方法区 (Method Area)
  4. 本地方法栈 (Native Method Stacks)
  5. 程序计数器 (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);
    }
} 
  1. 启动虚拟机进程,程序从AppMain的开始,先从classpath中找到并读取AppMain.class二进制文件(编译后),然后把AppMain类的类信息和方法信息放入方法区,这个过程叫AppMain类的加载过程;

  2. Java虚拟机定位到方法区AppMain类中的main()方法的字节码,开始执行它的指令,第一条语句是:

    Sample test1 = new Sample("测试1");
    
  3. 接着Java虚拟机到方法区中查找Sample类的信息,没有找到,然后通过步骤1重新加载Sample类到方法区;

  4. 在堆中为Sample对象实例分配内存,这个实例持有指向方法区的Sample类的信息的引用(引用是指Sample类的信息在方法区中的内存地址)

  5. 每一个线程都有一个栈,栈里面的元素被称为栈帧,每当线程调用一个方法的时候就会往栈里压入一个新帧,这里的帧是用来存储方法的参数、局部变量和运算过程中的临时数据。位于**“=”前的test1是一个在main()方法中定义的变量,它是一个局部变量,因此,它被会添加到了执行main()方法的主线程的java方法调用栈中,而“=”**将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用

  6. 接下来,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、虚引用虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收

虚引用主要用来跟踪对象被垃圾回收器回收的活动


参考地址

blog.csdn.net/rodbate/art…