目录
一、 程序计数器
我们都知道,线程是CPU调度的基本单位。java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时间,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,而当线程数大于CPU内核数的时候,线程之间就要根据时间片轮询抢夺CPU时间资源。当某线程再次获取到CPU时间资源的时候,CPU如何知道该线程要从哪里开始执行?为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、跳转、循环、异常处理、线程恢复等基础操作都会依赖这个计数器来完成。也就是说程序计数器就是用来指示某线程当前所执行到的语句,以便当再次获得CPU时间资源后,能从这条语句后继续执行。
每个线程都有一个独立的程序计数器,且各条线程之间的计数器互不影响,独立存储。所以他是一个“线程私有”的内存区域。此内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
二、虚拟机栈
每个方法被执行的时候,java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
接下来我们来看一个例子:
package jvm;
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute(){
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
int ans = new Math().compute();
}
}
用javap命令反解析出Math的class文件,重定向到 Math.txt文件中。
让我们来解析一下上述的代码:
iconst_1:将int型推送至栈顶,即将1推送至栈顶
istore_1: 将栈顶int型数值存入指定本地变量,即将1存入本地变量a,也就是给a赋值
iconst_2:将2推送至栈顶
istore_2:将2存入本地变量b,也就是给b赋值
iload_1: 将指定的int型本地变量推送至栈顶,即将1推至栈顶
iload_2: 将2推至栈顶
iadd: 将栈顶两int型数值相加并将结果压入栈顶,即计算1+2,并将结果3压入栈顶
bipush:将int型推送至栈顶,即将10推送至栈顶
imul:将栈顶两int型数值相乘并将结果压入栈顶,即计算10*3,并将结果30压入栈顶
istore_3:将30存入本地变量c,也就是给c赋值
iload_3:将30推至栈顶
ireturn:返回30
注意:
JVM中 int 类型数值,根据 取值范围将 入栈的 字节码指令 就分为4类:
取值 -1~5 采用 iconst
指令;
取值 -128~127 采用 bipush
指令;
取值 -32768~32767 采用 sipush
指令;
取值 -2147483648~2147483647 采用 ldc
指令。
上述流程的栈帧变化大致如下:
- 局部变量表
用于存放局部变量的(方法中的变量),包含8种基础数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来标识,其中64位长度的long和double类型的数据会占用2个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。注意,这里所说的“大小”指的是局部变量槽Slot的数量,虚拟机真正使用多大的内存空间来实现一个变量槽,完全由具体的虚拟机实现自行决定的。如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;如果java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常。 - 操作数栈
(1)虚拟机栈是一个栈,这个栈里面存的结构是当前执行方法的栈帧;栈帧里面又包含另外 一个栈——操作数栈,操作数栈里面存的是当前的操作数
(2)操作系统包含CPU+缓存+主内存
JVM执行引擎对应CPU
操作数栈相当于JVM所模拟出来的这个操作系统中的缓存
主内存对应 堆+栈
(3)存放 java 方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用 来操作的,操作的的元素可以是任意的 java 数据类型,所以一个方法刚刚开始的时候, 这个方法的操作数栈就是空的。
可以简单的理解为操作数栈就是一个工具空间,为了完成一些如简单计算等操作的。 - 动态链接
每个栈帧都保存了 一个可以指向当前方法所在类的运行时常量池的引用, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接 - 方法出口
分为两种情况:
1.正常返回(调用程序计数器中的地址作为返回的位置);
2.异常的话(通过异常处理表<非栈帧中的>来确定)
三、本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是虚拟机使用本地方法服务。
四、方法区
方法区又叫静态区,跟堆一样,被所有的线程共享。方法区中包含的都是在整个程序中永远唯一的元素如class和static变量(方法区包含所有的class和static)。方法区还含有常量池(后面会详细讲)。
假设我们有如下代码:
package jvm;
public class Process {
public static void main(String[] args) {
Cite cite = new Cite();
}
}
class Cite {
private int a = 1;
String str = "1";
}
其运行时JVM内存如下:
系统收到发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到Process.class文件,读取这个文件中的二进制数据,然后把Process类的类信息存放到运行时数据区的方法区中。这一过程称为Process类的加载过程。接着,Java虚拟机定位到方法区中Process类的main()方法的字节码,在栈中开始执行它的指令。第一个指令就是创建一个Cite实例,让cite引用变量引用这个实例,Java虚拟机发现方法区内没有Cite这个类,就尝试加载了Cite类, 把Cite类的类型信息存放在方法区里。然后在堆中分配内存, 这个Cite实例持有着指向方法区的Cite类的类型信息的引用。最后将Cite实例在堆中的地址赋给cite。
五、常量池
常量池主要分为静态常量池和运行时常量池,静态常量池主要是类信息中的类型的常量池,而运行时常量池是动态的,接下来我们详细介绍一下运行时常量池
1、基本类型的包装类和常量池
Java中基本类型的常量池技术不是由基本类型实现的,而是由基本类型对应的包装类型实现的。但是因为Java5.0后引入自动装包和自动拆包,所以这一点已经不重要了。
java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean。这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 两种浮点数类型的包装类Float,Double并没有实现常量池技术。接下来将以Integer为例说明。
Integer类含有一个IntegerCache的内部类,其中创建了数值[-128,127]的相应类型的缓存数据cache,也就是说IntegerCache的内部类预先为Integer类提供了一些Integer对象,以至不必再自行创建数值对应类型的对象了(当然也是可以自己创建一个[-128,127]相对应的Integer对象,只是这部分工作已经由IntegerCache内部类做了,没必要多此一举)。IntegerCache部分代码如下:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
...
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
...
}
...
}
由cache[k] = new Integer(j++) 可以看出该缓存仍然是在堆上创建的对象。
接下来我们再来看一下Integer的valueOf方法:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看出,在需要int封装情况下,如果该数值在cache的范围内,则不需要再堆上new出一个新的Integer对象,直接使用cache预先提供的对象即可,如果超过这个范围,则会再堆上自行创建一个新的Integer对象。
接下来我们做一个实验:
package jvm;
public class ConstantPool {
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(1);
Integer c = 1;
Integer d = 1;
Integer e = 777;
Integer f = 777;
System.out.println("1、 a(1) == b(1):" + (a == b));
System.out.println("2、 c(1) == a(1):" + (c == a));
System.out.println("3、 c(1) == d(1):" + (c == d));
System.out.println("4、 e(777) == f(777):" + (e == f));
System.out.println("5、 a.equals(b):" + a.equals(b));
System.out.println("6、 c.equals(a):" + c.equals(a));
System.out.println("7、 c.equals(d):" + c.equals(d));
System.out.println("8、e.equals(f):" + e.equals(f));
}
}
上述运行结果如下:
分析一下:
根据之前提到的Integer存在相应的缓存数据,不难看出c,d是在运行时常量池中的,二者的地址是相同,而a,b,e,f(e,f数值为777不在[-128,127]的范围内,所以会自动的在堆上创建一个新对象)这些new 出来的对象是在堆中的,地址互不相同。所以只有c == d才返回true。而Integer类重写了equals方法,只要数值相同就返回true。
注意:java中的"=="比较的两对象的堆地址,而equals则是类的方法,Object的equals方法如下:
public boolean equals(Object obj) { return (this == obj); }
可以看出Object类的equals是用"=="实现的,因此对于Object类来说二者是没有区别的。Object是所有类的基类,也就是说在不重写的情况下,各个类的equals方法和"=="是无区别的。而基本类型的包装类则重写了equals方法,以Integer类为例:
public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
可以看出Integer的equals方法返回的是数值的比较。
2、字符串常量池
注意:JDK1.7字符串常量池被单独从方法区移到堆中。
java中字符串的创建有2种方式,一种是常量池中创建,另一种是在堆上创建。其实他和基本数据类型的常量池在某种意义上是类似的。
我们来看如下代码:
package jvm;
public class ConstantPool {
public static void main(String[] args) {
String str1 = "a";
String str2 = "a";
String str3 = "b";
String str4 = "ab";
String str5 = new String(str1); //a
String str6 = new String("a"); //a
String str7 = new String(str1 + str3); //ab
String str8 = new String("c");
System.out.println("1、str1 == str2:" + (str1 == str2)); //true
System.out.println("2、str1 == str5:" + (str1 == str5)); //false
System.out.println("3、str5 == str6:" + (str5 == str6)); //false
System.out.println("4、str4 == str7:" + (str4 == str7)); //false
System.out.println("5、str2.equals(str5):" + (str2.equals(str5))); //true
}
}
其变量在JVM内存如下:
1、 采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"a"这个对象。
不存在对象:在字符串常量池中创建"a"这个对象,然后将池中该对象的引用地址返回给对象的引用str1,这样str1会指向字符串常量池中"a"这个字符串对象;
存在对象:不创建任何对象,直接将池中"a"这个对象的地址返回,赋给引用str2。因为str1、str2都是指向同一个字符串池中的"a"对象,所以str1 == str2的结果为true。
2、采用new关键字来新建一个对象时,JVM首先会在字符串池中查找有没有"a"这个字符串对象,存在对象:就不在池中再去创建"a"这个对象了,直接在堆中创建一个"a"字符串对象,然后将堆中的这个"a"对象的地址返回赋给引用str6,由此str6就指向了堆中创建的这个"a"字符串对象,由于str1和str5所指向的地址不同,所以str5 == str1的结果为false;
不存在对象:首先在字符串池中创建一个"c"字符串对象,然后再在堆中创建一个"c"字符串对象,然后将堆中这个"c"字符串对象的地址返回赋给str8引用,由此str8指向了堆中创建的这个"c"字符串对象。
接下来在看一个:
package jvm;
public class ConstantPool {
public static void main(String[] args) {
String str1 = new String("1") + new String("1");
str1.intern();
String str2 = "11";
System.out.println(str1 == str2); // true
}
}
第一句String str1 = new String(“1”) + new String(“1”);注意,此时"11"对象并没有在字符串常量池中保存引用。先进行new String("1"),由于字符串常量池中没有"1",则创建"1",然后将常量池中的"1"作为new String("1")对象的返回,第二个new String("1"),由于字符串常量池中已经有"1",则在堆上创建String对象,最后产生值为"11"的String对象。注意这个“11”是运算得到的,并不是字符串常量,所以不会在字符串常量池中创建。
第二句str1.intern();发现"11"对象并没有在字符串常量池中,于是将"11"对象在字符串常量池中保存当前字符串的引用,并返回当前字符串的引用(但没有接收)
第三句String str2 = "11";发现字符串常量池已经存在引用了,直接返回(拿到的也是与s3相同指向的引用)
因此s1==s2为true。
intern方法:如果常量池中已经有了此字符串,那么将常量池中该字符串的引用返回,如果没有,那么将该字符串对象添加到常量池中,并且将引用返回。
黄大牙牙yyds