JVM内存空间模型深入浅出

大家都知道,JAVA代码是在我们的JVM虚拟机上运行的,作为一名JAVA程序员,最入门的就是了解JVM内存模型的
原理,下面就由老夫来简单概述这JVM内存模型.

现在我们从JVM的内存空间开始讲起:
在这里插入图片描述
JVM可以简单分为三块区域:
1、类装载子系统
2、运行时数据区(内存模型)
3、执行引擎

在程序运行时,JAVA代码被编译器(javac.exe)编译成字节码文件(.class),然后经过类装载子系统将字节码文件加载到JVM内存中,最后通过执行引擎与JVM内存进行交互,执行我们的代码。

本文忽略了类加载机制,会在其他篇进行阐述。

首先,我给出一个简单的一段代码:

public class Math {
    public static final Integer CONTANT = 666;

    public int math() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
    public static void main(String[] args) {
        Math math = new Math();
        math.math();
        Math math2 =new Math();
        math2.math();
    }
}

我们可以看出程序是这样进行的:
1、执行主线程main方法
2、创建Math对象
3、执行math方法
4、程序结束

数据在栈中

表面看似很简单,那么这段代码在JVM虚拟机中到底是如何运行的呢?

当程序运行时,Math类的代码将会被编译成字节码文件,被类装载子系统加载入JVM的内存中;
程序开始执行,首先会找到程序的入口,main()方法,也是这段程序的主线程(我们姑且称它为线程1)~
由于栈是线程1独有的,线程1会在内存中开辟一片属于自己的栈内存空间;
而main方法是整个类的方法之一,所以是局部变量,于是就会在线程1专属的栈中再开辟一片空间给这个mian方法使用,这片空间叫做-----栈桢;
在这里插入图片描述
(假装有个线程2,所以画了一个小的线程2)
1、程序执行到 Math math = new Math(); //于是在堆中多了一个math对象。
2、程序执行到 math.math(); // 执行math方法,math方法是线程1 main方法调用的方法,所以会在线程1的栈中又会创建一个栈桢–math的栈桢。(这样做就体现出了了局部变量的作用域)
在这里插入图片描述

math方法执行前,先介绍一下栈桢和程序计数器。

程序计数器

程序计数器:线程私有的,程序计数器指定了线程下一步需要执行代码的地址。当然这里的代码指的是底层的代码,不是java代码。

栈桢

栈桢:栈桢是独立在栈内的,里面又包含了很多东西,最重要的还是
局部变量表:局部变量和对应值
操作数栈:用来保存数据的栈
动态链接:动态与对象产生关联,该栈桢是哪个对象的方法在执行,所以动态链接也是指向对象的指针
方法出口:指向下一步代码的地址

代码继续执行:
int a = 1; 此时会将1创建出来,入栈,也就是将1压入math方法的栈桢的操作数栈
在这里插入图片描述
然后会在局部变量表中创建局部变量a,并将操作数栈中的1出给a
在这里插入图片描述

相同的,执行 int b = 2;,也会和上面一样,在局部变量表中创建局部变量b,并将操作数栈中的1出给b
在这里插入图片描述
执行 int c = (a + b) * 10;
将a的值从局部变量表中取出来放入操作数栈
将b的值从局部变量表中取出来放入操作数栈
在这里插入图片描述
将a和b相加,于是会把操作数栈的1和2取出进行加法运算把结果再放入操作数栈中
在这里插入图片描述
将10压入操作数栈
将操作数栈中的值取出做乘法,
将结果再压入操作数栈
在局部变量表中创建一个变量c,
再将操作数栈中的值取出并赋给c
在这里插入图片描述
执行 return c; 将c的结果值30返回
最后math方法对应的栈桢会找到方法出口,跟着程序计数器继续执行,会返回到main方法栈桢,继续执行下一段代码。

接下来会执行
Math math2 =new Math();
math2.math();
和前面的math一样,又会重新在堆中创建一个对象,创建一片math2自己id栈桢区域。
在这里插入图片描述

由此可以总结:
栈 :会有多个栈桢,每个栈桢会有局部变量表、操作数栈、动态链接、方法出口等,局部变量的运行都在栈中运行,栈的结构也保证了变量的局部性和全局性,栈的数据时先进后出。

本地方法栈

本地方法区里面存放着本地方法,如线程的start(),都是我们执行引擎自动调用的,所以我不过多进行阐述

String在堆中

关于堆,堆是用来存放实例对象和全局变量的,所以对是所有线程共享的。而堆中还有个字符串常量池,所以我们下面对String 字符串的不同创建方式对堆展开了解:

String s1 = “Hello”;
String s2 = “Hello”;
String s3 = “Hel” + “lo”;
String s4 = “Hel” + new String(“lo”);
String s5 = new String(“Hello”);
String s6 = s5.intern();
String s7 = “H”;
String s8 = “ello”;
String s9 = s7 + s8;

①System.out.println(s1 == s2); // true
②System.out.println(s1 == s3); // true
③System.out.println(s1 == s4); // false
④System.out.println(s1 == s9); // false
⑤System.out.println(s4 == s5); // false
⑥System.out.println(s1 == s6); // true

我们知道,JDK1.7中JVM把String常量区从方法区中移除了;JDK1.8中JVM把String常量池移入了堆中,同时取消了“永久代”,改用元空间代替(Metaspace)
对于字符串的创建,我们都知道: 类似S1 直接创建String 会在堆中的字符串常量池中欧直接创建对应的字符串

那么通过创建的9个String字符串我们可以得出结论:
① s1 == s2 ;这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。
②s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量(已经定死的值),在编译期间,这种拼接会被优化,编译器直接帮你拼接好,因此String s3 = “Hel” + “lo”;在class文件中被优化成String s3 = “Hello”;,所以s1 == s3成立。
③ s1 == s4当然不相等,s4虽然也是拼接出来的,但new String(“lo”)这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同
④ s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,所以不做优化,等到运行时,s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。
⑤s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。
⑥ s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。

关于堆中字符串的创建:原文链接:https://yq.aliyun.com/articles/232513

堆中的GC

我们都知道对象都在堆中创建,所以堆的空间会被占用很多,所以有了GC垃圾回收机制,这里我简单介绍一下GC垃圾的回收机制:

在这里插入图片描述
堆中的数据分为年轻代(堆内存的1/3)和老年代(堆内存的2/3),而年轻代又分为Eden、From、To
当一个对象被创建时,会被放进Eden(如果对象非常大会被直接分配到老年代),此时并没发生GC,当过多的对象被创建,Eden被占满,此时便会产生一个小GC(执行引擎的一部分),创建一个GC线程,专门对Eden内的数据进行垃圾收集,将Eden的垃圾(比如没有引用、没有指针的对象)放入From。
当From满了后,又会产生一个小GC线程,将From的垃圾放入To,注意,此时的To会变成From来接受Eden的垃圾,而原来的From会变成To来接受新From的垃圾,这样的轮循默认最多持续15次,15次或To内存满了后会将To的数据全都放入老年代(比如线程池中的线程)。
老年代满了以后,就会触发执行引擎的父GC垃圾回收机制,此时JVM会停止其他所有的线程(STW=stop the world,小GC也会但是清理比较小所以影响不大),GC将老年代中的无用对象全部清理。
如果老年代中有过多的对象缺无法被GC清理(例如有BUG导致过多的对象有无用指针指向或者有BUG),老年代内存满了后便会造成内存溢出现象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值