“再也不用像写C++那样自己管理内存了,使用java的你,只需要注重业务逻辑。”在刚开始学习java时被告知这样的话,倍感欣喜。这可省了不少麻烦,开心之余有种迷之不祥预感也渐渐萦绕心头:真的不用关心jvm的内存管理吗?
后来我认为我转行去算命也不错,因为迷之不祥预感应验了:若想从初中级java程序员向中高级进阶,jvm的内存模型及管理是必备知识。既然如此,那么就让我们好好研究一下java这位优雅少妇的漂亮花裙子->jvm以及裙下所隐藏的秘密花园吧。
咱们都知道,计算机的内存就像中药铺的一个个药格子组成起来的巨大存储空间,包括jvm的众多进程独立管理各自的内存区域。
在运行时,为了方便管理,Jvm将自己的内存区域在逻辑上划分为:程序计数器,虚拟机栈,本地方法栈,堆和方法区共5个区域,如下图:
程序计数器:存储线程所执行的字节码行号。Java的多线程是通过线程来回切换,各个线程独占一个cpu的一个时间片(对多核cpu而言是一个内核的一个时间片)实现的线程的同步执行,那么当某个线程获得到cpu的使用权时,从程序计数器获知从字节码的哪一行开始继续执行。故而,程序计数器是线程隔离的。该区域占用非常小量的空间,线程私有,并且没有OutOfMemoryError。
虚拟机栈:存储线程执行的方法的栈帧。每个线程的方法在执行时会创建一个用于存储该方法局部变量、方法出口等信息的栈帧。而方法的执行既是栈帧的压栈和弹栈的过程。故而虚拟机栈是在jvm中存在线程运行的情况下才会存储数据,当线程方法执行完毕,此线程的虚拟机栈亦清空。
在创建栈帧时,方法的局部变量包含8种基本数据类型boolean,byte,char,int,short,long,double,float和引用类型。其中引用类型暂时可以简单理解为存储对象的地址,故而栈帧占用存储空间的大小是可以在编译期准确计算的。
此区域为线程隔离区域,各个线程拥有各自的虚拟机栈,故而局部变量线程间不可共享。
虚拟机栈有深度限制也有最大存储空间限制,故而此存储区域会抛出*StackOverFlowError*和*OutOfMemoryError*。
本地方法栈:作用与虚拟机栈类似,存储的为本地方法的信息。在Hotspot中,虚拟机栈与本地方法栈公用一块存储区域,并且也会抛出StackOverFlowError和OutOfMemoryError。
方法区:存储虚拟机加载的类信息、静态变量、常量等信息。类加载完成后,随即占用此区域的内存空间,并非等待线程运行时才消耗存储空间。
此区域与上述三个区域不同,为线程间共享的区域,即方法区存储的内容并没有按照各个执行线程分别存储,也不会因为线程执行完毕而回收。可以理解为在内存中它只存储一份,各个线程可以同时观察到。
类也不可无限制加载,故而虚拟机可以对此区域进行回收,但是当加载类时此区域存储空间不足时,亦会抛出OutOfMemoryError。
特别之处,常量处于方法区当中,逻辑上作为方法区的一个子区域管理,即常量池。常量可以在编译时产生,亦可在程序运行时产生,常用的方式为String的intern()方法。
堆:存储对象。咱们的对象都存储在此区域,如果还没找到对象的可以在这个区域找找看,不过别报太高期望,因为这里的对象可是会失踪的。
堆,用于存储程序运行过程中所创建的所有对象,例如数组对象,自定义对象等。此区域所占存储空间最大。
虽然几乎所有对象都是在线程中创建的,但是对象存储时却没有按照线程存储。即堆为线程共享区域。即使这样的设计,在正确使用线程的情况下,是不会导致各个线程的数据混乱,理由是对象虽然是线程共享的,但是对象的引用则可以控制为线程隔离的(例如作为方法局部变量的引用)。
堆内存的管理是虚拟机内存管理的重头戏,因为咱们常说的内存回收(也就是GC)主要的工作区域就是堆内存。对内存用于存储对象实例,jvm会自动收回不再使用的对象所占存储空间。
因为GC算法,堆内存又在逻辑上区分为新生代、老年代。新生代区分为eden和survivor区域。但是这种区分方式仅针对使用分代回收算法的虚拟机。
当此区域的内存空间不够分配时,会抛出*OutOfMemoryError*。
当我们对jvm内存模型有了初步的了解,接下来我们可以进一步的分析java代码执行过程中,代码与数据是如何在内存中存储的。
小弟原创,欢迎拍砖。