JVM学习笔记(五)方法区

方法区

JVM学习笔记(五)方法区
JVM学习笔记(五)方法区
首先,书上这样描述方法区:

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。它存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
  • 对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区成为“永久代”,本质上两者是不等价的,仅仅是因为HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说使用永生代来实现方法区而已,这样HotSpot虚拟机的垃圾回收器可以像管理Java对堆一样管理这部分内存,能够省去专门为这些方法区编写内存管理代码的工作。但是用永生代来实现方法区,现在并不是一个好主意,因为这样更容易遇到内存溢出的问题(永生代有-XX:MaxPermSize 的上限,J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB,就不会出现问题),而且有极少数方法会因为这个原因导致不同虚拟机下有不同的表现。因此,对于HotSpot虚拟机,根据官方发布的路线图信息,现在也有放弃永久代并逐步采用Native Memory来实现方法区的规划了,在目前已经发布的JDK 1.7的HotSpot中,已经把原来放在永久代的字符串常量池移除。
  • Java虚拟机规范对方法的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。在SUN公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
  • 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

因为方法区是被所有线程共享的,所以必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类还没有被加载的情况下,只应该有一个线程去加载,而另一个线程等待

方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。同样方法区也不必是连续的。方法区可以在堆(甚至是虚拟机自己的堆)中分配。jvm可以允许用户和程序指定方法区的初始大小,最小和最大尺寸。

方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展java程序,一些类也会成为垃圾。jvm可以回收一个未被引用类所占的空间,以使方法区的空间最小

方法区存储信息

1.类型信息:
每个加载的类型(类class、接口interface、枚举enum、注解annotation),jvm必须在方法区中存储以下类型信息:
对每个加载的类型,jvm必须在方法区中存储以下类型信息:

  • 这个类型的完整有效名
  • 这个类型直接父类的完整有效名(除非这个类型是interface或是java.lang.Object,两种情况下都没有父类)
  • 这个类型的修饰符(public,abstract, final的某个子集)
  • 这个类型直接接口的一个有序列表

完整有效名:
类型名称在java类文件和jvm中都以完整有效名出现。在java源代码中,完整有效名由类的所属包名称加一个".",再加上类名
组成。例如,类Object的所属包为java.lang,那它的完整名称为java.lang.Object,但在类文件里,所有的"."都被
斜杠“/”代替,就成为java/lang/Object。完整有效名在方法区中的表示根据不同的实现而不同。

除了以上的基本信息外,jvm还要为每个类型保存以下信息:

  • 类型的常量池( constant pool)
  • 域(Field)信息
  • 方法(Method)信息
  • 除了常量外的所有静态(static)变量

什么是常量
我们一般把内存地址不变,值可以改变的东西称为变量,换句话说,在内存地址不变的前提下内存的内容是可变的。
我们一般把若内存地址不变, 则值也不可以改变的东西称为常量,典型的String 就是不可变的,所以称之为 常量(constant)。此外,我们可以通过final关键字来定义常量,但严格来说,只有基本类型被其修饰后才是常量(对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变)。

用final修饰的变量表示常量,值一旦给定就无法改变,常量在定义的时候,就需要对常量进行初始化!
常量在程序运行过程中主要有2个作用:

  1. 代表常数,便于程序的修改(例如:圆周率的值)
  2. 增强程序的可读性(例如:常量UP、DOWN、LEFT和RIGHT分辨代表上下左右,其数值分别是1、2、3和4)
    JVM学习笔记(五)方法区

域信息
jvm必须在方法区中保存类型的所有域的相关信息以及域的声明顺序,
域的相关信息包括:

域名
域类型
域修饰符(public, private, protected,static,final volatile, transient的某个子集)

方法信息
jvm必须保存所有方法的以下信息,同样域信息一样包括声明顺序

方法名
方法的返回类型(或 void)
方法参数的数量和类型(有序的)
方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)除了abstract和native方法外,其他方法还有保存方法的字节码(bytecodes)操作数栈和方法栈帧的局部变量区的大小
异常表

类变量

类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在jvm使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。


