0
点赞
收藏
分享

微信扫一扫

Java课堂篇3_初识JMM、常量池简单理解(字符串常量池、静态常量池、大整型常量池)


写在前面

关于"== "和equals()不止一次碰到,在淦题的时候遇到过,在C#和Java中都涉及到

对于引用类型的数据,我们知道前者 比较的是 引用类型(对象)的地址,而后者比较的是 引用类型(对象)的值

举栗

public class DemoStringChangLiangChi {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2);

        Integer num1 = 3;
        Integer num2 = 3;
        System.out.println(num1 == num2);

        Integer num3 = 128;
        Integer num4 = 128;
        System.out.println((num3 == num4));
    }
}

输出结果为

Java课堂篇3_初识JMM、常量池简单理解(字符串常量池、静态常量池、大整型常量池)_字符串

目录

  • static静态区字符串常量池
  • static静态区的Integer装箱对象
  • C#中特殊对象string

开始

在说字符串常量池之前,我们需要了解Java运行时数据区

  • 方法区
  • 虚拟机栈VM Stack
  • 本地方法栈Native Method Stack
  • 程序计数器PC
  • 堆Heap
  • static静态区
  • 一般的堆区

程序计数器

  • 概念:较小的一块内存空间,可以看做是当前线程所执行的字节码的行号指示器,字节码解释器就是根据改变这个计数器的值从而选取下一条字节码指令。
  • 线程私有:每个线程都有一个自己的计数器,因为cpu是轮流在各个线程之间切换,因此需要 此计数器 恢复正确的执行位置,而且 分支、跳转、循环、异常处理都是依赖这个计数器完成

虚拟机栈

  • 概念:描述的是方法执行的 线程内存模型 ,每个方法被执行的时候,java虚拟机就会同步的创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。方法执行过程就对应着 栈帧 入栈、出栈的过程。
  • 局部变量表:放置了编译器各种可知的各种基本数据类型(boolean、char、byte、short、int、long、float、double)已及 对象变量(区别于对象实例)。其中存储空间以 局部变量槽 表示,long个double占2个 变量槽,其它占1个。
  • 当线程请求栈的深度大于最大限度,会抛出StackOverFlow异常,当栈拓展无法申请足够的内存,会抛出OutOfMemoryError异常

本地方法栈

  • 概念:和虚拟机栈发挥的作用类似,区别是前者为 java方法服务,后者为本地native方法服务。HotSpot虚拟机直接把二者合二为一。
  • 同虚拟机栈类似,也会抛出两种异常。

  • 概念:又称GC堆,因为是垃圾收集器管理的最大内存区域,几乎所有的 对象实例 都是在堆中分布。
  • 主流的HotSpot虚拟机主要采用的就是 经典分代 进行堆内存管理,将堆划分为新生代、老年代等。当今已经出现了其他回收算法。
  • 线程共享:从分配内存的角度看,线程共享的堆可以划分出多个线程私有的分配缓冲区TLAB,以提升对象分配的效率。一般对象的分配不要求堆内地址是连续的,但是对于大对象像数组,一般要求一段连续的堆空间。
  • 大小可通过-Xmx 和 -Xms设定,内存不足分配对象实例或者无法动态拓展就会抛出OutOfMemoryError异常

方法区

  • 概念:和java堆类似,各个线程共享,主要存放已被虚拟机加载的 类型信息、常量、静态变量等。虽然《Java虚拟机规范》把它描述为 逻辑堆,但是却有一个别名称之为 非堆,目的是和java堆区分。
  • 永久代:JDK8之前,HotSpot虚拟机设计团队决定将 GC收集器的分代回收 设计进方法区,或者使用永久代来实现方法区,目的是为了垃圾收集器能够像 管理 普通堆 的方式管理方法区。但是其他虚拟机像JRockit、IBM是不存在永久代的。
  • 元空间代替永久代:在JDK1.6的时候,HotSpot开发团队就有放弃永久代而采用 本地内存 来实现方法区的计划了,到了JDK1.7,已经把原先放在 永久代的 字符串常量池、静态变量移除,到了JDK8,终于完全废弃了永久代的概念,改用本地内存实现的元空间来代替。
  • 这块的内存回收目标主要是针对 常量池的 回收和对类型的卸载。

