前言:前面讲了,一个JVM主要由三个部分组成,前面我已经讲了执行引擎和类加载子系统,在这篇中就详细说下运行时数据区。

总体认识下运行时数据区

Java 元空间数据结构_字符串


jdk1.7之前,HotSpot虚拟机对于方法区的实现称之为“永久代”, Permanent Generation 。

jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”, Meta Space 。

方法区

存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等等。
JDK1.6的时候方法区是用永久代实现的;
JDK1.7的时候先把运行时常量池(包括字符串常量池)转移到了堆中;
JDK1.8的时候彻底废除永久代,方法区用元空间实现,其中的类信息、方法信息在元空间;

永久代和元空间存储位置和存储内容的区别:
1)存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存。
2)存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到 了堆和元空间中。

1.7及以前,如果加载的类过多,会导致方法区的OOM,抛出异常"java.lang.OutOfMemoryError: PermGen space 其实就是方法区的OOM。
而1.8及以后抛出的异常会是 java.lang.OutOfMemoryError: Metaspace 可以看到出现的是元空间溢出。

运行时常量池和字符串常量池

在1.7及以后运行时常量池由方法区转到了堆中

常量池区别
class常量池(静态常量池)、运行时常量池、字符串常量池区别:

  • class常量池本质就是class文件中的东西,是在磁盘中,它存放的是编译期间生产的字面量(双引号的字符串和flinal修饰的常量)、符号引用(类或接口全限定名、变量或方法名、变量或方法描述信息),这部分内容在类加载后进入运行时常量池,每个类都有一个class常量池。
  • 运行时常量池是在JVM内存中,同样的也是一个类一个运行时常量池,其内容基本上都是来自于class常量池,存的是被解析(类加载中有个解析这一步)之后的直接引用。
  • 字符串常量池也是在JVM中,是全局唯一的,其中双引号的字符串是进入字符串常量池;可以通过String#intern()方法动态添加进去。
  • 字符串常量池在逻辑上是属于运行时常量池的一部分。
  • 1.6及以前常量池是在方法区中,1.7及以后在堆中。

字符串常量池为什么要唯一?
String类型是一个引用类型,它就需要在堆中产生一个对象占用内存,如果很多相同的字符串就会占用大量内存,所以弄成全局唯一。

什么时候进入字符串常量池?
简单点讲就是执行ldc指令的,而用双引号的字符串就会执行ldc指令,那就看什么时候执行ldc指令。下面第一个类中执行完main方法后,"hello"不会进入字符串常量池;第二个会。

public class Test{
    String a = "hello";
    public static void main(String[] args){}
}
public class Test22{
    static String a = "hello";
    public static void main(String[] args){}
}

如何在字符串常量池中查找字符串?

为了提高匹配速度,即更快的查找某个字符串是否在常量池中,设计的时候在常量池中维护了一个Stringtable,它类似于HashTable,其中key是字符串的hashcode,value是引用地址。

所以,字符串常量池查找字符串的方式:

1)根据字符串的 hashcode 找到对应entry。如果没冲突,它可能只是一个entry,如果有冲突,它可能是一个entry链表,然后Java再遍历entry链表,匹配引用对应的字符串。

2)如果找得到字符串,返回引用。如果找不到字符串,会把字符串放到常量池,并把引用保存到Stringtable里。

3)jdk1.6中StringTable的长度是固定的,就是1009;1.7及以后可以通过参数设置 -XX:StringTableSize=99991

Java 元空间数据结构_jvm_02

字符串常量池

它是java为了节省空间而设计的一个内存区域,java中所有的类共享一个字符串常量池。
比如A类中需要一个“hello”的字符串常量,B类也需要同样的字符串常量,他们都是从字符串常量池中获取的字符串,并且获得得到的字符串常量的地址是一样的。

字符串常量池案例分析
  • 单独使用””引号创建的字符串都是常量,编译期就已经确定存储到String Pool中。
  • 使用new String(“”)创建的对象会存储到heap中,是运行期新创建的。
  • 使用只包含常量的字符串连接符如”aa”+”bb”创建的也是常量,编译期就能确定已经存储到String Pool中。
  • 使用包含变量的字符串(如果s用final修饰,s就是常量了)连接如”aa”+s创建的对象是运行期才创建的,存储到heap中。
  • 运行期调用String的intern()方法可以向String Pool中动态添加对象。

案例分析:

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

String str3 = new String("abc");
System.out.println(str3 == str2);   //false

String str4 = "a" + "b";
System.out.println(str4 == "ab");   //true

// final修饰的是常量,所以 下面的 str5 就相当于是 “ab”
final String s = "a";
String str5 = s + "b";
System.out.println(str5 == "ab");   //true

String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab");   //false

