我们已经习惯于写出类似Object obj=new Object();型的语句,然而背后究竟发生了什么?从JVM内存结构分析更有助于加深理解记忆。下面试着举例说明:

JVM内存分区:

这里写图片描述

如上图所示,JVM主要分为以上几块:程序计数器,本地方法栈,虚拟机栈,堆和方法区。稍微粗糙一些得分法是JVM分为栈和堆,栈包括虚拟机栈,本地方法栈,程序计数器,堆分为堆和方法区。需要说明的是这里所说的Java内存和硬件上的内存并不能完全对应,Java内存还可能包含CPU的寄存器,高速缓存等。

程序计数器

程序计数器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则程序计数器中不存储任何信息。

本地方法栈

众所周知,Java的底层是C实现的,所有调用的C代码都被称为本地方法(native method)。JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

虚拟机栈

JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。

以上三块是伴随着线程同生共死的,生命周期由编译器控制。

堆 Java Heap

它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Java Heap是线程共享的。Heap中的对象的内存需要等待GC进行回收。

方法区

方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息。方法区域也是全局共享的,在一定的条件下它也会被GC。

其中,方法区中有一个运行常量池:存放的为类中的固定的常量信息、方法和Field的引用信息等。

内存分配过程

了解了JVM的内存分区,接下来就以实例分析到底变量都存放在哪里。由于下面的例子和本地方法栈,程序计数器关系不大,所以暂时略去。

一段司空见惯的代码:

public class student{
    String name;
    Integer age;
    String score    
    public void study(){
        system.out.println("I am studing");
    }   
    public void sayHello(){
        system.out.println("Hello");
    }
}

public class computer(){    
    String brand;
    String speed;
}

public class test(){
    public static void main(String args[]){
        Student stu1 =new Student();
        stu1.name="wang";
        stu1.age=24;

        Student stu2=new Student();
        stu2.name="li";
        stu2.age=23;

        stu1.computer.brand="Lenovo";
        stu1.speed="2.5GHz"
    }
}

内存存储详情图:

这里写图片描述

由图中所示:
栈存储的是局部变量,如上述代码中的stu1,stu2。
堆的heap区存储的是new创建出的变量。
堆的方法区存储的是类的方法(动态,静态),变量(动态,静态),常量。

我们知道:Java中,除了基本数据类型,都是引用类型,也就是说这些数据类型存的都是引用地址,直至一个接一个的引用,最终指向到基本类型,基本类型存储的才是值的本身。

这样就可以理解图中的所有箭头,一条箭头代表一个引用,箭尾是引用的地址,箭尖是被引用的变量。

由此,我们来剖析代码在内存中是如何实现的:
1,首先JRE加载了编译好的class文件,将相关类的方法,变量,常量都放到了类区。然后开始执行语句。

2,执行第一句语句Student stu1=new Student();
按照Java由右向左逐步执行的机制,首先是运行new Student()在Java Heap中开辟出一个可以存储Student类所有属性和方法的引用的连续空间。方法都指向了方法区的对应方法,而属性在未赋值的情况下都是null或0。
之后是执行Student stu1在栈空间内分配一个空间存储一个存储Student类型变量引用的空间。
最后运行等号,等号就是将变量区中变量的地址赋值给栈中的变量,即建立了引用关系。

3, 执行第二句:stu1.name="wang";“wang”是一个已经存在于方法区的常量,stu1.name是变量区中的一个空变量,等号将两者连接,将“wang”的地址赋给了stu1.name。
以此类推,以下都如图所示。被赋值的变量存储了相应的地址,未赋值的保持为空。

4,值得注意的是,Java Heap的变量也可以相互引用,例如stu1.computer就是从一个变量引用到另一个变量。

静态变量与方法与与一般变量和方法所不同的是,在用new关键词创建类对象时,这个类里面的static变量和方法不会被初始化,栈中的对象直接对它们进行引用。因此,静态方法无需实例化即可使用,静态变量被类的所有实例所共享。

顺带提一句垃圾回收机制(garbage collection),Java采用的是自动的垃圾回收机制,当虚拟机,发现某一个变量在之后不会被使用到,例如图中stu1已经用完了,就会把stu1引用的一整块在变量区的内容释放掉。

垃圾回收是一个后台线程,在不知不觉中帮我们管理着内存,好处是解放了程序员,编写程序少操了一份心,坏处是相比C++这种需要主动释放内存的语言少了一点灵活性。Java可以使用System.gc()来提醒虚拟机可以考虑进行一下垃圾回收,但是系统可能不一定听你的,你可以建议,但做不做由它,Java这种脾气在很多地方都有体现。

总结一下,
Java内存管理分为两大块,栈和堆,堆又分为变量区和类区;
栈引用变量区,变量区引用类区,变量区还相互引用,类区内部也相互引用;
Java的有自动的垃圾回收机制,可以将不再被使用的变量释放。
核心:一定要深入理解Java的引用机制。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