通过Java虚拟机理解String s=new String("abc");创建几个对象 - Go语言中文社区

通过Java虚拟机理解String s=new String("abc");创建几个对象


以前看java虚拟机的知识的时候都是零零散散看的,这段时间刚好自己可以有很多的时间做自己的事情了。所以抽空把《深入理解Java虚拟机》看完了。这本书讲了Java虚拟机是如何改善代码的,以及我们写的代码在虚拟机上运行的时候会发生什么,总结的一句话就是可以让我们通过现象看到本质,让我们写代码的时候不仅仅是做一个API小王子,也可以在写代码的时候优化程序,最大的感触就是多看看一些比较出名的书,比很多入门的书感觉好多了,不过最开始的时候,我觉得看入门的比较好,毕竟可以在短时间内了解整个体系。话不多说,我下面开始展开一下标题上的意思。

首先,我们先了解一下java虚拟机区的各个区域,内容部分摘抄于《深入理解Java虚拟机》,在这里多扯一句,那就是有时候进行虚拟机调优的时候,如果出现内存溢出异常不一定说是堆内存太小了导致对象分配不下,也有可能是是其他区域,例如Direct Memory空间不足,因为他不是说空间不足就会进行垃圾回收,要等到老年代满了后Full GC,然后才会帮它清理内存的废弃对象。所以有的时候不一定是堆内存不足,要查看是否是堆内存不足的话,可以查看GC次数。出了Java堆和永久代之外,以下区域也会占用较多的内存,比如说:Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory。线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的时候这块内存占用也比较客观。如果无法分配,可能会抛出IOException:Too many open files异常。JNI代码:如果代码使用JNI调用本地库,那么本地库使用的内存也不在堆中。虚拟机和GC:虚拟机、GC的代码也要消耗一定的内存。

1.程序计数器:程序计数器是一块较小的内存空间,线程私有,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

2.Java虚拟机栈:虚拟机栈描述的时Java方法执行的内存模型,线程私有。每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表中存放着编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double),对象应用(reference类型,它不等同于对象本身,可以是一个指向对象起始地址的引用类型,也可能时指向一个代表对象的句柄或其他与此对象相关的位置,这个我觉得用指针的理解方式比较好理解。比如说String s=new String(“abc");中,s就是存储在这个局部变量表中,而他指向的时存储在堆里面的new String(“abc");下面我会画图表示)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。这里稍微提一下,因为double和long是64位,所以在对于原子性的时候,允许虚拟机将没有被volatile修饰的64位数据的读写操作分别为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load,store,read和write这四个操作的原子性,这就是所谓的long和double的非原子性协定。如果有多个线程共享一个并未声明位volaitile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个即非原值,也不是其他线程修改的值代表了“半个变量”的数值。所以现在各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的long和double变量专门声明位volatile。

3.本地方法栈:本地方法栈和虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

4.堆:是Java虚拟机所管理内存中最大的一块,也是垃圾收集器管理的主要区域,线程共享,唯一目的是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也逐渐变得不是那么的“绝对“了。

5.方法区:用来存储已被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码等数据。线程共享。

6.运行时常量池:运行时常量池是方法区的一部分,用来存放编译期生成的各种字面量和符号引用,这部分内容将会在类加载后进入方法区的运行时常量池中存放。但是也不一定时只有编译期间才能存入运行时常量池中,运行期间也可能将新的常量放入池中。

通过以上内容的介绍,当我们写上String s1=new String("abc")是,这里是创建了两个对象,一个是存放在堆中的new String(“abc");一个是存放在运行时常量池中的abc。之所以会有存放在常量池总的abc是因为字符串类型的元素会经常创建,如果又新创建一个对象,如String s2="abc",这里会直接指向常量池中的”abc“字符串,避免重复创建,提高效率。但是我们试试s1和s2是相等的吗?这是因为刚刚说了这里会创建两个对象,s1因为用了new关键字,所以是指向堆上的“abc”,而s2是指向方法去上的运行时常量池。 (注:以下画图是用https://www.processon.com/ 这个网址做的),所以是不相等的。

 

如果我们加上代码String s3=new String(“abc"),那么s1和s3相等吗?答案如下:因为使用了new关键字,新建一个s3时,就是在堆上新建了一个new String(“abc");s1和s3指向的是两个不同的对象,所以不相等。

 

 

那么我们String s4=“abc"呢s2==s4吗?因为“abc“是存储在运行常量池中,所以s2和s4是指向同一个对象,这里就不会新建一个对象了,我们可以节约内存。

 

 在这里我们顺便扯一下String的intern方法,之前说运行常量池不一定是编译期间存入的数据,也能是运行时的,这里的intern方法就可以在运行时将元素存放到运行时常量池中。我们加入以下代码,String s5=new String("bcd");String s6=s5.intern();String s7="bcd";看看s6和s7是否相等。我们看看intern()方法api的解释:

/** * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();**/

就是说如果常量池中没有这个字符串所对应的值的时候,会在运行时常量池中创建一个,然后返回运行时常量池中的引用,如果有的话,那就直接返回。所以s6和s7是相等的。


 

 

嗯。。我们在这里再说一下,如果我们加入这样的代码String s8=“abc”+“bcd”;String s9=s2+s7;那么s8和s9相等吗?其实还是不相等的,在这里虚拟机进行了优化,String s8=“abc”+“bcd”;在虚拟机上会变成String s8=“abcbcd”,也就是直接在运行时常量池中创建对象。而String s9=s2+s7;变成StringBuilder s9=new StringBuilder();s9.append(s2);s9.append(s7);return s9.toString();这里是在堆上的引用。

 

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/lxh123456789asd/article/details/83929473
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-04-19 13:13:45
  • 阅读 ( 791 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