前言
对于新手而言,Java对象内存可能是一个不太容易理解的点,在这里我把自己的学习心得给大家分享一下,
也是为了给自己记录一下笔记,毕竟好记性不如烂笔头,有不对的地方希望大家可以指出,互相学习进步。
废话不多说,直接进入正题。
对象内存产生分析
类本身属于引用数据类型,所以对于引用数据类型就必须为其进行内存分析。在分析之前,
我们首先给出两块内存空间的概念。
- 堆内存空间(Heap): 保存的是对象中具体的属性信息;
- 栈内存空间(Stack): 保存的堆内存的地址数值;可以简化一下,假设保存在栈内存的是对象的名称。栈内存只能保存一块对应的堆内存空间地址。
所以按照以上分析我们开始进行对象的内存分析;
分析单对象创建代码
简单的定义一个实体类:
public class Person {
//名称
private String name;
//年龄
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
创建一个Main方法:
public class Main {
public static void main(String[] args) {
//声明并实例化Person对象
Person person = new Person();
//给对象赋值
person.setName("张三");
person.setAge(23);
//打印对象信息
System.out.println(person.toString());
}
}
执行得到的结果为:
Person [name=张三, age=23]
到这儿相信大家都能明白,下面我们就拿它来做我们的对象的内存分析。
首先我们经过上面已经知道堆内存空间和栈内存空间,那么这段代码究竟是怎么在堆内存和栈内存之中分布的呢?下面给出一个图片来解释:
严格的来说,我们的堆内存存储的是属性信息,栈内存存储的是地址信息,栈内存根据地址空间的地址数值:此处假设为ox0001,找到对应的堆内存空间,以此来对堆内存空间进行操作。一个栈内存只能够保存一个地址信息。这种就叫做等价画法,但是相对不太容易理解,下面我们进行较容易理解的画法。
我们可以看到:
Person person = new Person();
实例化对象时,含有关键字:new ,当我们看到关键字new时,就可以表示开辟了一块堆内存空间。
此时堆内存空间存在的值为属性的默认值。
栈内存空间中:按照不严格的说法(便于理解),在栈内存中保存的是对象的名称,所以对象名person在栈内存中。
当执行:
person.setName("张三");
person.setAge(23);
此时,我们修改的是栈内存对应堆内存的内容,即堆内存中的name和age则被赋值为:“张三”,23。所以执行结果为:
Person [name=张三, age=23]
分步式创建对象
实际上对象的创建格式上述是直接使用:Person person = new Person();形式直接创建,
但对象的创建格式不止一种,还有另外一种分步的方式来创建对象。下面我们来看分步创建对象时怎么处理内存的:
public class Main {
public static void main(String[] args) {
//声明Person对象
Person person = null;
//实例化person对象
person = new Person();
//给person对象赋值
person.setName("张三");
person.setAge(23);
//打印对象信息
System.out.println(person.toString());
}
}
上述为分步的方式来创建对象,先声明,后实例化。
根据上图,当执行:
Person person = null;
声明一个person对象时,此时只有栈内存,并不会指向堆内存,因为已经明确表示,此时的person对象指向的是null。没有堆内存。
当执行:
person = new Person();
实例化person对象时,当看到关键字new,则即表示开辟了堆内存空间。
堆内存空间开辟,但此时属性值为默认值;
接下来,当执行:
person.setName("张三");
person.setAge(23);
给实例对象赋值时,则为:
此时,我们修改的是栈内存对应堆内存的内容,即堆内存中的name和age则被赋值为:“张三”,23。所以执行结果为:
Person [name=张三, age=23]
由此我们可以看出,分步创建对象和直接创建对象,除了最开始,其余都是相同的。
注意: 关于引用数据类型操作存在的重要问题:
理论上当对象开辟堆内存(实例化对象)那么属性才会进行内存的分配,那么如果说使用了没有实例化的对象呢?观察一下代码:
public class Main {
public static void main(String[] args) {
//声明实例化对象personA
Person person = null;
//给personA对象赋值
person.setName("张三");
person.setAge(23);
//打印对象信息
System.out.println(person.toString());
}
}
则运行结果为:
Exception in thread "main" java.lang.NullPointerException
at com.example.Main.main(Main.java:8)
那么此时返回的是“NullPointerException”异常,就是空指向异常,这个异常只有引用数据类型会出现,
出现的原因只有一点,那就是使用了没有开辟堆内存空间的引用对象。
分析多对象创建代码
上述内容表述开辟了一个对象,那么也可以开辟两个或多个,下面分析多个对象开辟的内存分析:
分析一下代码:
public class Main {
public static void main(String[] args) {
//声明实例化对象personA
Person personA = new Person();
//给personA对象赋值
personA.setName("张三");
personA.setAge(23);
//声明实例化对象personB
Person personB = new Person();
//给personB对象赋值
personB.setName("李四");
personB.setAge(26);
//打印personA与personB信息
System.out.println(personA.toString());
System.out.println(personB.toString());
}
}
那么此时的执行结果为:
Person [name=张三, age=23]
Person [name=李四, age=26]
分析上述代码内存处理情况:
当personA和personB在创建实例化对象时,都会在堆内存中开辟出各自的空间。
当执行给两个实例化对象赋值操作时:
//给personA对象赋值
personA.setName("张三");
personA.setAge(23);
//给personB对象赋值
personB.setName("李四");
personB.setAge(26);
内存操作为:
personA 和 personB 会分别给自己栈内存地址对应的堆内存空间内的属性赋值。彼此之间不会互相影响。
初步引用传递分析
引用传递是在引用数据类型上所使用的一个操作定义,是Java的精髓,它的操作性质与C语言中的指针是相同的,
进行内存的操作。换到程序中,就是一块堆内存空间可以同时被多个栈内存所指向。
引用传递
- 代码分析:
public class Main {
public static void main(String[] args) {
//声明实例化对象personA
Person personA = new Person();
//给personA对象赋值
personA.setName("张三");
personA.setAge(23);
//引用传递
Person personB = personA;
//给personB对象赋值
personB.setName("李四");
//打印personA信息
System.out.println(personA.toString());
}
}
那么此时执行的结果 personA 为:
Person [name=李四, age=23]
内存分析如下图:
当代码执行到:
//声明实例化对象personA
Person personA = new Person();
//给personA对象赋值
personA.setName("张三");
personA.setAge(23);
此处时,内存分配已经在上述中讲述过,此处不再赘述。重点讲解下面的代码执行内存分析:
//引用传递
Person personB = personA;
//给personB对象赋值
personB.setName("李四");
Person personB = personA 此处代码表示将 personA 保存的堆内存地址赋值给了 personB ,
那么就表示了 personB 与personA 将保存同样的地址的名字。如下图:
接下来执行:personB.setName("李四"); personB把name的值改了,因为personB和personA指向同一个地址,那么也就是personA中的name被修改了。
所以结果为:
Person [name=李四, age=23]
注意: 以上是采用了声明对象的方式进行了引用数据类型的接收,那么如果说此时两个对象都已经明确实例化并设置内容了呢?
- 观察引用传递代码
public class Main {
public static void main(String[] args) {
//声明实例化对象personA
Person personA = new Person();
//声明实例化对象personB
Person personB = new Person();
//给personA对象赋值
personA.setName("张三");
personA.setAge(23);
//给personB赋值
personB.setName("李四");
personB.setAge(26);
//引用传递
personB = personA;
//给personB对象赋值
personB.setName("王五");
//打印personA信息
System.out.println(personA.toString());
}
}
执行结果为:
Person [name=王五, age=23]
内存分析如下:
上图表示的代码内容为:
//声明实例化对象personA
Person personA = new Person();
//声明实例化对象personB
Person personB = new Person();
//给personA对象赋值
personA.setName("张三");
personA.setAge(23);
//给personB赋值
personB.setName("李四");
personB.setAge(26);
上述对于此部分已经有了详细的讲述,此处不再赘述,重点讲解引用传递部分代码:
//引用传递
personB = personA;
//给personB对象赋值
personB.setName("王五");
此部分内存表示如下图:
当personA 的值赋给 personB时,那么就表示了 personB 与personA 将保存同样的地址的名字,指向同一块堆内存空间,personB原有空间则变成垃圾空间。当给personB的name重新赋值,实际上就是给personB与personA共同指向的堆内存空间内的name属性赋值,所以personA的值就位:
Person [name=王五, age=23]
通过以上分析应该可以发现几点:
1、使用关键字new永恒可以开辟新的堆内存。堆内存空间保存的就是属性。
2、栈内存只能够保存一块堆内存的试用地址。
3、引用传递的本质就在于同一块堆内存空间可以被不同的栈内存所指向。
4、在发生引用传递时,如果操作的栈内存原本有堆内存指向,那么改变堆空间就以为着改变内存指向。
5、如果一块堆内存没有被任何的栈内存所指向,那么此空间将成为垃圾空间,所有的垃圾空间会自动的被
GC(垃圾收集器)回收并且释放。
垃圾回收咱们不确定它的回收时间,所以还是尽可能少的产生垃圾空间哦。
最后还有一点,如果一个对象己经被明确初始化,并且设置了内容,那么把它重新设置为null,
就是表示这个对象将放弃原本的指向,变为一个没有指向的栈内存,它原本指向的堆内存也就变成了垃圾空间。