###class常量池(Class Constant Pool):
jvm为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,
integer, 和floating point常量)和对象类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。
因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java程序的动态链接中起了核心的作用。
JVM学习笔记(五)方法区
字面量
字面量是指由字母,数字等构成的字符串或者数值,它只能作为右值出现,所谓右值是指等号右边的值,如:int a=123这里的a为左值,123为右值。

int a;//a变量
const int b=10;//b为常量,10为字面量
string str="hello world";//str为变量,hello world为也字面量

符号引用
符号引用,顾名思义,就是一个符号,符号引用被使用的时候,才会解析这个符号。如果熟悉linux或unix系统的,可以把这个符号引用看作一个文件的软链接,当使用这个软连接的时候,才会真正解析它,展开它找到实际的文件。

对于符号引用,在类加载层面上讨论比较多,源码级别只是一个形式上的讨论。
当一个类被加载时,该类所用到的别的类的符号引用都会保存在常量池,实际代码执行的时候,首次遇到某个别的类时,JVM会对常量池的该类的符号引用展开,转为直接引用,这样下次再遇到同样的类型时,JVM就不再解析,而直接使用这个已经被解析过的直接引用。

除了上述的类加载过程的符号引用说法,对于源码级别来说,就是依照引用的解析过程来区别代码中某些数据属于符号引用还是直接引用,如,System.out.println(“test” + “abc”);//这里发生的效果相当于直接引用,而假设某个String s = “abc”; System.out.println(“test” + s);//这里的发生的效果相当于符号引用,即把s展开解析,也就相当于s是"abc"的一个符号链接,也就是说在编译的时候,class文件并没有直接展看s,而把这个s看作一个符号,在实际的代码执行时,才会展开这个s。
8种基本类型的包装类和常量池
1. java中基本类型的包装类的大部分都实现了常量池技术

即Byte,Short,Integer,Long,Character,Boolean;
Integer i1 = 40;
Integer i2 = 40;
System.out.println(i1==i2);//输出TRUE

这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。

//Integer 缓存代码 :
public static Integer valueOf(int i) {
     assert IntegerCache.high >= 127;
     if (i >= IntegerCache.low && i <= IntegerCache.high)
         return IntegerCache.cache[i + (-IntegerCache.low)];
     return new Integer(i);
 }
Integer i1 = 400;
Integer i2 = 400;
System.out.println(i1==i2);//输出false

2. 两种浮点数类型的包装类Float,Double并没有实现常量池技术。

Double i1=1.2;
Double i2=1.2;
System.out.println(i1==i2);//输出false

3. 应用常量池的场景
(1)Integer i1=40;Java在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
(2)Integer i1 = new Integer(40);这种情况下会创建新的对象。

Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//输出false

4. 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

解释:语句i4 == i5 + i6,因为+这个操作符不适用于Integer对象,首先i5和i6进行自动拆箱操作,进行数值相加,即i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。

###运行时常量池
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。


引申:
Java String类中的intern()方法
首先源码对intern方法的
详细解释:

简单来说就是intern用来返回常量池中的某字符串,如果常量池中已经存在该字符串(equal判断),则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后返回引用。

所以我们知道:字符串常量池中存放的是对象而不是引用
例子如下:

public static void main(String[] args) {    
            String str1 = "123";
            String str2 = "123";

            String a = new String("123");
            String b = new String("123");;

            System.out.println(str1 == str2);//true
            System.out.println(a == b);//false


            System.out.println(str1 == b);//false
            String bb=b.intern();
            System.out.println((str1== bb));//true
}

JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
JVM学习笔记(五)方法区

String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);

在JDK 1.7下,当执行str2.intern();时,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对堆中的**“str01”的引用**(注意这里是引用 ,就是这个区别于JDK 1.6的地方。在JDK1.6下是生成原字符串的拷贝),而在进行String str1 = “str01”;字面量赋值的时候,常量池中已经存在一个引用,所以直接返回了该引用,因此str1和str2都指向堆中的同一个字符串,返回true。

String str2 = new String("str")+new String("01");
String str1 = "str01";
str2.intern();
System.out.println(str2==str1);

将中间两行调换位置以后,因为在进行字面量赋值(String str1 = “str01″)的时候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的对象,再进行intern方法时,对str1和str2已经没有影响了,所以返回false。


###字符串常量池

