Java内存区域与“String”对象比较问题

一、Java运行时数据区域

Java内存区域与“String”对象比较问题_第1张图片

方法区与堆是所有线程共享的部分,虚拟机栈、本地方法栈、程序计数器为每个线程私有的部分。

1、方法区

​ 方法区是各个线程共享的内存区域,它用于保存被虚拟机加载的类信息、静态变量、常量、即时编译器编译后的代码缓存等数据;运行时常量池是方法区的一部分。

字符串常量池保存在永久代中,而永久代

1、运行时常量池

  • JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  • JDK1.7字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代 。
  • JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

​ 运行时常量池是方法区的一部分;对于字符串常量池,JDK1.7之前是保存在永久代(永久代在方法区中),JDK1.7之后移到了Java堆中 。

​ 运行时常量池具有动态性,java语言不要求常量一定在编译期产生,运行期间也可以将新的常量放入池中。最常见的就是String.intern()方法。

String s1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(s1);
String intern1 = s1.intern();
System.out.println(s1.intern() == s1);//true
String s2 = new StringBuilder("计算机").append("软件").toString();
String intern2 = s2.intern();
System.out.println(s2.intern() == s2);//false

debug结果显示为:
Java内存区域与“String”对象比较问题_第2张图片

  1. 调用s1.intern()方法时,首先会去字符串常量池中查找是否有有一个对象的值是“计算机软件”。如果有,则返回池中对象的引用;如果没有,就把s1指向的对象的引用放入池中。
  2. 当调用s2.intern()方法时,返回的是字符串常量池中s1的实例引用,故第二次比较的内存地址是不一样的,所以是false。

2、为什么要把永久代替换成元空间?

​ 永久代存放在方法区,它有一个JVM本身设置固定大小上限,无法进行调整;而元空间是在本地内存中,它的内存上限是本机可用内存的限制。而且元空间在本地内存中,数据交互更快一点吧。

2、Java堆

​ Java堆是所有内存共享的一块内存区域,用于保存对象实例,所有new操作生成的对象都存放在此处。细分java堆得目的只是为了更好地回收内存、或更快地分配内存。如果在Java堆中没有足够的内存完成实例分配,会造成OutOfMemoryError异常。

3、程序计数器

​ Java线程私有的内存区域,用于存放线程执行的下一条指令;以及为了Java多线程时线程切换后能回到正确的执行位置。各个线程之间计数器互不影响。

4、虚拟机栈

​ 描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机就会同步创建一个栈帧用于存储互补变量表、操作数栈、动态连接、方法出口灯信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量槽(slot)的大小是32位,long和double会占用两个槽。

5、本地方法栈

​ Java中native关键字修饰的方法都是在本地方法栈中运行,它的作用域虚拟机栈类似,只不过服务的方法对象不一样而已。

二、对象的创建

创建对象的流程:

Java内存区域与“String”对象比较问题_第3张图片

当Java虚拟机遇到一条字节码new指令的时候

1、类加载检查

​ 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

2、分配内存

​ 类加载检查通过以后,虚拟机为新对象分配内存。分配的内存大小在类加载的时候已经确定。分配内存有两种方式:指针碰撞、空闲列表。

1、分配内存的两种方式

选择哪种方式是由Java堆是否规整决定的。

1、指针碰撞(Bump The Pointer)

​ 指针碰撞的前提是Java堆中的内存是绝对规整的,空闲的内存放在一边,使用过的内存放在一边,中间使用一个指针作为分界点的指示器。如下图所示:

Java内存区域与“String”对象比较问题_第4张图片

指针碰撞需要维持内存连续,所以需要虚拟机采用的垃圾收集器具备空间整理的能力。

  • 线程不安全问题

    ​ 同时修改指针指向的位置时,在并发的情况下是线程不安全的。假如正在给对象A分配内存,但是指针还没有修改,此时对象B又同时使用原来的指针来分配内存的情况。

  • 解决方案

    1、对分配内存空间的动作进行同步处理----实际上就是采用**CAS+失败重试**的方法保证更新的操作的原子性。

    2、把内存分配的动作按照线程划分在不同的空间之中进行(这样线程之间相互不干扰),即每个线程在Java堆中预先分配一小块内存,成为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个内存的TLAB中进行,只有TLAB用完了,才会需要同步锁定

2、空闲列表(Free List)

​ 如果Java堆得垃圾收集器不支持空间压缩整理的功能,使用的内存与空闲内存交错的分布。那么在分配内存的时候就不能简单地进行指针碰撞,虚拟机维护一个可用内存列表,用于在分配对象内存的时候从列表中找到一个足够大的空间划分给对象实例。

3、初始化零值

