文章目录
前言
一篇文章让0基础小白也能彻底搞明白JVM内存模型的文章
一、java语言的跨平台特性
java特性跨平台,如何做到的?
不同系统(如WIN、linux、Mac)提供不同的jdk,jar有jvm有对应系统的实现;
比如写一份hellowarld代码,编译成class文件后,然后虚拟机根据不同的平台,生成对应的机器码;
如果是其他语言,可能还面临在不同的平台上,还需要修改一定的代码,因为每个平台生成的机器码会有差异;
二、JVM整体结构及内存模型
先看一下完整的整体结构,有个印象,然后接着往后看,来一步步解析
三、JVM虚拟机主要组成部分以及作用
1、组成
JVM包含2个子系统和2个组件;
2个子系统为ClassLoader(类加载子系统)、Execution engine(字节码执行引擎);
2个组件为Runtime data area(运行时数据区)、Native interface(本地接口)
ClassLoader(类加载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区的元空间。
Execution engine(执行引擎):执行classes中的指令。
Native interface(本地接口):与native libraries交互,是其他编程语言交互的接口。
Runtime data area(运行时数据区):这就是我们常说的JVM的内存。
2、作用
首先通过编译器把java代码转换成字节码,类加载器再把字节码加载到内存中,将其放在运行时数据区的元空间内,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,在交给CPU去执行,而这个过程需要调用其他语言的本地库接口(native inteface)来实现整个程序的功能。
JVM执行顺序简单总结:
类加载子系统是由C++实现的,通过他把字节码文件丢到内存区域,最后在通过字节码执行引擎(也是C++实现),来执行内存区域的代码;
*
3、java程序运行机制
1、首先通过ide开发工具编写java源代码,源文件后缀为.java;
2、再利用编译器javac命令将源代码编译成字节码文件,字节码文件的后缀名为.class;
3、运行字节码的工作是由解释器(java命令)来完成的;
4、最终由解析器执行引擎翻译后交给CPU执行;
大白话总结
类的加载是指将类的class文件中的二进制数据读入到内存中,将它放在数据区的方法区内,然后在堆区创建一个java.lang.class对象,用来封装类在方法区内的数据结构;
四、内存模型(运行时数据区)
Java虚拟机在执行java程序的过程中会把它所管理的内存区域划分成若干个不同的数据区域。
这些区域都有各自的用途、以及创建和销毁的时间;
内存模型内分为5个区域:
其中上图标黄的代表堆和方法区(元空间)是所有线程共享区域;
栈、本地方法栈、程序计数器是每个线程私有的;
1、java堆
java虚拟机中内存最大的一块区域,是被所有线程共享的,几乎所有对象实例都在这里分配内存;
堆里面又有3块区域;
分别是Eden(新生代区)、Survivor(幸存者区)、老年代区;
new出来的对象和字符串常量池都放在Eden区;如果Eden区放满后,会触发字节码执行引擎,他后台会开启Minor GC垃圾收集线程,会把无用的垃圾做垃圾收集;
2、java虚拟机栈(线程)
用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
只要有一个线程开始运行主方法,就会在栈内存的空间里,单独给这个线程划分专属的内存空间,用来存放这个线程里的所有局部变量,这就是栈内存空间;
与栈的关系
栈里面存放的局部变量如果是对象类型,哪他的对象实例都在堆里面,栈里面存放的都是对象在堆里面的内存地址;
什么是栈帧?
当执行每一个方法时,都会给每一个方法分配一个新的内存空间,存放里面的局部变量、操作数栈、动态链接、方法出口;
栈帧的结构:后进先出;
大白话解释:
比如方法的A里面调用了方法B,B方法调用C,在程序里这种情况就是后调用的反而先执行;
当执行完方法时,这个空间就会被释放掉;
1、局部变量:
存放局部变量;
2、操作数栈:
用来存放局部变量的实际值;
这里的操作数栈、其实和ajva的栈是相似的结构;
有赋值或者计算操作时,就会把局部变量的值从局部变量表里拿出来,放到操作数栈里,最终做操作的时候,会从栈顶里把要计算的值取出来,进行计算,然后把计算的结果重新放入栈顶;
3、动态链接:
程序运行时,去调用到具体某个方法时,直接根据符号找到内存中的地址,直接引用;
大白话解释;
比如A方法里调用B方法,B方法是在运行期间去解析B方法这个符号,从而找到对应的代码位置,进行引用;
4、方法出口:
调用的方法执行完后,需要回到之前调用方法的这个现场,当它在调用的时候,就把现场保留了,
就把这些现场保存在方法出口里;
补充:
方法里面new的对象,是存放在他内存空间的局部变量表;
他和正常存放在堆里面new出来对象的关系是什么?
main方法里面存放是一个空的内存空间,他存放的是堆里面那个对象的地址(位置、指针);
3、方法区(元空间)
用于存储已被虚拟机加载的类信息(类信息、方法名称等)、常量、静态变量、即时编译后的代码等数据等;
补充:
但是这里有一点需要注意,比如类下有静态对象变量(如static User user = new User()),这个对象值还是放在堆里面,方法区这个user是存放的指针,方法区和堆也是一个引用关系;
元空间用的是直接的物理内存;以字节为单位,默认是21M,不设置的话是沾满为止;当沾满时,他也会触发FULL GC,他不仅会回收方法区,还会回收堆;回收完后,方法区还有一个自动扩容机制:
推荐!!!正常情况要设置值;不然会有大量的FULL GC产生,严重情况甚至有可能让整个服务挂掉;
与栈的关系
方法里面存放的静态变量如果是对象类型,哪他的对象实例都在堆里面,方法区里面存放的都是对象在堆里面的内存地址;
4、本地方法栈
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务java方法,而本地方法栈是为虚拟机调用native方法服务的;
补充:
线程运行过中,如果需要调到本地方法(如标识native的方法),本地方法也需要内存,
这些就是在本地方法栈里;比如new Thread().start(),就放在本地方法栈,native是C++、C实现的;
windows环境下就是去调用本地的xx.dll;
5、程序计数器
当前线程 所执行的字节码的行号指示器,字节码解析器的工作中就是通过改变这个计数器的值,来获取下一条需要执行的字节码指令,如分支、循环、跳转、异常的处理、线程回复等基础功能,都需要依赖这个计数器来完成;
简单总结:
他就是存放编译字节码后的文件,每个方法里 ,每行代码执行的位置,
每执行完一行代码,字节码执行引擎都会修改程序计数器的值;
为什么需要程序计数器?
因为我们程序都是多线程,假如正在执行,被线程优先级更高的CPU给抢占过去了,当前线程被挂起了,后面被恢复后,它怎么知道要从哪开始执行呢?,这时候就根据程序计数器的值来继续执行;
五、堆栈的区别
1、物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。
在GC的时候也要考虑到不连续的分配,所以会有各种算法。比如标记-清理,复制、标记-压缩;
新生代建议使用复制算法、老年代建议使用标记-是压缩;
栈使用的是数据结构中的栈,先进后出的原则(参考现实里的叠盘子,最底下的到底部才能拿到),所以他的物理地址分配是连续的,所以性能快;
2、内存的区别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆远远大于栈;
栈是连续的,所以分配的内存大小要在编译器就确认,大小是固定的。
3、存放的内容
堆存放的是对象的实例和数组。因此该区域更关注的是数据的存储
栈存放的是局部变量、操作数栈、返回结果。该区域更关注的是程序方法的执行。
PS:
1、静态变量在方法区
2、静态的对象还是放在堆
4、程序可见度
堆对于整个应用程序都是共享、可见的
栈只对于线程是可见的,所以也是线程私有的,他的生命周期和线程相同。
总结
弄明白JVM内存模型,对写代码开发、和后续JVM调优是非常有帮助的,也是高阶程序员必须掌握的技能;
如果有讲的不对的地方还请各位指点一二,如果觉得写的还可以,希望点个赞给我一点鼓舞,感谢!