延伸:String定义与基础
1.String的声明
JVM学习笔记(五)方法区
由JDK中关于String的声明可知道:
不同字符串可能共享同一个底层char数组,例如字符串 String s=”abc” 与 s.substring(1) 就共享同一个char数组:char[] c = {‘a’,’b’,’c’}。其中,前者的 offset 和 count 的值分别为0和3,后者的 offset 和 count 的值分别为1和2。
public String substring(int beginIndex)

"unhappy".substring(2)returns"happy"
"Harbison".substring(3)returns"bison"

2.offset 和 count 两个成员变量不是多余的,比如,在执行substring操作时。
2.String 的不可变性
众所周知,在Java中,String类是不可变类 (基本类型的包装类都是不可改变的) 的典型代表,也是Immutable设计模式的典型应用。String变量一旦初始化后就不能更改,禁止改变对象的状态,从而增加共享对象的坚固性、减少对象访问的错误,同时还避免了在多线程共享时进行同步的需要。那么,到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态指的是不能改变对象内的成员变量,包括:

  • 基本数据类型的值不能改变;
  • 引用类型的变量不能指向其他的对象;
  • 引用类型指向的对象的状态也不能改变;

除此之外,还应具有以下特点:

  • 除了构造函数之外,不应该有其它任何函数(至少是任何public函数)修改任何成员变量;
  • 任何使成员变量获得新值的函数都应该将新的值保存在新的对象中,而保持原来的对象不被修改。

3.为什么String对象是不可变的?
要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.6中,String 的成员变量有以下几个:

public final class String
    implements java.io.Serializable, Comparable<string>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;

    /** Cache the hash code for the string */
    private int hash; // Default to 0</string>

在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中String类的主要成员变量就剩下了两个:

public final class String
    implements java.io.Serializable, Comparable<string>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0</string>

由以上的代码可以看出, 在Java中,String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章java中数组的特性)。 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:
JVM学习笔记(五)方法区
value,offset和count这三个变量都是
private
的,并且没有提供setValue,setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了也不能被改变。所以,可以认为String对象是不可变的了。

那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代码:

String a = "ABCabc";
System.out.println("a = " + a);    // a = ABCabc

a = a.replace('A', 'a');
System.out.println("a = " + a);    //a = aBCabc

那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace(‘A’, ‘a’)时, 方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用a。String中replace方法的源码可以说明问题:
JVM学习笔记(五)方法区
4.String对象真的不可变吗?
从上文可知String的成员变量是private final的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。**value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗?**比如,将数组中的某个位置上的字符变为下划线“_”。**至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组,那么,用什么方式可以访问私有成员呢?没错,用反射,可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。**下面是实例代码:

public static void testReflection() throws Exception {

    //创建字符串"Hello World", 并赋给引用s
    String s = "Hello World"; 

    System.out.println("s = " + s); //Hello World

    //获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");

    //改变value属性的访问权限
    valueFieldOfString.setAccessible(true);

    //获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s);

    //改变value所引用的数组中的第5个字符
    value[5] = '_';

    System.out.println("s = " + s);  //Hello_World
}

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。


字符串常量池是全局的,JVM中独此一份,因此也称为全局字符串常量池。**运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。**其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。其中:

在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;
在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;
jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。

字符串常量池是什么?
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上
在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
在JDK7.0中,StringTable的长度可以通过参数指定:

-XX:StringTableSize=66666

字符串常量池里放的是什么?
在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。关于String在内存中的存储和String#intern()方法的说明,上面已经讲了:
需要说明的是:字符串常量池中的字符串只存在一份!

String s1 = "hello,world!";
String s2 = "hello,world!";

即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。

String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false

这两种不同的创建方法是有差别的,第一种方式是在常量池中拿对象的引用,第二种方式是直接在堆内存空间创建一个新的对象。只要使用new方法,便需要创建新的对象。编译程序先去字符串常量池检查,是否存在“abcd”,如果不存在,则在常量池中开辟一个内存空间存放“abcd”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“abcd”常量,节省内存空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“str2”,存放的值为堆中String实例的内存地址,这个过程就是将引用str2指向new出来的String实例
连接表达式 +

  • 只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中
  • 对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中。
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

例子2