运行时常量池

  • 方法区的一部分,Class文件除了类的版本、字段、方法、接口等描述信息外,还有一项是== 常量池表==,用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后 放到 方法区的 运行时常量池
  • 运行时常量池相对于 Class文件常量池(在常量池表的内容,像static关键字修饰的) 的另外一个特征就是 具备动态性,java语言并不要求只有预先放到 类中Class常量池 才能能进去方法区的常量池,运行时间也可以将新的常量放入 运行时常量池,像String类的intern()方法(字符串常量池

直接内存

  • 并不是虚拟机运行时期数据的一部分,而不是《Java虚拟机规范》定义的内存区域,但是这部分被频繁的使用,并且也可能导致OutOfMemory异常
  • 可以简单理解 总内存 减去 虚拟机内存 剩下的内存。

一、常量池

常量池

  • JDK7之前,字符串常量池、静态变量池 放在方法区,为方便内存管理而使用永久代实现,故称为 永久代,
  • JDK8之后, 字符串常量池、静态变量池 全部放到了堆中

字符串常量池

  • 在JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的 字符串常量池中存储,返回的是永久代的这个字符串的实例的引用(指针)
  • 在JDK7及其以上,intern()就不需要在拷贝字符串的实例到永久代了,既然字符串常量池已经移到了Java堆中,只需要在字符串常量池里记录首次出现的实例 引用(地址即可)
  • 在Java中,我们知道new出来的是对象,对象变量存储在栈区,对象实例存储在堆区,且每一个new出来的对象地址都是不同的

String s3 = new String("abc");
    String s4 = new String("abc");
    System.out.println("s3 == s4? " + s3 == s4);//false
    System.out.println("s3 equals s4 ?" + s3.equals(s4));//true
    System.out.println("s3 == s1 ?" + s3 == s1) ;//false
    System.out.println("s3 equals s1 ?" + s3.equals(s1)) ;//true

但是为什么 s1 == s2true,而s1 == s3s3 == s4false

  • 因为String s1 = "abc",而不是String s1 = new String("abc")
  • 对于直接双引号的赋值,区别new String()的方式,开始并不是直接在堆区开辟空间,而是在一个静态区一个称之为字符串常量池的地方先查找,此字符串之前是否被定义。也就是"abc"始终只有一个,而s3、s4是堆中两个不同的对象引用(指针)
  • 如果没找到就把此时的这个字符串放入常量池
  • 如果找到,就不在再申请空间,而是把 字符串对象变量 指向 这个 常量池中的字符串(即共享起来)
  • 前面的s1先放在常量池中,当s2出现后,会先池中找,当然能找到,于是s1和s2同时指向常量池的同一地址,所以 s1 == s2返回true

静态常量池

  • static修饰一块代码:静态代码块
  • static修饰成员变量:静态变量,此时可以看做属于类对象,而不是属于某个对象
  • static修饰成员方法:静态方法,此时可以看做属于类对象,而不是属于某个对象

static在的地方可以简单看作是 脱离的、单独的

Java课堂篇3_初识JMM、常量池简单理解(字符串常量池、静态常量池、大整型常量池)_字符串_02

Integer常量池

Integer num1 = 3;
        Integer num2 = 3;
        System.out.println(num1 == num2);//true

        Integer num3 = 128;
        Integer num4 = 128;
        System.out.println((num3 == num4));//false

为什么会出现这样的结果呢?

  • 装箱
    我们需要先知道装箱的概念,在Java中,int为基本类型的数据,而大整型Integer是对象类型的数据,我们从基本类型的数据 封装成 引用类型的数据,称之为装箱
  • 当我们 Integer num1 = 3;的时候,编译器会自动帮我们完成这个装箱的操作,相当于 integer num1 = new Integer(3)

Integer num5 = new Integer(3);
        Integer num6 = new Integer(3);
        System.out.println(num5 == num6);//false
        System.out.println(num5.equals(num6));//true

和上面字符串原理类似

  • 如果是new出来的,则每个对象在堆区开辟空间,空间地址不同
  • 如果是直接赋值 出来的,对于数值范围在(-128 - 127)之间的数值,第一次出现的情况放入常量池,准确来说是常量池放着该对象的引用(地址)
  • 当初始化此范围内的int值后,会先到常量区找是否存在该int值对应的Integer对象
  • 已经存在:指向直接根据常量池存在的引用
  • 不存在:将引用(地址)放在常量池

二、C#中特殊对象string

参考往期博客:C#课堂篇1_格式化输出、数据类型间的转换、string 对象特殊类型(相比值类型、引用类型)


举报

相关推荐

0 条评论