读 - 深入理解java虚拟机 - 笔记(一) - java内存区域模型

时间:2022-04-11 16:48:39

使用java也有好几年了,记得大学时是在听老师讲,觉得好像没多少东西啊,工作第一年并没有找到java岗位,而是做cobol去了,估计知道cobol的人少之又少吧。的确,这个语言真是太古老了,而且做这个最痛苦的是,很多资料都是企业内部资料,外面能找到的寥寥无几。

干了一年,更多的时候都是在发邮件,扯皮,解决bug。银行的系统追求的是稳定而不是创新,做的很憋屈,又想重新回到java岗位上,但是简历投了不少,苦于之前并没有java项目经验,哎,只能说毕业时决策失误,不过好在基础不错,到是有几家给了offer,选择了一家自研公司,进去了。

得益于基础知识不错,ssm框架和mvc思想还过得去,上手很快。代码写出来问题也不多,评审时也过得去。也算成功入门了,入门之后很长时间都是熟悉业务,研究系统,没有过多的时间抽身出来读书,大概快一年的时候,对于手头工作能很好的处理,问题也能很容易解决了,此时终于有时间可以自己去读一读书了,本人其实读书不是很有耐心,但是读过几次,写点博客,发现还是很有动力的。

前面的jdk源码还是会继续读下去的,最近也刚入手了并发编程的书,准备结合源码去读一读,顺带一起的还是购入了虚拟机,关于虚拟机之前在碰到静态变量时其实已经去了解了不少,但是并没有记录心得。


分割线----------------------------------------------------------------------------------------------

废话太多。

对于平时编写java代码来说,内存的申请,使用都不是我们关心的事,作为小码农来说,就是new,new,new,所以今天就来看看jvm是如何来管理内存的。网络盗图一张,这图很常见,多的很,自己也就不画了。

读 - 深入理解java虚拟机 - 笔记(一) - java内存区域模型

看最外面是叫运行时数据区,我们知道java是要编译之后才能运行的。那么首先java编译期会将.java文件编译成.class文件,然后jvm中的类加载器会去加载类的字节码,加载完毕之后呢,会交给jvm执行引擎。对于怎么加载的,怎么编译的,不在第一张讨论啊,以后会看到的,到时候再说。引擎执行时肯定会有一个空间来保存这些数据和相关信息,这个空间就是运行时数据区了,Run Time Data Area嘛,因此,可以说平时所谓的内存管理就是针对这段空间进行管理的。

通常很多人都会笼统的说,虚拟机内存分为栈区,堆区,这是很粗略的分,像我们java从业者肯定不能满足于此啊。

上图可以看见,堆区和方法区是线程共享的,这也是多线程需要注意的,为什么new出来的对象,你修改之后,其他线程会读取到脏数据,因为在虚拟机中它是共享区域,像所谓的栈区,也就是虚拟机栈,本地方法栈,程序计数器都是线程独有的,每一个线程都存储自己独有的数据,不难理解,在方法内部新建的临时变量为什么不会出现并发问题,像前面读取的CAS的操作时,为什么内部会出现一个临时变量,因为它是这个线程独有的,其他线程看不见。

一.程序计数器.

程序计数器是指CPU中的寄存器,保存的是当前执行的指令的地址(也可以认为保存的是下一条指令所在存储单元的地址)。当CPU需要执行指令时,需要从程序计数器中读取当前需要执行的指令所在的存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器变自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至完成所有指令。

jvm中的寄存器并非物理概念上的CPU寄存器,但是功能在逻辑上是等同的,也是用来指示应该执行那条指令的。可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖它。

可以想象一下,它的工作是需要知道程序下一条指令应该运行什么。那么就意味着每个线程都应该有自己的程序计数器,这就是为什么程序计数器是程序私有的。事实上,jvm中多线程运行时是通过轮流切换来获得程序执行时间的,因此,在某个具体的时刻,一个CPU只会执行一条线程中的指令,那么此时问题就会出现,在线程切换后CPU是如何知道从线程1切换到线程2之后,执行线程2中哪一条指令呢?所以对于每个线程都记录了自己的程序计数器。

jvm规范中还有一个规定,如果线程执行的是一个java方法,那么记录的是当前正在执行的字节码指令地址,如果执行的是Native方法,这个计数器就为Undefined。

对于上面为什么记录是Undefined,我也不知道是为什么,可以参考下面回答。

https://www.zhihu.com/question/40598119

需要注意的最后一点,在程序计数器这一块内存中,是没有OOM问题的,这个应该在规范中有所体现,他能存放下java方法的字节码地址值。


二.java虚拟机栈

与程序计数器一样,虚拟机栈也是程序私有的,它的生命周期与线程一致,线程结束它就结束了。每个方法在执行时都会创建一个栈帧,存放如局部变量,操作数栈,动态链接,方法出口等信息,每个方法从调用开始到执行结束,就对应着栈帧从入栈到出栈的过程。

局部变量表:存放的是局部变量(包括方法定义的非静态变量以及形参),这些局部变量就是基本类型变量,是直接存储变量的值,而对于引用变量来说,存储的是指向对象的引用值。这些变量在编译期就能确定大小,因此程序执行期间局部变量表的大小是不会改变的。

操作数栈:程序中所有计算过程都是借助操作数栈来完成的。操作数栈是一个LIFO栈(Last-In-First-Out),栈帧中操作数栈的长度由编译期决定。栈帧刚刚创建时,操作数栈是空的。java虚拟机提供字节码从局部变量表或者对象实例的字段中复制常量或者变量值到操作数栈中。当然也有指令取出这些数据。因此虚拟机把操作数栈作为工作区。

begin
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end

在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。图5-10详细表述了这个过程中局部变量和操作数栈的状态变化,图中没
有使用的局部变量区和操作数栈区域以空白表示。

读 - 深入理解java虚拟机 - 笔记(一) - java内存区域模型

动态链接:这个链接是指向运行时常量池的引用,这些链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。符号引用和直接引用是类加载里的知识点,在这边不做阐述,后续里会有。

方法出口:当一个方法执行完之后就要返回调用它的地方,这边就是保存一个方法返回地址。

虚拟机规范中,对这个区域定义两个异常情况,SOF 和OOM ,SOF异常是当线程请求的栈深度大于虚拟机所允许的栈深度时,抛出SOF异常,这是基于虚拟机栈被实现为固定大小内存时出现的。如果虚拟机被实现为动态扩展内存大小时,如果扩展时无法申请到足够大的内存时,会抛出OOM异常。

三.本地方法栈

本地方法栈和虚拟机栈原理类似,虚拟机栈是执行的java方法,而本地方法栈执行的是Native方法。因此也会有SOF和OOM异常出现。

四.堆

堆是虚拟机中内存占用最大的一块,堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,虚拟机规范中指出,所有的对象实例和数组都需要在堆上分配。堆是垃圾回收器管理的主要区域,很多时候被称为GC堆,它可以物理上不连续,只要逻辑上连续即可,可通过-Xmx和-Xms控制。如果堆中没有内存完成实例分配,就会出现OOM异常。

五.方法区

方法区与堆一样,被各个线程共享,用于存储虚拟机加载的类信息,常量,静态变量,及编译器编译后的代码等数据,虽然虚拟机规范将方法区描述为堆的一个逻辑部分,但是它确有个别名,非堆。

六.运行时常量池

运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。它是具有动态性,java语言并非要求常量一定只有编译期才能产生,运行时也能将新的常量放入,如String.intern()方法。