String str2 = "ab";  //1个对象  
String str3 = "cd";  //1个对象                                         
String str4 = str2+str3;                                        
String str5 = "abcd";    
System.out.println("str4 = str5 : " + (str4==str5)); // false  

我们看这个例子,局部变量 str2,str3 指向字符串常量池中的两个对象。在运行时,第三行代码(str2+str3)实质上会被分解成五个步骤,分别是:

  • (1). 调用 String 类的静态方法 String.valueOf() 将 str2 转换为字符串表示;
  • (2). JVM 在堆中创建一个StringBuilder对象,同时用str2指向转换后的字符串对象进行初始化;
  • (3). 调用StringBuilder对象的append方法完成与str3所指向的字符串对象的合并;
  • (4). 调用 StringBuilder 的** toString() 方法在堆中创建一个 String对象**;
  • (5). 将刚刚生成的String对象的堆地址存赋给局部变量引用str4

我们将其反编译一下,来看看Java编译器究竟做了什么:

String str4 = (new StringBuilder(String.valueOf(str2))).append(str3).toString(); 

而引用str5指向的是字符串常量池中字面值”abcd”所对应的字符串对象。由上面的内容我们可以知道,引用str4和str5指向的对象的地址必定不一样。这时,内存中实际上会存在五个字符串对象: 三个在字符串常量池中的String对象、一个在堆中的String对象和一个在堆中的StringBuilder对象。

特例1

public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B;  // 将两个常量用+连接对s进行初始化 
String t = "abcd";   
if (s == t) {   
    System.out.println("s等于t,它们是同一个对象");   
} else {   
    System.out.println("s不等于t,它们不是同一个对象");   
}   
} 
s等于t,它们是同一个对象

多个确定量(常量)字符串相加时(情况1),编译器直接将它们编辑为相加后的字符串,这样的情况下用“+”比StringBuilder运行时效率更高。
A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s=”ab”+”cd”;
特例2

public static final String A; // 常量A
public static final String B;    // 常量B
static {   
A = "ab";   
B = "cd";   
}   
public static void main(String[] args) {   
// 将两个常量用+连接对s进行初始化   
String s = A + B;   
String t = "abcd";   
if (s == t) {   
    System.out.println("s等于t,它们是同一个对象");   
} else {   
    System.out.println("s不等于t,它们不是同一个对象");   
}   
} 
s不等于t,它们不是同一个对象

A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。


延伸:Java中的String,StringBuilder,StringBuffer三者的区别
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。
1.首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String
String最慢的原因:
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的**。StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改**,而不进行创建和回收的操作,所以速度要比String快很多。
2. 再来说线程安全
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
3. 总结一下

String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况


创建了几个对象?

  1. String s1 = new String(“xyz”);
    考虑类加载阶段和实际执行时。
    (1)类加载对一个类只会进行一次。”xyz”在类加载时就已经创建并驻留了(如果该类被加载之前已经有”xyz”字符串被驻留过则不需要重复创建用于驻留的”xyz”实例)。驻留的字符串是放在全局共享的字符串常量池中的。
    (2)在这段代码后续被运行的时候,”xyz”字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1持有。
    这条语句创建了2个对象。
    首先String s1是定义了一个字符串变量,并未产生对象,等于不产生对象,那么只有后面的new String(“xyz”)了。把它拆分成**"xyz"new String()**,首先在字符串常量池去寻找有没有"xyz"这个字符串,没有就创建一个“xyz”字符串对象在常量池中,然后new String把这个字符串对象拷贝一份到堆中,返回这个对象的引用。所以一共产生两个对象。

2.String test = “a” + “b” + “c”;
会创建几个字符串对象,在字符串常量池中保存几个引用么?
答案是只创建了一个对象,在常量池中也只保存一个引用
看到了么,实际上编译期间,已经将这三个字面量合成了一个。这样做实际上是一种优化,避免了创建多余的字符串对象,也没有发生字符串拼接问题。 只创建了一个对象,在字符串池只会有一个对象。因为它是一行定义的对象,编译时只会初始化一次字符串缓冲池的数据。如果是 String a=“a”;String b=“b”;String c=“c”;String d=a+b+c;这里就创建了4个对象。
汇总例子:

