1、JVM中的堆栈(1.8)
1.1 JVM内存模型
参考文档:
官网文档网址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
https://baijiahao.baidu.com/s?id=1698998030180488662&wfr=spider&for=pc
https://blog.csdn.net/u014781844/article/details/106903564
1.2 堆栈的比较
2、new对象的时候发生了什么
Java在new一个对象的时候:
1、会先查看对象所属的类有没有加载到内存中
2、如果没有,就会先通过类的全限定名来加载
3、加载并初始化完成后,再进行对象的创建工作
①类加载过程
通过双亲委派模型进行类的加载,先将请求传送给父类加载器,如果父类无法完成这个加载请求,子加载器才会尝试自己去加载。初始化也是先加载父类后加载子类。最终方法区会存储静态变量、类初始化代码、实例变量、实例初始化代码、实例方法等。
②创建对象
在堆中开辟对象的所需的内存。然后对实例变量和初始化方法进行执行。还需要在栈中定义了类引用变量,然后将堆内对象的地址赋值给引用变量。
2.1 对象详解(第一次使用该类)
详见:https://www.cnblogs.com/JackPn/p/9386182.html
2.2 举个例子
/**
* 个人信息
*/
public class Person {
int age;
String name;
public void walk(String name){
System.out.println(name + "正在走路!");
}
}
/**
* 对象创建过程
*/
public class NewObject {
public static void main(String[] args) {
String newObject = "简单的对象创建过程!";
System.out.println(newObject);
Person person = new Person();
person.name = "张三";
person.age = 20;
person.walk(person.name);
}
}
3、 值引用和对象引用的区别
- java只有值传递 本质都是值的拷贝(内存地址本身也是一种数值)
- 值传递:传递的是值的拷贝
- 引用传递:传递的是对象的内存地址的拷贝
4、Code Demo
4.1 一个实例对象可以有多个引用变量
/**
* 一个实例对象可以有多个引用变量
* ps:对象的内存地址伴随每次gc或者是重新启动都会改变
*/
@Test
public void getAddress() {
//pa为引用变量,存储在栈中;new Person为实例对象,存储在堆中
Person pa = new Person("张三", 18, "中国");
//打印引用对象pa指向的地址A
System.out.println("pa第一次获取地址A的值(内存地址)为:" + VM.current().addressOf(pa));
//打印地址A实际存储的实例对象信息
System.out.println("pa地址A存储的对象信息为:" + pa);
//Java中的对象是JVM在管理,JVM会在她认为合适的时候对对象进行移动,比如,在某些需要整理内存碎片的GC算法下发生的GC,从而会导致对象的地址变动,这里我们手动gc下
System.gc();
System.out.println("pa第二次获取地址A的值(内存地址)为:" + VM.current().addressOf(pa));
Person pb = pa;
//打印引用对象pb指向的地址A
System.out.println("pb第一次获取地址A的值(内存地址)为:" + VM.current().addressOf(pb));
//打印地址A实际存储的实例对象信息
System.out.println("pb地址A存储的对象信息为:" + pb);
if (VM.current().addressOf(pa) == VM.current().addressOf(pb)) {
System.out.println("pa和pb为同一个对象");
}
//pa第一次获取地址A的值(内存地址)为:31898735320
//pa地址A存储的对象信息为:Person(name=张三, age=18, country=中国)
//pa第二次获取地址A的值(内存地址)为:29033054880
//pb第一次获取地址A的值(内存地址)为:29033054880
//pb地址A存储的对象信息为:Person(name=张三, age=18, country=中国)
//pa和pb为同一个对象
}
4.2 基本数据类型比较
/**
* 基本数据类型比较
*/
@Test
public void intCompare() {
int a = 3;
int b = 3;
int c = a;
System.out.println(VM.current().addressOf(3));
System.out.println(VM.current().addressOf(a));
System.out.println(VM.current().addressOf(b));
System.out.println(VM.current().addressOf(c));
if (a == b && b == c) {
System.out.println("都相等");
}
}
4.3 值传递:基本数据类型
/**
* 值传递:基本数据类型
* 引用变量和数值都存储在栈中,数值的内存地址是不变的,比如3-31868843208,4-31868843224
* ps:数值的内存地址,伴随每次启动基本不会改变,除非手动触发gc
*/
@Test
public void testInt() {
int a = 3;
System.out.println("a的当前值:" + a);
System.out.println("a的内存地址:" + VM.current().addressOf(a));
add(a);
System.out.println("第一自增后,a的当前值:" + a);
System.out.println("第一自增后,a内存地址:" + VM.current().addressOf(a));
//手动触发gc
// System.gc();
a = add(a);
System.out.println("第二自增后,a返回后的当前值:" + a);
System.out.println("第二自增后,a返回后的内存地址:" + VM.current().addressOf(a));
//a的当前值:3
//a的内存地址:31868843208
//a自增前的当前值:3
//a自增前的内存地址:31868843208
//a自增后的当前值:4
//数字4的内存地址:31868843224
//a自增后的内存地址:31868843224
//第一自增后,a的当前值:3
//第一自增后,a内存地址:31868843208
//a自增前的当前值:3
//a自增前的内存地址:31868843208
//a自增后的当前值:4
//数字4的内存地址:31868843224
//a自增后的内存地址:31868843224
//第二自增后,a返回后的当前值:4
//第二自增后,a返回后的内存地址:31868843224
}
/**
* 数值+1
*
* @param a
* @return
*/
public int add(int a) {
System.out.println("a自增前的当前值:" + a);
System.out.println("a自增前的内存地址:" + VM.current().addressOf(a));
a = a + 1;
System.out.println("a自增后的当前值:" + a);
System.out.println("数字4的内存地址:" + VM.current().addressOf(4));
System.out.println("a自增后的内存地址:" + VM.current().addressOf(a));
return a;
}
4.4 简单的引用地址传递-1
/**
* 简单的引用地址传递-1
* 传参的时候会把引用地址copy一份给方法参数
*/
@Test
public void testString() {
//name指向的是方法区-常量池中"张三"的内存地址A
String name = "张三";
//调用方法时,会把name存储的内存地址copy一份赋值给参数变量str,此时name和str都指向"张三"
//执行方法内容后,str重新赋值并指向"李四",而name的指向依旧不变还是"张三"
changeValue(name);
System.out.println(name);
//张三
}
/**
* 将值修改为:"李四"
*
* @param str
*/
public void changeValue(String str) {
str = "李四";
}
4.5 简单的引用地址传递-2
/**
* 简单的引用地址传递-2
*/
@Test
public void testStringBuilder() {
StringBuilder sb = new StringBuilder("王五");
//调用方法时,会把sb存储的内存地址copy一份赋值给参数变量stringBuilder,此时sb和stringBuilder都指向"王五"
//执行方法后,改变所指向对象的值"王五"+"赵六",这里需要注意引用地址值没变,改变的是对象的值
changeSbValue(sb);
System.out.println(sb);
//王五赵六
}
/**
* 增加"赵六"
*
* @param stringBuilder
*/
public void changeSbValue(StringBuilder stringBuilder) {
stringBuilder.append("赵六");
}
4.6 思考:为啥一个对象执行完后,要对引用变量设置=null?
/**
* 思考:为啥一个对象执行完后,要对引用变量设置=null?
* 为了gc,当引用变量设置为空,可以是原先引用的对象没有引用,根据gc可达性分析的原则,当对象不存在引用时,会被回收
*/
@Test
public void setNull() {
Person person = null;
System.out.println("未new时的内存地址:" + VM.current().addressOf(person));
person = new Person("张三", 30, "中国");
System.out.println("设null前的内存地址:" + VM.current().addressOf(person));
person = null;
System.out.println("设null后的内存地址:" + VM.current().addressOf(person));
//未new时的内存地址:0
//设null前的内存地址:31904171312
//设null后的内存地址:0
}
4.7 思考:final关键字修饰的变量一旦被赋值后,还有修改吗?
/**
* 思考:final关键字修饰的变量一旦被赋值后,还有修改吗?
* 这里需要理解一个概念:修饰的变量不可变,注意这里的不可变是指引用不可变,值是可变的
*/
@Test
public void setFinal() {
//创建了一个引用age指向20这个值
final int age = 20;
//创建了一个引用name,指向常量池"张三"这个值,其中"张三"这个常量值是不可改变的
final String name = "张三";
//age = 32; //能修改成32吗
//name = "李四"; //能修改成李四吗?
final Person person = new Person("张三", 30, "中国");
System.out.println("person修改前内容:" + person);
System.out.println("person内容修改前的内存地址:" + VM.current().addressOf(person));
person.setCountry("深圳");
System.out.println("person修改后内容:" + person);
System.out.println("person内容修改后的内存地址:" + VM.current().addressOf(person));
//person修改前内容:Person(name=张三, age=30, country=中国)
//person内容修改前的内存地址:31898614976
//person修改后内容:Person(name=张三, age=30, country=深圳)
//person内容修改后的内存地址:31898614976
}
4.8 思考::java中的基本数据类型一定存储在栈中吗?
首先说明,"java中的基本数据类型一定存储在栈中的吗?”这句话肯定是错误的。
下面让我们一起来分析一下原因:
基本数据类型是放在栈中还是放在堆中,这取决于基本类型在何处声明,下面对数据类型在内存中的存储问题来解释一下:
一:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因
在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。
(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中
(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。
二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量
(1)当声明的是基本类型的变量其变量名及其值放在堆内存中的
(2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
此外,为了反驳观点" Java的基本数据类型都是存储在栈的 ",我们也可以随便举出一个反例,例如:
int[] array=new int[]{1,2};
由于new了一个对象,所以new int[]{1,2}这个对象时存储在堆中的,也就是说1,2这两个基本数据类型是存储在堆中,
这也就很有效的反驳了基本数据类型一定是存储在栈中~~
4.9 探索:在-128——127的值存在栈中,超出-128——127这个范围的值是存哪?
int i1 = 1;
int a1 = -128;
int b1 = -129;
int c1 = 127;
int d1 = 128;
/**
* 思考:在-128——127的值存在栈中,超出-128——127这个范围的值是存哪?根据测试结果推测在堆中
*/
@Test
public void setLocalVar() {
//范围内取值 每次重启内存地址值不变,除非手动gc
int i2 = 1;
int i4 = 1;
System.out.println("每次重启值不变,i1=:" + VM.current().addressOf(i1));
// System.gc();
System.out.println("每次重启值不变,i2=:" + VM.current().addressOf(i2));
System.out.println("每次重启值不变,i3=:" + VM.current().addressOf(1));
System.out.println("每次重启值不变,i4=:" + VM.current().addressOf(i4));
//起始边界值 每次重启值不变
int a2 = -128;
System.out.println("每次重启值不变,a1=:" + VM.current().addressOf(a1));
System.out.println("每次重启值不变,a2=:" + VM.current().addressOf(a2));
System.out.println("每次重启值不变,a3=:" + VM.current().addressOf(-128));
//结束边界值 每次重启值不变
int c2 = 127;
System.out.println("每次重启值不变,c1=:" + VM.current().addressOf(c1));
System.out.println("每次重启值不变,c2=:" + VM.current().addressOf(c2));
System.out.println("每次重启值不变,c3=:" + VM.current().addressOf(127));
//超出起始边界值 每次重启值都会改变
int b2 = -129;
System.out.println("每次重启值都会改变,b1=:" + VM.current().addressOf(b1));
System.out.println("每次重启值都会改变,b2=:" + VM.current().addressOf(b2));
System.out.println("每次重启值都会改变,b3=:" + VM.current().addressOf(-129));
int b4 = -129;//b2和b4的内存地址值不一样,可以初步判断每次都是在堆中创建一个新的对象空间
System.out.println("每次重启值都会改变,b4=:" + VM.current().addressOf(b4));
//超出结束边界值 每次重启值都会改变
int d2 = 128;
System.out.println("每次重启值都会改变,d1=:" + VM.current().addressOf(d1));
System.out.println("每次重启值都会改变,d2=:" + VM.current().addressOf(d2));
System.out.println("每次重启值都会改变,d3=:" + VM.current().addressOf(128));
}
5、思考
待补充