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只是线程栈中的一个引用,就像下面这样:

Java VisualVM 查看内存使用方法 jvm查看内存对象_引用

2,当代码执行

Test configB=configA;

时,我们建立了一个指向configA的对象的引用(名叫configB),注意这个引用不是指向configA的,而是直接指向堆内存中的对象本身的,于是就变成下面这样:

Java VisualVM 查看内存使用方法 jvm查看内存对象_Test_02

可见configA和configB都是这个对象的引用,他们共用一段内存。

3,当代码执行

configB.setId(20);

时,configB把堆内存中对象的id设置为20,因为configA和configB共用了对象,所以后面输出configA对象的id时,输出的是20,也就是如下图所示:

Java VisualVM 查看内存使用方法 jvm查看内存对象_引用_03

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就是指向这个对象的引用,如下图:

Java VisualVM 查看内存使用方法 jvm查看内存对象_引用_04

2,当代码执行

TestFieldClass fieldClass=config.getTestFieldClass();

时,创建的fieldClass实际上是指向这个对象的引用,这个对象本身保存在堆内存中,这个时候,刚刚创建的fieldClass和config对象的testFieldClass属性一样,都是指向这个对象的引用,如下图:

Java VisualVM 查看内存使用方法 jvm查看内存对象_JVM_05

3,当代码执行

fieldClass.setId(20);

fieldClass把这个对象的id设置为20,实际上修改了堆内存中这个对象的id值,如下图

Java VisualVM 查看内存使用方法 jvm查看内存对象_对象_06

正因为如此,后面输出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参数指向的目标还是一样的,如下图:

Java VisualVM 查看内存使用方法 jvm查看内存对象_Test_07

但是到了blockTime = sdf.parse("2018-06-28");这里,等号右边的部分在堆内存中创建了一个新的Date对象,并让blockTime把引用指向了他,也就是说,从此blockTime和config的date属性已经没关系了,变成了下面这样:

Java VisualVM 查看内存使用方法 jvm查看内存对象_引用_08

可以看到,在整个过程中,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对象的内容,而不是例三中改变引用的目标,这个例子在内存模型中的结果是这样的:

Java VisualVM 查看内存使用方法 jvm查看内存对象_引用_09

 

以上就是关于JVM内存模型的几个实例,多多了解内存模型对于开发的工作还是很有帮助的,能少挖不少坑。