package com.xwl;
/**
 * 关于常量池的问题
 * @author Administrator
 * 字符串常量池
 */
public class AboutInterview {
    public static void main(String[] args) {
         String s1 = "hello";
         String s2 = "hello";
         String s3 = "he" + "llo";
         String s4 = "hel" + new String("lo");
         String s5 = new String("hello");
         String s6 = s5.intern();
         String s7 = "h";
         String s8 = "ello";
         String s9 = s7 + s8;
         System.out.println(s1==s2);//true  s1创建后在串池中,s2创建时候发现已经有,这返回s1的地址 因此相等
         System.out.println(s1==s3);//true  s3字符串拼接后就是s1 jvm在编译期间就已经对它进行优化   因此相等
         System.out.println(s1==s4);//false
         /**
          * s4中的new String("lo")生成了两个对象,"lo","new String("lo")",
          *"lo"存在字符串常量池,"new String("lo")"存在堆中,
          *String s4 = "hel" + new String("lo")实质上是两个对象的相加,
          *编译器不会进行优化,相加的结果存在堆中,而s1存在字符串常量池中,当然不相等。
          */
         System.out.println(s1==s9);//false
         System.out.println(s4==s5);//false
         System.out.println(s1==s6);//true
         System.out.println("-----------------------------------");
         String c1 = new String("hello");
         String c2 = "hello";
         String intern1 = c1.intern();
         System.out.println(intern1 == c2);
         String c3 = new String("hello") + new String("hello");
         String c4 = "hellohello";
         String intern3 = c3.intern();
         System.out.println(intern3 == c4); 
         /**
          * jdk1.6下字符串常量池是在永久区中,是与堆完全独立的两个空间,
          * c1指向堆中的内容,c2指向字符串常量池中的内容,两者当然不一样,
          * c1.intern1()将字面量加入字符串常量池中,
          * 由于字符串常量池中已经存在该字面量,所以返回该字面量的唯一引用,intern1==c2就输出true。
          * jdk1.7,1.8下字符串常量池已经转移到堆中了,是堆中的一部分内容,
          * jvm设计人员对intern()进行了一些修改.
          * 当执行c3.intern()时,jvm不再把s3对应的字面量复制一份到字符串常量池中,
          * 而是在字符串常量池中存储一份s3的引用,这个引用指向堆中的字面量,
          * 当运行到String c4 = "hellohello"时,
          * 发现字符串常量池已经存在一个指向堆中该字面量的引用,则返回这个引用,而这个引用就是c3。所以c3==c4输出true。
          */
          System.out.println(c3 == c4);
          /**
          * s3指向堆内容,接着s4="hellohello"发现字符串常量池中没有该字面量,创建并返回该引用。
          * s3,s4一个在堆,一个在字符串常量池,是两个独立的引用,所以s3==s4输出false。
          */ 
        }
}

JVM学习笔记(五)方法区

JVM学习笔记(五)方法区
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。

方法区和运行时常量区溢出

在JDK 1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过**-XX:PermSize和-XX:MaxPermSize**限制方法区大小,从而间接限制其中常量池的容量,代码如下所示:

/**  
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M 
 * @author zzm  
 */  
public class RuntimeConstantPoolOOM {  
 
public static void main(String[] args) {  
// 使用List保持着常量池引用,避免Full GC回收常量池行为  
List<String> list = new ArrayList<String>();  
// 10MB的PermSize在integer范围内足够产生OOM了  
int i = 0;   
while (true) {  
list.add(String.valueOf(i++).intern());  
         }  
    }  
}

运行结果:

  • Exception in thread “main” java.lang.OutOfMemoryError: PermGen space
  • at java.lang.String.intern(Native Method)
  • at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。
而使用JDK 1.7运行这段程序就不会得到相同的结果,while循环将一直进行下去。还可以引申出一个更有意思的影响,代码如下:

public class RuntimeConstantPoolOOM {  
 
 public static void main(String[] args) {  
 public static void main(String[] args) {  
 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);  
 } }  
}

这段代码在JDK 1.6中运行,会得到两个false,而在JDK 1.7中运行,会得到一个true和一个false。产生差异的原因是:在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true

疑问来了:“java”这个字符串在常量池中什么时候存在了?
java虚拟机会自动调用System类

