方法区这一部分需要仔细剖析一下。
方法区常被称为永久代,本质上两者并不等价,仅仅是因为GC分代收集会扩展至方法区。方法区存放已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。需要注意的是,Java6时,String等字符串常量的信息是置于方法区中的,但到了Java7,已经移动到了堆区域。具体的可以通过String.intern()方法体现出来。
类信息包括以下几种:
1.类型全限定名。
2.类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)。
3.类型是类类型还是接口类型。
4.类型的访问修饰符(public、abstract或final的某个子集)。
5.任何直接超接口的全限定名的有序列表。
6.类型的常量池。
7.字段信息。
8.方法信息。
9.除了常量意外的所有类(静态)变量。
10.一个到类ClassLoader的引用。
11.一个到Class类的引用
需要注意的是,class对象是存放在堆区的,而不是方法区。类的元数据(元数据并不是类的class对象,class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等都是在方法区的)才是存放在方法区的
方法区中存放的内容在上图一目了然。
永久代
绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。
Java8中就不存在永久代,取而代之的是元空间(Metaspace)。
元空间的本质与永久代类似,都是对JVM规范中方法区的实现。二者最大的区别在于,元空间并不在虚拟机之中,而是使用本地内存。默认情况下,元空间的大小受本地内存的限制,但是可以通过以下参数来指定元空间的大小。
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
作出永久代向元空间的转换原因有以下几点:
1.字符串存在永久代中,容易出现性能问题和内存溢出。
2.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出
3.永久代会为GC带来不必要的复杂度,并且回收效率偏低
------------------------------
运行时常量池:
运行时常量池作为方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特性是具备动态性,Java语言并不要求常量一定只有编译期才能产生,并非预先放入class文件的常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中。比如String.intern()方法。
常量池的好处:
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。可以节省内存空间,常量池中所有相同的字符串常量可以被合并,只占用一个空间。同时可以节省运行时间。比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
双等号==的含义
- 基本数据类型之间应用双等号,比较的是他们的数值。
- 复合数据类型(类)之间应用双等号,比较的是他们在内存中的存放地址。
下面为常见的考点:
Integer与常量池
Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); System.out.println("i1=i2 " + (i1 == i2)); System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); System.out.println("i1=i4 " + (i1 == i4)); System.out.println("i4=i5 " + (i4 == i5)); System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); System.out.println("40=i5+i6 " + (40 == i5 + i6)); i1=i2 true i1=i2+i3 true i1=i4 false i4=i5 false i4=i5+i6 true 40=i5+i6 true
Integer i1=40; Java在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40),从而使用常量池中的对象。同理,i2与i1引用同一个常量池中的对象。
Integer i4=new Integer(40);这种情况下回创建新的对象。
i4与i5分别代表不同的对象。
语句i4=i5+i6,因为+这个操作不适用于Integer对象,首先i5和i6进行自动拆箱操作,进行数值相加。即i4==40.然后Integer对象无法与数值进行直接比较,所以i4自动拆箱为int值40,最终这条语句转为40==40.进行数值比较。
String与常量池
String str1 = "abcd"; String str2 = new String("abcd"); System.out.println(str1==str2);//false String str1 = "str"; String str2 = "ing"; String str3 = "str" + "ing"; String str4 = str1 + str2; System.out.println(str3 == str4);//false String str5 = "string"; System.out.println(str3 == str5);//true
Java中字符串创建的两种方式:1.字面量形式,如String str="abc"另外一种使用new这种标准的构造对象的方法,如String str=new String("abc");
问题1
当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池中,并返回该引用。
当使用new来构造字符串对象时,不管字符串常量池中有没有相同内容的对象的引用。新的字符串对象都会创建。所以str1与str2代表不同的对象。
注意若str3=str2.intern()则有str1==str3为true。对于使用new创建的字符串对象,如果想将这个对象的引用加入到字符串常量池中,可以使用intern方法。调用intern后,首先检查字符串常量池中是否有该对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入并返回给变量。
针对问题2中的str3与str4.
str3等价于直接在字符串常量池中创建一个“string”对象,同时返回引用。str4则是
而字符串拼接中间会产生StringBuilder对象,之后返回sb.toString().
有一道有意思的题目
- String s = null;
- s += "abc";
- System.out.println(s);
答案是nullabc
两个字符串str1, str2的拼接首先会调用 String.valueOf(obj),这个Obj为str1,而String.valueOf(Obj)中的实现是return obj == null ? "null" : obj.toString(), 然后产生StringBuilder, 调用的StringBuilder(str1)构造方法, 把StringBuilder初始化,长度为str1.length()+16,并且调用append(str1)! 接下来调用StringBuilder.append(str2), 把第二个字符串拼接进去, 然后调用StringBuilder.toString返回结果!
所以这里str3==str4返回false的原因是二者对应的内存地址不一样。但是equal时,返回true。
这样一来,str3==str5也就解释的通了。