实例浅析JVM内存模型和对象引用

目录

 

JVM的内存模型有以下的设定:

第一个栗子 

第二个例子

第三个例子:

第四个栗子


网上很少看到有这样的分析,原来有些地方总模糊不清,其实应该有更多的栗子来细说一下对象,实例等都是在什么时候?比如类加载过程,在哪里创建?比如堆还是栈,创建了几个?什么关系?网上基本都是清一色的理论没有具体的栗子,如果有这方面好文的同学帮忙留个言发个链接,谢啦

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内存模型的几个实例,多多了解内存模型对于开发的工作还是很有帮助的,能少挖不少坑。

猜你喜欢

转载自blog.csdn.net/Goligory/article/details/104547880
今日推荐