浅谈java内存区域

浅谈java内存区域

背景

java运行时,java虚拟机即jvm,会拿到操作系统给他分配的内存区域。jvm把这个内存分为5个区域,每个区域都有自己的用途,他们的数据结构不同并且创建时间和销毁时间也不一样,执行过程不一样,这样可以提高代码的运行效率。jvm把内存区域的分为五个区域,他们分别是栈区(虚拟机栈)、堆区、本地方法栈、程序计数器、方法区(元空间)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tRBXYPzd-1657023809220)(java内存区域/image-20220705150640640.png)]

本地方法栈和程序计数器

其中,本地方法栈是存储c++的native方法运行的栈区(因为java的底层是c++,所以不要感觉疑惑)。程序计数器是指向程序当前运行的位置的,这两个部分我们只要了解一下就行,这两个区域是java程序员很少去在意的。程序计数器记录了每一个线程当前执行的代码行,且程序计数器是线程私有的

字节码解释器在工作中时下一步该干啥、到哪了,就是通过它来确定的。大家都知道在多线程的情况下,CPU在执行线程时是通过轮流切换线程实现的,也就是说一个CPU处理器(假设是单核)都只会执行一条线程中的指令,因此为了线程切换后能恢复到正确的执行位置,每个线程都要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。很明显,程序计数器就是线程私有的。

方法区(重要)

  1. 跟Java堆一样,方法区是各个线程共享的内存区域。

  2. 方法区描述为堆的逻辑部分,为了与Java堆区分开,所以方法区还有一个别名叫”非堆“

  3. 此区域是用于存储已被虚拟机加载的类信息(即类版本、方法、字段等信息)、常量静态变量即编译器编译后的代码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJkrV0cG-1657023809222)(java内存区域/image-20220705165237483.png)]

  4. 在java1.8之前有一个区域叫永久代,后来被方法区取代了。

    • 以前在永久代区域里面主要存放的是class和元数据信息(就是什么对象,类和他们之间关系的那些信息就叫元数据信息),class被加载的时候就会被放入永久代。gc不会在主程序运行的时候对永久代进行清理,这样会导致一个问题,就是永久代区域会随着加载的class的增多而胀满,最终抛出OOM异常
    • java8移除了永久代,取而代之的是一个叫做元数据区的概念,也叫做元空间。元空间和永久代是类似的,但它们最大的区别是元空间并不在虚拟机中,而是使用的本地内存,因此默认情况下,元空间的大小仅受本地内存的限制。

常量池

常量池在元空间(方法区)里面。

常量池作用:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享

JDK1.6及以前,常量池在方法区,这时的方法区也叫做永久代;
JDK1.7的时候,方法区合并到了堆内存中,这时的常量池也可以说是在堆内存中;
JDK1.8及以后,方法区又从堆内存中剥离出来了,但实现方式与之前的永久代不同,这时的方法区被叫做元空间,常量池就存储在元空间。

常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。符号引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

像字符串;String s=“Hello”,这个s是在栈中,“Hello”是在常量池里面。如果是String s1=new String(“jkfl”);这个s1在栈中,这个new出来的对象是在堆中的,且“jkfl”在常量池(这class文件信息里的常量池里)。常量池里面有的对象,那个对象一样的就不会再创建了,但是用new是在堆里的,他们的规则不同,在堆里面,每次new都是会再创建对象的。但是new String(“sdkl”),其实它是创建了两个对象,但是也可能是一个。

就比如下面这个例子(要是前面常量池里面有“china”这个常量的话,这个new String(“china”)就创建了一个堆里的对象,并指向“china”)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kp4Y2VFB-1657023809223)(java内存区域/image-20220705172656312.png)]

所以面试的时候会问一道题:String s=new String(“hello”);这个语句一共创建了多少个变量,结果是:一个或两个(要是)原来常量池里面有“hello”的话就创建了一个,要是常量池里面没有“hello”的话,就是创建两个对象。

常量池中有什么可以看下面这个图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GV8vH0iw-1657023809225)(java内存区域/image-20220705171459148.png)]

常量池还有一个知识点就是:如下例子

package com.liudashuai;
public class HelloWorld {
    public static void main(String[] args) {
        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
        
    }
}

要注意的是String的==是比较两个字符串对象的地址是否一样,要比较内容的话用equal()这个方法。