​ 分配完内存之后,虚拟机必须将分配到的内存初始化为零值;这布操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型对应的零值。

4、设置对象头

​ 对对象进行必要的设置,例如这个对象是哪个类的实例,如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄信息。

5、执行方法

​ 此时对象中的字段都是零值,对象需要的其他资源和状态信息还没有按照预定的意图初始化好。执行此方法后会按照程序员的意志对对象初始化。

三、String对象“==”比较问题

1、常量表与JVM结构图

常量池存在于JVM中的方法区里。方法区里包括:类变量、类信息、方法信息、常量池(包含符号引用,常量池以表的形式存在 )。

问题:为什么static修饰的方法无法访问到没有static修饰的实例方法?

  • static方法保存在方法区,可直接类名.方法名调用。
  • 实例方法需要通过对象调用,而对象需要new出来,它保存在堆内存里。
  • 两者在不同的位置,并且有时类方法已经初始化了,但实例方法还没有,故无法访问到。

2、Integer对象“==“比较结果分析

首先来看一个例子:

public class Demo {
	public static void main(String[] args) {
		Integer integer1 = 127;
		Integer integer2 = 127;
		Integer integer3 = 129;
		Integer integer4 = 129;
		System.out.println(integer1 == integer2); // true
		System.out.println(integer3 == integer4); // false
	}
}

为什么这里会出现false呢?按我们平时的理解,这两个结果应该都是true才对。

首先得明白以下几点知识:

  1. Java中,“==”用来比较的是变量的内存地址,基本类型除外(上面的例子类型改成int后,结构都是true)。

  2. “equals”默认与“==”一样,也就是说从Object对象继承过来的方法,默认比较地址。

    // Object类中的equals方法,默认使用==比较地址
    public boolean equals(Object obj) {
            return (this == obj);
        }
    
  3. 在Java中,所有对象的超类都是Object。

​ 其实这是因为Java对于非new出来的变量声明以后都会保存在常量池里。而CONSTANT_Integer保存的数据范围为(-128~127),每一个这个范围内同样大小的变量会指向同一个常量池的地址。而超出这个范围的变量(如:129)会重新创建一个Integer实例保存这个数据。上面的例子中integer3integer4虽然字面上的值一样,但是它们的内存地址不一样,所以就出现了false的结果。

知识补充:方法修饰符native

在Java中方法修饰符native表示这个方法是通过JNI来调用C\C++来执行的,在JVM中的本地方法栈中执行。

3、String对象的“==”比较结果分析

1、示例1

public class Demo {
	public static void main(String[] args) {
		String s1 = "llopp";
		String s2 = "llopp";
		System.out.println(s1 == s2);  //true
	}
}

String对象在JVM中的状态

Java内存区域与“String”对象比较问题_第5张图片

符号引用s1、s2保存在JVM的线程栈中,tag保存在常量池的CONSTANT_String中,实际值保存在CONSTANT_utf8中。当以非new方式定义s1时,会在常量池中创建该变量,该变量指向一个内存地址。当s2创建时会先去常量池里查询是否已有该值,有则让这两变量指向同一个内存地址,没有便重新在常量池里指向另一个内存地址保存新变量的值。

2、示例2

public class Demo {
	public static void main(String[] args) {
		String s1 = "llopp";
		String s2= new String("llopp");
		System.out.println(s1 == s2);  //false
	}
}

Java内存区域与“String”对象比较问题_第6张图片

s2是new出来的,此时在堆内存里创建对象。符号引用指向堆内存中开辟的空间地址。堆空间有一个指针指向常量池中值的位置。

s1s2比较的是地址,但是常量池与堆空间地址不一样,故结果为false。

面试题:new String(“hello”);创建了几个对象?

答:如果“hello”这个字面值在前面已经出现过,那么只创建了一个对象。如果没有出现,那么创建了2个对象。

3、示例3

public class Demo {
	public static void main(String[] args) {
		String s1 = "llopp";
		String s2= new String("llopp");
		System.out.println(s1 == s2.intern());  //true
	}
}

String的intern()方法返回的是常量池里面字面值的地址。如果常量池里面没有这个字面值,那么先把这个字面值放到常量表之后返回常量表的地址。

4、示例4

public class Demo {
	public static void main(String[] args) {
		String s1 = "llopp";
		String s2=  "llo";
        String s3=  s2 + "pp";   
		System.out.println(s1 == s3);  //false
	}
}

Java内存区域与“String”对象比较问题_第7张图片

声明s3时会new一个StringBulider对象拼接s1、s2,比较的是堆中stringBulider地址与常量池地址,结果肯定是false。

你可能感兴趣的:(JVM)