深入理解java虚拟机的内存模型以及垃圾回收机制

搜索到的谈及jvm内存模型的博客都是在讲概念,几乎没有能把程序执行过程联系起来去讲,相信看完这篇blog,对java内存管理会有一个清晰的认识。

java虚拟机(jvm)是一个什么概念

相信大家一定会说:堆;栈;面试必问;一处编译,到处浪;垃圾回收。。。。。。

不用代码讲就是耍流氓,全文以此简单小代码为例
/**
*created by 冰·封
*/
public class Test
{
public static final int constInt=520;   //静态常量

public int mathAdd()
{
int a=3;
int b=1;
int c=(a+b)*5;
return c;
}
public static void main(String []args)
{
Test test=new Test();          
test.mathAdd();
}
}

深入理解java虚拟机的内存模型以及垃圾回收机制_第1张图片
我们写好的代码首先会被编译成字节码文件,java虚拟机会将字节码文件放到到类加载子系统里面,类加载子系统会将字节码文件加载到运行时数据区(java虚拟机内存空间)里面,执行引擎会从数据区里面拿出数据去运行。

本文只解析内存模型,面试高频的这一块。

接下来我们看看内存模型里面这几块是怎么关联的

学过java基础的人都知道new出来的对象都是放入堆里的,堆有很多深层次概念,最后讲。

方法区(元空间)

为什么把它放在前面去说,因为下面其他模块解析要用得到。
在jdk1.8之前,有个名字叫永久代,1.8之后改了名字叫元空间,实际上,我们的class文件被类装载子系统主要加载到的地方就是方法区。
方法区主要存储静态变量、常量以及类元信息
什么叫类元信息呢?就想象成把类拆了。
class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,class文件转变为方法区中的一段特定的数据结构。这个数据结构会存储如下信息:类型信息、字段信息、方法信息、其他信息。(这里不用去理解,知道就好)
每个类的这些类元信息,无论是在构建这个类的实例还是调用这个类某个对象的方法,都会访问方法区的这些元数据。(记住,下面用得到)

栈(全称 线程栈)

栈是java虚拟机内存里一块小空间,什么是栈呢,栈是一种数据结构,计算机类考研必考,(某大学学习数据结构的链接)这里知道先进后出的特点就可以了,我们看程序中的mathAdd方法,里面有一些局部变量,main方法里也有一个test这个对象变量,它也是一个局部变量,这里变量就是放在栈内存空间里面。
栈是一个线程独有的,每个线程都应该有自己独立的栈,都应该有自己独立的存储空间。
深入理解java虚拟机的内存模型以及垃圾回收机制_第2张图片
每个方法中都有属于自己的局部变量,他们的作用域不一样,同一个线程中的变量都存放在这个栈中,为了区分,我们引入了栈帧这个概念。那什么是栈帧呢?栈帧就是每个方法中自己的变量的存储空间。先执行主方法,给main()方法先开辟一块空间,将main()方法进栈,再执行mathAdd()方法,将mathAdd方法入栈。mathAdd方法用完后,mathAdd方法出栈销毁。
主方法main()执行完成后出栈销毁。
深入理解java虚拟机的内存模型以及垃圾回收机制_第3张图片
其实除了局部变量表以外,栈帧里还有一些其他的组成部分
如操作数栈,动态链接,方法出口
深入理解java虚拟机的内存模型以及垃圾回收机制_第4张图片
不知到读者有没有研究过字节码,我们在cmd控制台中javac Test.java ,获得Test.class文件,用sublime Test打开,
深入理解java虚拟机的内存模型以及垃圾回收机制_第5张图片
这就是class文件,研究java虚拟机底层要研究这些字节码,这些都有着具体的含义,在oracle官网可以查到,但这些都不是给人看的,是给机器看的,jdk给我们提供了一个命令,javap,为了让我们的字节码文件更可读,变了一种形式去展示。
深入理解java虚拟机的内存模型以及垃圾回收机制_第6张图片

可以看到,javap -c ,可以进行对class文件进行反编译。
深入理解java虚拟机的内存模型以及垃圾回收机制_第7张图片
Javap -c命令所产生的与我们所写的代码几乎对应。
我们看看字节码隐藏了什么秘密,又和我们的内存模型有什么联系呢?
这些东西不要求看明白,只要知道怎么去查就可以了。oracle官网可以下载相关文档,我这里有下载好的 JVM指令,提取码sdtm

内存变量表和操作数栈(联系在一块)

