JVM的内存模型有以下的设定:
1,有一块内存空间当做主存,叫做堆内存。
2,线程各自有各自的本地内存,叫线程栈,也叫调用栈。
3,线程栈里包含了当前线程执行的方法调用相关信息,还有当前方法的本地变量信息。
4,各线程只能访问自己的线程栈,不能访问其他线程的线程栈。
5,所有原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,各线程之间独立,但是线程之间可以传输原始类型的副本(还是不能算共享)。
6,非原始类型的对象会被存储到堆中,对这个对象的引用会被存储到栈中。
7,对象的成员方法中的原始类型会被存储到栈中。
8,对象的成员变量,包括原始类型和包装类型,还有static类型的变量,都跟着类本身一起存到堆中。
9,如果某个线程要用对象的原始类型成员变量,会拷贝一份到自己的线程栈中。
10,如果某个线程要用对象的包装类型变量,会直接访问堆。
对于以上几点,下面用几个简单的例子来阐述一下,一共四个例子,先上全部的代码,后面分别分析。
全部代码如下:
package test;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test {
public static void main(String[] args) {
case1();
case2();
case3();
case4();
}
public static void case1(){
Test configA=new Test();
configA.setId(10);
Test configB=configA;
System.out.println(configA.getId());
configB.setId(20);
System.out.println(configA.getId());
System.out.println(configA.hashCode());
System.out.println(configB.hashCode());
}
public static void case2(){
Test config=new Test();
config.setTestFieldClass(new TestFieldClass());
TestFieldClass fieldClass=config.getTestFieldClass();
System.out.println(config.getTestFieldClass().getId());
fieldClass.setId(20);
System.out.println(config.getTestFieldClass().getId());
System.out.println(fieldClass.hashCode());
System.out.println(config.getTestFieldClass().hashCode());
}
public static void case3(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
String a = "1970-01-01";
Test config=new Test();
config.setDate(sdf.parse(a));
Date blockTime = config.getDate();
blockTime = sdf.parse("2018-06-28");
System.out.println(sdf.format(blockTime));
System.out.println(sdf.format(config.getDate()));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void case4(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
String a = "1970-01-01";
Test config=new Test();
config.setDate(sdf.parse(a));
Date blockTime = config.getDate();
blockTime.setTime(new Date().getTime());
System.out.println(sdf.format(blockTime));
System.out.println(sdf.format(config.getDate()));
} catch (Exception e) {
e.printStackTrace();
}
}
public Integer id;
public Date date;
public TestFieldClass testFieldClass;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public TestFieldClass getTestFieldClass() {
return testFieldClass;
}
public void setTestFieldClass(TestFieldClass testFieldClass) {
this.testFieldClass = testFieldClass;
}
/**
* Test类的成员变量
*/
public static class TestFieldClass {
public Integer id;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}
}
运行结果:
10
20
709769211
709769211
null
20
1966953839
1966953839
2018-06-28
1970-01-01
2018-06-28
2018-06-28
下面分别解析一下
第一个例子:
public static void case1(){
Test configA=new Test();
configA.setId(10);
Test configB=configA;
System.out.println(configA.getId());
configB.setId(20);
System.out.println(configA.getId());
System.out.println(configA.hashCode());
System.out.println(configB.hashCode());
}
运行结果是:
10
20
776894132
776894132
解析:
1,根据内存模型的设定,当代码执行
Test configA=new Test();
configA.setId(10);
时,实际上在堆内存中创建了一个Test类的对象,保存在堆内存中,然后由configA来指向他,configA只是线程栈中的一个引用,就像下面这样:
2,当代码执行
Test configB=configA;
时,我们建立了一个指向configA的对象的引用(名叫configB),注意这个引用不是指向configA的,而是直接指向堆内存中的对象本身的,于是就变成下面这样:
可见configA和configB都是这个对象的引用,他们共用一段内存。
3,当代码执行
configB.setId(20);
时,configB把堆内存中对象的id设置为20,因为configA和configB共用了对象,所以后面输出configA对象的id时,输出的是20,也就是如下图所示:
4,也是因为二者共用了对象,所以代码最后输出的哈希值是一样的。
下面是第二个例子,代码如下:
public static void case2(){
Test config=new Test();
config.setTestFieldClass(new TestFieldClass());
TestFieldClass fieldClass=config.getTestFieldClass();
System.out.println(config.getTestFieldClass().getId());
fieldClass.setId(20);
System.out.println(config.getTestFieldClass().getId());
System.out.println(fieldClass.hashCode());
System.out.println(config.getTestFieldClass().hashCode());
}
输出结果:
null
20
559102764
559102764
解析:
1,对象config中设置了成员变量testFieldClass,在这个例子里,对象config实际上保存在堆内存中,config的成员变量testFieldClass的对象也保存在堆内存中,而config的成员变量testFieldClass就是指向这个对象的引用,如下图:
2,当代码执行
TestFieldClass fieldClass=config.getTestFieldClass();
时,创建的fieldClass实际上是指向这个对象的引用,这个对象本身保存在堆内存中,这个时候,刚刚创建的fieldClass和config对象的testFieldClass属性一样,都是指向这个对象的引用,如下图:
3,当代码执行
fieldClass.setId(20);
fieldClass把这个对象的id设置为20,实际上修改了堆内存中这个对象的id值,如下图
正因为如此,后面输出config.getTestFieldClass().getId()时,输出的结果是20。
4,前面说到fieldClass和config对象的testFieldClass属性都是指向这个对象的引用,所以最后他们输出的哈希值相同,都是559102764。
下面是第三个例子,代码如下:
public static void case3(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
String a = "1970-01-01";
Test config=new Test();
config.setDate(sdf.parse(a));
Date blockTime = config.getDate();
blockTime = sdf.parse("2018-06-28");
System.out.println(sdf.format(blockTime));
System.out.println(sdf.format(config.getDate()));
} catch (Exception e) {
e.printStackTrace();
}
}
注意输出结果,两个日期不同:
2018-06-28
1970-01-01
解析:
1,变量blockTime来自config变量中的date属性,一开始blockTime的日期是初始的1970-01-01,然后把blockTime的时间改成了2018-06-28,从输出结果上看,变量blockTime的时间改了,而config变量中的date属性值没有跟着blockTime的修改而修改。
2,这个例子乍一看并不符合JVM内存模型的设定,因为多数情况下只有基本类型才会保存在线程栈中,而Date类不是基本类型,他也应该保存在堆内存中,被各种引用共享。
3,导致两个日期输出不同的原因在于这行代码:
blockTime = sdf.parse("2018-06-28");
我们经常把这种带等号的语句叫做赋值语句,而从内存模型的角度来说,这不是赋值,而是一种引用的重定向,虽然在Date blockTime = config.getDate();这里,blockTime引用指向的目标和config的date参数指向的目标还是一样的,如下图:
但是到了blockTime = sdf.parse("2018-06-28");这里,等号右边的部分在堆内存中创建了一个新的Date对象,并让blockTime把引用指向了他,也就是说,从此blockTime和config的date属性已经没关系了,变成了下面这样:
可以看到,在整个过程中,config的date属性所引用的目标没有发生变化,这也就是上面输出不同的原因。
从上面的例三可以知道,非基本类型的变量确实是保存在堆内存中的,而引用的重定向(等号)会让引用直接指向堆内存中的其他对象。
引用的指向挪走了,那之前的对象怎么办?JVM的垃圾回收器(GC)一直在一边候着呢,堆内存中的对象要是没人指向了(或者一段时间内没人指向了,取决于GC的算法),GC就会把这个对象拖走并销毁,然后释放他占用的内存。当然例三中1970-01-01的那个Date对象不会被GC回收,虽然blockTime的指向移走了,但config的date属性还在指向他。
根据以上,我们得到了例四:
public static void case4(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
String a = "1970-01-01";
Test config=new Test();
config.setDate(sdf.parse(a));
Date blockTime = config.getDate();
blockTime.setTime(new Date().getTime());
System.out.println(sdf.format(blockTime));
System.out.println(sdf.format(config.getDate()));
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
2018-06-28
2018-06-28
解析:
1,这个例子中输出的时间一样了,原因在于这个例子中的blockTime使用了以下方法赋值:
blockTime.setTime(new Date().getTime());
直接改变了blockTime对象的内容,而不是例三中改变引用的目标,这个例子在内存模型中的结果是这样的:
以上就是关于JVM内存模型的几个实例,多多了解内存模型对于开发的工作还是很有帮助的,能少挖不少坑。