JVM对象实例化以及String

1、创建对象的方式
(1)new
(2)class的newinstance方法:反射的方式,只能调用空参构造器,权限必须是public
(3)constructor的newinstance方法:反射的方式,可以调空参、带参的构造器,权限没有要求
(4)使用clone():不调用任何构造器,要求当前的类实现cloneable接口,来实现clone()
(5)使用反序列化:从文件、网络中获取一个对象的二进制流
在这里插入图片描述

2、创建对象的过程
(1)判断对象对应的类是否加载、连接、初始化
(2)为对象分配内存,如果内存规整,使用指针碰撞法,如果不规整,虚拟机维护一个空闲列表
(3)初始化分配到的内存(默认初始化 int a,这样外部也可以调用,只不过为0 int占4个字节 string引用类型,也就占4个字节,虽然不知道对象具体内容,但也能确定内存大小)
(4)设置对象的对象头
(6)执行init方法进行初始化
对象属性的赋值:默认初始化,显式初始化,构造器初始化,代码块初始化

第一步,当程序遇到 new 关键字时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟
机加载,如果没有被加载,那么会进行类的加载过程,如果已经被加载,那么进行下一步,为对象
分配内存空间;
第二步,加载完类之后,需要在堆内存中为该对象分配一定的空间,该空间的大小在类加载完成时
就已经确定下来了,这里多说一点,为对象分配内存空间有两种方式:
(1)第一种是 jvm 将堆区抽象为两块区域,一块是已经被其他对象占用的区域,另一块是空白区域,
中间通过一个指针进行标注,这时只需要将指针向空白区域移动相应大小空间,就完成了内存的分
配,当然这种划分的方式要求虚拟机的对内存是地址连续的,且虚拟机带有内存压缩机制,可以在
内存分配完成时压缩内存,形成连续地址空间,这种分配内存方式成为“指针碰撞”,但是很明显,
这种方式也存在一个比较严重的问题,那就是多线程创建对象时,会导致指针划分不一致的问题,
例如 A 线程刚刚将指针移动到新位置,但是 B 线程之前读取到的是指针之前的位置,这样划分内存
时就出现不一致的问题,解决这种问题,虚拟机采用了循环 CAS 操作来保证内存的正确划分;
(2)第二种也是为了解决第一种分配方式的不足而创建的方式,多线程分配内存时,虚拟机为每个
线程分配了不同的空间,这样每个线程在分配内存时只是在自己的空间中操作,从而避免了上述问
题,不需要同步。当然,当线程自己的空间用完了才需要需申请空间,这时候需要进行同步锁定。
为每个线程分配的空间称为“本地线程分配缓冲(TLAB)”,是否启用 TLAB 需要通过
-XX:+/-UseTLAB 参数来设定。
第三步,分配完内存后,需要对对象的字段进行零值初始化,对象头除外,零值初始化意思就是对
对象的字段赋 0 值,或者 null 值,这也就解释了为什么这些字段在不需要进程初始化时候就能直接
使用;
第四步,这里,虚拟机需要对这个将要创建出来的对象,进行信息标记,包括存储在新生代/老年代,
对象的哈希码,元数据信息,这些标记存放在对象头信息中,对象头非常复杂,这里不作解释,可
以另行百度;
第五步,也就是最后一步,执行对象的构造方法,这里做的操作才是程序员真正想做的操作,例如
初始化其他对象啊等等操作,至此,对象创建成功。

3、对象就一定在堆空间中吗
不一定,可能在栈上分配。
(1)JVM的逃逸分析可以分析出某个对象是否永远会在线程中或者某个方法内,如果一直在的话就说明对于某些没有逃逸的对象是可以在栈上分配的。JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC
方法逃逸:当一个对象在方法中定义之后,被外部方法引用
线程逃逸:指类变量或者实例变量被其他线程所访问。

4、对象分配的两种方法
(1)指针碰撞(Serial、ParNew 等带 Compact 过程的收集器)
假设 Java 堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间
放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与
对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
(2)空闲列表(CMS 这种基于 Mark-Sweep 算法的收集器)
如果 Java 堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单
地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列
表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”

4、对象内存布局(重要)
`public class Customer{
int id = 1001;
String name;
Account acct;

{
    name = "匿名客户";
}

public Customer() {
    acct = new Account();
}

}

public class CustomerTest{
public static void main(string[] args){
Customer cust=new Customer();
}
}`在这里插入图片描述
1、首先main方法存放在虚拟机栈的栈帧中,一个方法对应一个栈帧。main方法里的两个局部变量存放在局部变量表中。一个栈帧中有局部变量表、操作数栈、方法返回值、动态连接、即时编译期编译后的代码。
2、局部变量表中的cust指向了堆空间中的new Customer()对象,对象的内存布局有对象头,实例数据和对齐填充。其中对象头里面包含了两部分,分别是运行时元数据(Mark Word)和类型指针,如果是数组,还要记录数组长度。
2.1、运行时元数据里面包含了:哈希表、GC分代年龄、锁状态标志等。
类型指针指向了存放在方法区的Customer的class类元信息。
2.2、实例数据:它是对象真正存储的有效信息,包含程序代码中定义的各种字段,然后String类型的指向了字符串常量池中的信息

2.3、常量什么时候被放入常量池
常量池分为类常量池、运行时常量池和字符串常量池,前面这两个常量池在方法区中,字符串常量池在堆空间中。字符串常量在编译的时候就确定了,存放在字符串常量池中。在运行的时候,JVM读取从数据到方法区的运行时常量池。
3、String
3.1、基本特性
(1)字符串,声明是final,不可被继承,
(2)实现了serializable接口,支持序列化,实现了comparable接口,可以比较大小
(3)JDK8以前是char[]数组,9之后是byte[]保存的字符串