s1==s2这个因为他们赋值的时候都是用字面量的,他们都是指向常量池里的那个常量对象的,所以地址一样,结果是true。

s1==s3这个也是true,为什么呢?因为这里有一个优化,String s3=“Hel”+“lo”,这个拼接的结果在常量区存在,在class文件中被优化成String s3=“Hello”,所以,他们指向了一个”Hello“常量。所以这个s1 == s2的结果是true。

String ss1="nihao";
String ss2="ni"+"hao";
String ss3="ni"+"hao1";
System.out.println(ss1==ss2);//true
System.out.println(ss1==ss3);//false

但是s1== s4是false,因为常量其实分为运行时的常量和运行前的常量,像字面量是属于运行前的常量,运行前的常量,编译器是已知的,所以可以在生成class文件的时候优化,但是运行时的常量,比如,new String(“jkal”)这种是运行时的常量,运行时才会在常量区生成”jkal“常量,所以不会优化。像运行前的常量是会被放到class文件的常量池里面的。( 这个可以看前面那个图,就是方法区下面有,class文件信息(这里面就是放字面量常量的,字符串优化也是这里面做的)和运行时的常量池

还有就是s1==s9也是false,为什么呢,因为s9是由s7+s8拼接成的,s7与s8都是变量,他们也是不可预料的,只有在运行的时候才知道s7与s8是什么,所以,字符串的优化影响不到它,所以他们的结果是false。= =两边并没有指向一个地址。

s4==s5是false,他们肯定地址不同。这个不用解释。

堆区(重要)

Java堆是java虚拟机所管理的内存中最大的一块,是被所有线程都共享的内存区域。存在的唯一目的就是存放对象实例,几乎所有的对象实例和数组都在这里进行分配内存。不过目前随着技术的不断发展,也并不是所有的对象实例都在堆中分配内存,可能也存在栈上分配。由于所占空间大,又存放各种实例对象,因此java虚拟机的垃圾回收机制主要管理的就是此区域JVM规范中规定堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

所以与栈区对比存在一些优缺点:

  • 栈由系统自动分配,速度快,但是程序员无法控制。

  • 堆是有程序员自己分配,速度较慢,容易产生碎片,不过用起来方便。

  • 堆不同于堆栈(堆栈就是指栈,不是堆)的另一个好处是:编译器不需要知道要从堆里分配多少存储区域,也不必知道存储的数据在堆里存活多长时间,这个由gc来处理。因此,在堆里分配存储有很大的灵活性。但是因此用堆进行存储分配比用堆栈进行存储需要更多的时间

基本所有对象都在堆上被创建,而对象的声明在栈中,它存着堆上的引用。例如 Object object = new Object();

object为对象的声明,存在虚拟机栈里面,新建的object对象存在于堆上面,包括类的成员变量

栈区

JVM 中的栈包括 Java 虚拟机栈本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 方法服务,本地方法栈则为 JVM 使用到的 Native 方法服务。两者作用是极其相似的,本文主要介绍 Java 虚拟机栈,以下简称栈。

栈是线程私有的,他的生命周期与线程相同(随线程而生,随线程而灭)。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间

栈帧是栈的元素(是用于支持虚拟机进行方法调用和方法执行的数据结构)。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。

结构如图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJmrwGUG-1657023809228)(java内存区域/image-20220705200100640.png)

局部变量表

栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,
持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析
另外的一部分将在每一次运行时期转化为直接引用,这部分称为动态连接
比如我们在创建一个Student对象时的数据存储结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-npmMJiVf-1657023809229)(java内存区域/image-20220705200957885.png)]

方法返回

当一个方法开始执行后,只有2种方式可以退出这个方法 :
方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
  
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。

一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。

而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

用两张图来总结

注意:成员变量存在与堆的对象里面

在这里插入图片描述

下图的基本数据类型:int i=1;是说基本数据类型的变量都是放在堆栈里面的。

在这里插入图片描述

注意点

java这几个区,就堆和方法区是所有线程共享的数据区。这几个区都是用来存数据的。

局部变量是随着方法的出栈而出栈的(我觉得局部变量是出了它所在的” } “就消失了,因为一个方法里面可以用多个for(int i=1;i<12;i++){……}),成员变量和对象都是由GC来回收的。

String s=new String(“dfs”);的这个s可以叫引用,可以不用叫变量。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值