private static final String launcher_name = "java";

private static final String java_version = "1.7.0_51";

private static final String java_runtime_name = "Java(TM) SE Runtime Environment";

private static final String java_runtime_version = "1.7.0_51-b13";

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等),但在本次实验中操作起来比较麻烦。在代码中,笔者借助CGLib直接操作字节码运行时生成了大量的动态类
值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前的很多主流框架,如Spring、Hibernate,在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。另外,JVM上的动态语言(例如Groovy等)通常都会持续创建类来实现语言的动态性,随着这类语言的流行,也越来越容易遇到溢出场景。
代码如下 借助CGLib使方法区出现内存溢出异常

/**  
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M 
 * @author zzm  
 */  
public class JavaMethodAreaOOM {  
 
 public static void main(String[] args) {  
 while (true) {  
 Enhancer enhancer = new Enhancer();  
 enhancer.setSuperclass(OOMObject.class);  
 enhancer.setUseCache(false);  
 enhancer.setCallback(new MethodInterceptor() {  
 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {  
 return proxy.invokeSuper(obj, args);  
 }  
 });  
 enhancer.create();  
 }  
 }  
 
static class OOMObject {  
 
}  
}

运行结果:

Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
… 8 more

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了CGLib字节码增强和动态语言之外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

元空间

为什么废弃永久代
官方说明翻译如下:

移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

理解元空间
对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它可永久代有什么不同的?存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
1.元空间的内存大小
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
2.常用配置参数
1.MetaspaceSize
初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数.

2.MaxMetaspaceSize
限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。

3.MinMetaspaceFreeRatio
当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。

4.MaxMetasaceFreeRatio
当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。

5.MaxMetaspaceExpansion
Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。

6.MinMetaspaceExpansion
Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。

例子
①:

 public class JvmMethodArea {

    public static String var1;
    public static String var2 = "var2";
    public static String var3 = new String("var3");

    static {
        System.out.println("static");
    }

    public static void main(String[] args) {
        System.out.println("main");
    }
}

String[] args这个引用存在堆栈上,引用的数组本身在堆上
public static String var1;
public static String var2;
静态引用放在方法区(var1这个引用本身),“var2”; 这个引用的字符串放在常量池
new String(“var3”);这个引用的字符串在堆上

②:

String s1 = "abc"
String s2 = new String("abc")
String s3 = new String("abc")
String s4 = new String("abc").intern()

s1 == s2 ,s1 == s4, s2 == s3,s2 == s4
分别为false true false false
s1是java栈中的变量,存储的是对字符串常量池中的“abc”的引用即地址,
s2,s3会先在java堆中创建一个对象,存储对字符串常量池中的“abc”的引用,然后在java栈中的引用变量指向这个对象.
s4 intern()方法直接返回“abc”常量的引用
双等号==的含义
基本数据类型之间应用双等号,比较的是他们的数值。
复合数据类型(类)之间应用双等号,比较的是他们在内存中的存放地址。

实践
假设我们有如下java代码:

String s = "hi";

为了方便起见,就这么简单,没错!将代码编译成class文件后,用winhex打开二进制格式的class文件。如图:
JVM学习笔记(五)方法区
简单讲解一下class文件的结构,开头的4个字节是class文件魔数,用来标识这是一个class文件,说白话点就是文件头,既:CA FE BA BE。
紧接着4个字节是java的版本号,这里的版本号是34,因为笔者是用jdk8编译的,版本号的高低和jdk版本的高低相对应,高版本可以兼容低版本,但低版本无法执行高版本。所以,如果哪天读者想知道别人的class文件是用什么jdk版本编译的,就可以看这4个字节。
接下来就是常量池入口,入口处用2个字节标识常量池常量数量,本例中数值为00 1A,翻译成十进制是26,也就是有25个常量,其中第0个常量是特殊值,所以只有25个常量。
常量池中存放了各种类型的常量,他们都有自己的类型,并且都有自己的存储规范,本文只关注字符串常量,字符串常量以01开头(1个字节),接着用2个字节记录字符串长度,然后就是字符串实际内容。本例中为:01 00 02 68 69。
PS:引申的类文件结构的博文,本人有时间会在后面发出。