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

以下地址是我在学习JVM内存模型的笔记和总结:

https://blog.csdn.net/lkforce/article/details/70332311

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVMJava虚拟机)模型是指Java程序在运行时的执行环境,包括JVM的组成部分和它们的工作原理。JVM内存模型是指Java虚拟机管理程序运行时内存的方式,包括内存划分、内存分配和对象回收等机制。 JVM模型的组成部分包括类加载器、解释器、即时编译器、垃圾收集器等。类加载器负责将Java类加载到JVM中,并将其转换为可执行代码;解释器负责解释字节码并执行相应的指令;即时编译器则将频繁执行的代码编译成本地机器码,以提高程序的执行效率;垃圾收集器则负责回收程序运行时不再使用的内存。 JVM内存模型规定了Java程序运行时内存的分配和管理方式。JVM内存模型将内存划分为不同的区域,包括方法区、堆、虚拟机栈、本地方法栈和程序计数器。其中,堆是Java程序运行时内存中最大的区域,用于存储对象实例。虚拟机栈和本地方法栈则用于存储程序执行时的局部变量和方法调用信息。方法区用于存储已加载的类信息、常量池、静态变量等数据。程序计数器则用于记录当前线程所执行的字节码指令位置。 JVM内存模型还包括垃圾回收机制,用于自动回收程序运行时不再使用的内存。垃圾回收机制采用标记-清除、复制、标记-整理等不同的算法来回收内存。 总之,JVM模型和JVM内存模型Java程序运行时的重要组成部分,了解和掌握它们的工作原理对于编写高效、稳定的Java程序至关重要。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值