看mathAdd方法里面的指令,结合代码一起一行行看下去
int a=3; 所对应的指令:
1、iconst_3 将int类型常量3压入栈
2、istore_1 将int类型值存入局部变量1
第一步先将常量3放入操作数栈中,当运行到第二步时,jvm会在局部变量中开辟一块空间用来存储变量a,然后将操作数栈中常量3拿走放到局部变量开辟的空间里,此时a=3。
int b=1; 所对应的指令:
1、iconst_1 将int类型常量1压入栈
2、istore_2 将int类型值存入局部变量2
第一步先将常量1放入操作数栈中,当运行到第二步时,jvm会在局部变量中开辟一块空间用来存储变量b,然后将操作数栈中常量1拿走放到局部变量开辟的空间里,此时b=1。
深入理解java虚拟机的内存模型以及垃圾回收机制_第8张图片
int c=(a+b)*5; 所对应的指令:
1、iload_1 从局部变量1中装载int类型值
2、iload_2 从局部变量2中装载int类型值
3、iadd 执行int类型的加法
4、iconst_5 将int类型常量5压入栈
5、imul 执行int类型的乘法
6、istore_3 将int类型值存入局部变量3
第一步先将局部变量1中值(也就是a的值3)装在操作数栈中(但变量a还在局部变量表中,因为此方法还没有执行完),第二步将局部变量2中的值(也就是b的值1)装在操作数栈中,第三步将操作栈中处于最上面的两个值弹出相加得到一个值放在操作数栈中,第四步将常量5放入操作栈中,第五步,将操作数栈最上面两个数弹出相乘得到一个值放在操作数栈中,第六步,在局部变量表中开辟一块空间存储变量c,将操作数栈中的栈顶元素值放到变量c的存储空间中。
return c; 所对应的指令:
1、iload_3 从局部变量3中装载int类型值
2、ireturn 从方法中返回int类型的数据
先将局部变量3中值(也就是c的值)装在操作数栈中,返回int的值。
综上所述:局部数据表显然是存储局部变量的,操作数栈是数据处理的中转站,只不过是以栈的形式。

动态链接

我们看一下main()方法栈帧
深入理解java虚拟机的内存模型以及垃圾回收机制_第9张图片
主函数里有个变量test和mathAdd方法里面的变量a,b,c有些不同,a,b,c对应的值是常量,test对应的值是一个对象,我们知道new出来的对象是放在堆里的,test(引用)有个指针指向堆中的对象。假设我们再new一个对象 Test test2=new Test(); test2.mathAdd(); 这是又有了一个变量指向新new出的对象。
test和test2对象从同一个模板里new出来的,内部结构应该一样。
其实,每一个被new出来的对象都有一个指针指向方法区的类元信息,就是谈方法区要记住的那句话。深入理解java虚拟机的内存模型以及垃圾回收机制_第10张图片
test和test2调用mathAdd方法是通过指针到方法区找到类元信息,找到这个mathAdd方法,
动态链接就是程序在运行过程中通过指针动态的去寻找这些方法

方法出口

mathAdd方法在主函数中被调用,用完后,还要执行后面的代码,方法出口就会保存当时进入mathAdd方法时的地址指针和一些现场信息。
在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的程序计数器的值作为返回地址,,如果是因为异常退出的,则是需要通过异常处理表来确定。

程序计数器

大家公认的说法:指向下行代码对应地址的指针。好理解的:
为什么字节码文件中的指令能顺序执行下去呢?是因为在每个当前指令被执行之后,程序计数器自动改变自身的值指向顺序中的下一个指令。每个线程都有自己独立的程序计数器。深入理解java虚拟机的内存模型以及垃圾回收机制_第11张图片

本地方法栈

这个程序中没有用到,new Thread().start()这个方法里就有本地方法。
哪里有???
打开eclipse
在这里插入图片描述
按住ctrl,点击Thread,进入Thread,找到start方法,
深入理解java虚拟机的内存模型以及垃圾回收机制_第12张图片
我们可以看到方法中又调用了start0这个方法,ctrl,点击它,
深入理解java虚拟机的内存模型以及垃圾回收机制_第13张图片
这就是java程序中的本地方法,底层不是java语言实现的,是c语言实现的。到windows类库中找c语言实现的dll文件。c语言代码中也有一些局部变量,这些就保存在本地方法栈中。几乎不用,知道概念就可以。

当我们把所有的方法执行完,这个线程也就结束了,线程1中所有的东西全都销毁了,程序结束了,但java虚拟机还运行着,然保存在堆中的对象还在,这个对象就是垃圾,要清理,不然会导致oom(内存溢出)
这就引起了堆更为复杂的内存结构,堆的分代模型
深入理解java虚拟机的内存模型以及垃圾回收机制_第14张图片
Eden,from,to内存分配8:1:1
分代模型将堆分为年轻代和老年代,我们新new出来的对象都是放在年轻代中,年轻代又分为伊甸园区(Eden)和Survivor区,我们新new出来的对象真正是放在伊甸园区
假设我们给堆初始化分600M的空间,老年代默认2/3,400M,年轻代200M,伊甸园160M,from区20M,to区20M。
我们不停往伊甸园区new对象,那伊甸园区总会放满,那java虚拟这个执行引擎就会触发一个线程,叫做gc(垃圾收集)(这里说的gc是minor gc,不是full gc)。没有任何指针指向它们,这些对象就成了无用对象,gc收集无用的对象。而还有指针指向的对象(可能另一个线程里的)会被放到Survivor区from里面去,from区也会被放满,也会执行gc,将无效的对象清理掉,有用的对象(比如像连接池对象,永远不会被清理掉)放入to区,这时to区变成from区,from区变成to区,满了就触发gc往另一个区转,当在Survivor区来来回回转了15次(默认),还活着,将其放入老年区,当老年代也放满后,就会触发full gc。当full gc也没法释放出任何对象,这时就会提示oom(内存泄漏)。

java虚拟机还有很多东西,学习方向我这有,评论找我要。

你可能感兴趣的:(java)