3.2、String的基本特性:不可变性
(1)通过字面量的方式(区别于new)给一个字符串赋值,这个时候字符串声明在字符串常量池中
String s = “cc”;
(2)字符串常量池中不会储存相同内容的字符串的

3.3、String的内存分配
(1)如果是直接使用双引号声明出来的string对象会直接储存在常量池中
(2)new的话在堆空间中

3.4、字符串的拼接操作
(1)常量与常量的拼接结果在常量池,原理是编译期优化
(2)只要其中有一个是变量,结果就在堆中,原理是StringBulder
(3)如果拼接结果用intern()方法,那么首先会查看常量池中是否有对应的字符串,入股没有的话就
把字符串放到池中,并返回对象地址。
举例Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz

  public static void test1() {
      // 都是常量,前端编译期会进行代码优化
      // 通过idea直接看对应的反编译的class文件,会显示 String s1 = "abc"; 说明做了代码优化
      String s1 = "a" + "b" + "c";  
      String s2 = "abc"; 
  
      // true,有上述可知,s1和s2实际上指向字符串常量池中的同一个值
      System.out.println(s1 == s2); 
  }

例子2

public static void test5() {
    String s1 = "javaEE";
    String s2 = "hadoop";

    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";    
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4); // true 编译期优化
    System.out.println(s3 == s5); // false s1是变量,不能编译期优化
    System.out.println(s3 == s6); // false s2是变量,不能编译期优化
    System.out.println(s3 == s7); // false s1、s2都是变量
    System.out.println(s5 == s6); // false s5、s6 不同的对象实例
    System.out.println(s5 == s7); // false s5、s7 不同的对象实例
    System.out.println(s6 == s7); // false s6、s7 不同的对象实例

    String s8 = s6.intern();
    System.out.println(s3 == s8); // true intern之后,s8和s3一样,指向字符串常量池中的"javaEEhadoop"
}
s5和s6,它里面有变量,这样变量加上一个字符串的话,就相当于在堆空间新生成了一个对象,

这样的话s5和s6它们所指向的就是堆空间的地址,而不是常量池。
S8的话就指向了常量池中的javaEEhadoop

例子3

public void test6(){
    String s0 = "beijing";
    String s1 = "bei";
    String s2 = "jing";
    String s3 = s1 + s2;
    System.out.println(s0 == s3); // false s3指向对象实例,s0指向字符串常量池中的"beijing"
    String s7 = "shanxi";
    final String s4 = "shan";
    final String s5 = "xi";
    String s6 = s4 + s5;
    System.out.println(s6 == s7); // true s4和s5是final修饰的,编译期就能确定s6的值了
}

s6=s7,是因为s4和s5都是被final修饰的,因此在编译的时候就确定了。
例子4:

public void test3(){
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3==s4);
}

它这里,s4=s1+s2,相加之后,s4指向了堆空间新建的对象,然后堆空间的对象,并没有指向字符串中的常量,所以s4里并没有常量池中的"ab"。

例子5
在这里插入图片描述
s1是直接通过字面量赋值的方法,所以beijing存放在字符串常量池,s1就直接指向他
s2是通过new对象的方式,堆空间和字符串常量池就都会有它,但是s2指向的是堆空间的地址
所以s1 != s2
s3的话用了intern方法,因为它是新建的,因此就是直接指向了字符串常量池。

例子5!

/**
 * ① String s = new String("1")
 * 创建了两个对象
 * 		堆空间中一个new对象
 * 		字符串常量池中一个字符串常量"1"(注意:此时字符串常量池中已有"1")
 * ② s.intern()由于字符串常量池中已存在"1"
 * 
 * s  指向的是堆空间中的对象地址
 * s2 指向的是堆空间中常量池中"1"的地址
 * 所以不相等
 */
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s==s2); // jdk1.6 false jdk7/8 false

/*
 * ① String s3 = new String("1") + new String("1")
 * 等价于new String("11"),但是,常量池中并不生成字符串"11";
 *
 * ② s3.intern()
 * 由于此时常量池中并无"11",所以把s3中记录的对象的地址存入常量池
 * 所以s3 和 s4 指向的都是一个地址
*/
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3==s4); //jdk1.6 false jdk7/8 true

(1)因为s是现在堆空间new了个对象,所以它是指向堆空间的,虽然用了intern,但没有任何改变,因为它本身也连着字符串常量池
(2)s2是字面量赋值,所以s != s2
(3)s3 = “11” 但这个只是在堆空间中,这个时候堆空间的"11"并没有连接字符串常量池
然后虽然用了intern方法,连接上了字符串常量池,但s3是指向对空间的,因此s3 != s4
而在JDK1.7之后,
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址

String s3 = new String(“1”) + new String(“1”);
s3.intern();
String s4 = “11”;
System.out.println(s3==s4); //jdk1.6 false jdk7/8 true
就是s3一开始是在堆空间new了个对象,因为是拼接的,所以常量池中并没有,
然后s3.intern,此时常量池中并没有"11",因此会把对象的引用指向常量池中,返回的是常量池的的引用地址,因此s3 == s4 !

String s3 = new String(“1”) + new String(“1”);
String s4 = “11”;
s3.intern();
System.out.println(s3==s4); //jdk1.6 false jdk7/8 false
这样的话s4 = “11”,这个时候字符串常量池中已经有了"11",s3.intern方法也就没有用了
所以 s3 != s4

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值