系列文章目录
待补充
这是第一篇,系列文章目录待补充
文章目录
前言
JVM作为Java已经封装好的一部分,自行控制垃圾的回收、管理内存,一般很少需要开发者去控制,但是遇到一些内存溢出、内存泄露,就需要掌握JVM的一些知识了,才能分析问题,进行优化。
提示:以下是本篇文章正文内容,会结合实际代码来分析,使用的Java版本为Java8
一、JVM是什么?
JVM即Java内存模型,其具体又可细分为五部分,分别为方法区、堆、虚拟机栈、本地方法栈跟程序计数器,前面的方法区与堆是所有线程共享的区域。
说明:JVM的五部分暂时不展示说明,本文侧重中Java7之后方法区的一个变化情况。
二、方法区中的变化
1.Java7开始变革
到了JDK7的HotSpot,已经把原本放在方法区(永久代)的字符串常量池、静态变量等移出,而到了JDK8,终于完成废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK7中永久代还剩余的内容(只要是类型信息)全部都移动元空间中。
-------------------------------------------------- 引用自 深入理解Java虚拟机_JVM高级特性与最佳实践_第3版_周志明 P77
理解一下,就是JDK8之后,去掉 字符串常量池 与静态变量的方法区变成了元空间。元空间不再与堆连续,而是存在于本地内存(Native memory),能有效避免OOM的发生。
具体如下图所示:
2.对String的一些影响
public static void StringTest() {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
以上的代码,在Java6中运行是 false false,在Java7中运行的结果是 true false
这两个测试用例的要点在于:
str1初始不存在于常量池,即不在常量池的对象调用 String.intern()方法,在不同的jdk版本的结果具有差异性。
str2初始就存在于常量池,即存在常量池的对象在不同的jdk版本调用 String.intern()方法后,一直返回的是常量池中对象的地址。
String.intern()是本地方法,会返回String对象在字符串常量池中的引用地址。
如果字符串常量池包含此对象,直接返回;如果不包括,则会添加进字符串常量池,再返回常量池中的地址
str1测试结果的差异在于:
Java7之后,字符串常量池以及静态变量转移到了堆中,因此对于存在于堆中但不存在与字符串常量池的对象,加入方式发生了变化:
不再会重新在字符串常量池中创建一遍 ,而是指向于堆中已有的那个对象,即String.intern()返回的常量池中对象地址就是堆中的对象地址。
public static void StringTest() {
//1.6之后的字符串常量池就在堆中 intern的对象不存在常量池,这时的加入就是指向堆中的对象地址,因此返回的引用就是String的字符串实例地址
String str1 = new StringBuilder("计算机").append("软件").toString();
//true
System.out.println(str1.intern() == str1);
//Java字符串在str2对象前就存在于常量池了(加载sun.misc.Version这个类时), str2.intern()返回的是常量池中字符串的地址 str2返回的是新创建string对象的地址
String str2 = new StringBuilder("ja").append("va").toString();
//str3是常量池中的地址 str4是新建string对象的地址,与str2也不一样
String str3 = "java";
String str4 = new String("java");
//false
System.out.println(str2.intern() == str2);
//true
System.out.println(str2.intern() == str3);
//false
System.out.println(str2.intern() == str4);
}
三、String不同创建方式的区别变化
1.new String(“19”); 与 String s1 = “19”;
创建时为了避免直接引用常量池的字符了,将创建不相等的字符串,以下例子都是如此。
public static void String1() {
System.out.println(" ===========创建字符对象============");
String s2 = new String("19");
System.out.println(" ===========直接创建============");
String s3 = "28";
}
相同点:
s2、s3在创建时都会将相应的字符串加入到常量池中
差异点:
s2会在堆中创建对象,指向常量池中;s3不会在堆中创建堆中,而是在虚拟机栈中存在
2. new StringBuilder(“1”).append(“1”).toString(); 与 new String(“1”) + new String(“1”);
public static void String1() {
System.out.println(" ========字符StringBuilder append拼接开始创建=======");
String s = new StringBuilder("计算机").append("软件").toString();
System.out.println(" ===========字符串 + 拼接开始创建============");
String s1 = new String("1") + new String("1");
}
结果:
s、s1创建后都在堆中,创建过程中的常量字符串 “计算机” 、“软件”、“1”都在常量池中。并且String +创建时,会优化成StringBuilder append的方式进行创建。
3. 分析四种创建的字节码
public static void String3() {
System.out.println(" ===========开始执行string3============");
String str = new StringBuilder("计算机").append("软件").toString();
System.out.println(" ===========字符串拼接开始创建============");
String s1 = new String("1") + new String("1");
System.out.println(" ===========创建字符对象============");
String s2 = new String("19");
System.out.println(" ===========直接创建============");
String s3 = "28";
}
两种查看方式:
- javap -c 或者 javap -verbose 查看class文件对应的字节码
例如 javap -verbose StringConstant.class - 也可在idea软件中直接查看Java文件的字节码 ByteCode, Intellij IDEA 的一个工具菜单可以直接查看字节码,打开 ByteCode 插件窗口方法如下:View -> Show bytecode
从code29后可以看到两个String对象+ 加号拼接,会优化成StringBuilder(“1”).append(“1”).toString()的方式
从code83可以看到 new String对象的创建方式比直接 “” 创建,多个一个初始化的过程
创建过程中的常量都被加入到了字符串常量池中,并且new String 跟 “ ” 创建的结果也在会常量池中
总结
- 两个String 对象 使用+ 加号拼接,会优化成StringBuilder(“计算机”).append(“软件”).toString(), 因此只要不在for循环中使用+拼接,其实使用+拼接字符串也可行,有JVM给你兜底优化。
- new String(“19”); 与 String string13 = “19”; 创建时都会将相应的字符串加入到常量池中,但是第一个会在堆中创建对象,第二个不会
- Java7之后,字符串常量池以及静态变量转移到了堆中,因此对于存在于堆中但不存在与字符串常量池的对象,调用String.intern()后 不再会
重新在字符串常量池中创建一遍对象,而是直接指向于堆中已有的那个对象,即String.intern()返回的常量池中对象地址就是堆中的对象地址。
进阶点
new String( “ aa” + v); 创建中有常量字符 也有变量的时候
- 常量“ aa” 会加入到常量池,
- v 如果是String堆中的对象 (v不会在常量池 除非调用intern()方法);如果是 String v = “19”; 那么v在创建时已经在常量池。即这步中的v不在被加入到常量池中
- “ aa” + v, 都不会被加入到常量池
在四种创建方式中,会将遇到的常量 直接加入到常量池中,最终的对象是否在常量池 看具体的创建方式而定。