// 字符串只要调用api,就想当于 new 了一个新对象,都是在堆中
String str7 = "abc".substring(0, 2);
System.out.println(str7 == "ab");   //false
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");  //false

intern()方法案例分析
首先在jdk1.7及以后,调用intern()方法,首先判读在字符串常量池中是否有该字符串,如果有直接返回字符串常量池中的地址;如果没有将stringtable中的value值指向堆中的地址
在jdk1.6及以前,intern() 方法首先判断在字符串常量池中是否有该字符串,如果有直接返回字符串常量池中的地址;如果没有则会在字符串常量池中创建一个对象,并且stringtable中的value指向的是字符串常量池中的地址,并且intern()方法返回的也是字符串常量池中的地址
在jdk1.6的时候,字符串常量池在永久代,永久代是一块大小固定的区域,一般也就是在32~96M之间,永久代和堆属于物理隔绝的两块内存区域,所以调用intern()方法只能在字符串常量池中新创建一个对象,这样就会导致许多重复的字符串(堆中一个,永久代中一个),造成性能损失。
jdk1.7的时候,字符串常量池转移到堆,常量池的大小也不会受限于固定大小,所以在调用intern()方法时,如果字符串常量池中没有只需要复制引用即可,指向的还是堆中的地址。这样就能减少对象的创建;同时位于堆中的常量池可以被垃圾回收,当常量池中的字符串不再存在指向它的引用 时,JVM就会回收该字符串。

String s5 = "a";
String s6 = "abc";
String s7 = s5 + "bc";
System.out.println(s6 == s7.intern());  //true

String a = "hello";
String b = new String("hello");
System.out.println(a == b); //false

String c = "world";
System.out.println(c.intern() == c);    //true

String d = new String("mike");
System.out.println(d.intern() == d);    //false

// 这行代码创建了5个对象:堆中有个"jo"、字符串常量池中有个"jo"、堆中有个"hn"、字符串常量池中有个"hn"、堆中有个"john"
String e = new String("jo") + new String("hn");
System.out.println(e.intern() == e);    //true

// 字符串常量池中本身就内置了 "java" 这个对象,java在启动的时候会把一部分的字符串添加到字符串常量池中
String f = new String("ja") + new String("va");
System.out.println(f.intern() == f);    //false

结果都有,具体的分析过程这里就不一一分析了。

Java堆

Java堆被所有线程共享,在Java虚拟机启动时创建。是虚拟机管理最大的一块内存,是垃圾回收的主要区域。所有的对象实例以及数组都要在堆上分配。
不过随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了(第一篇中有介绍)。

堆内存划分

在1.8以前,分为新生代+老年代+永久代(Perm),不同代采用不同的GC回收算法。
1.8及以后,分为新生代+老年代+元空间。元空间已经不算在JVM运行时数据区了,使用的是物理内存,直接受本机的物理内存限制。

新生代
  • Eden空间
  • From Survivor空间
  • To Survivor空间
老年代

堆大小 = 新生代 + 老年代
堆的大小可通过参数 -Xms(堆的初始容量)、-Xmx(堆的最大容量)来指定。
默认的,Edem : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定 。即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总
是有一块 Survivor 区域是空闲着的。
新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

程序计数器

也叫PC寄存器,是一块较小的内存空间,由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(针对多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。这里存的是字节码指令的行号.
如果一个线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个Native方法,这个计数器的值则为空。

Java虚拟机栈

虚拟机栈也是线程私有,而且生命周期与线程相同,每个Java方法在执行的时候都会创建一个栈帧。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变
量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一
个栈帧在虚拟机栈里从入栈到出栈的过程。

一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。对于JVM执行引擎来说,在在活
动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法
称为当前方法,定义这个方法的类叫做当前类。

执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。如果当前方法调用了其他方法或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。
调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧成为了当前栈帧。方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧(返回给方法调用者),随后虚拟机将会丢弃此栈帧。

栈帧的中的各个部分
  • 局部变量表
    存放方法内定义的局部变量和形参,存放的是基本数据类型和引用类型;局部变量表在编译的时候是可以预知的。
    存放的最小单位是变量槽(Variable Slot),它的大小是32位,也就是说如果需要存long或者double类型时会使用两个连续的 Slot 来存储。
    虚拟机是通过索引来查找对应的局部变量,索引范围从0到局部变量表最大范围。
  • 操作数栈
  • 动态连接
  • 方法返回
  • 附加信息

本地方法栈

本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法(比如C++方法)服务。
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压 入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新 的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体。
是由非java语言在外面实现的。下面给了一个示例:

public class IHaveNatives{
	native public void Native1( int x ) ;
	native static public long Native2() ;
	native synchronized private float Native3( Object o ) ;
	native void Native4( int[] ary ) throws Exception ;
}