目录
有两篇文章学习内存图
- 学习内存图+6个例子(一)
- 学习数组内存图(二)
定义变量
基本类型:
8大类型:
类型 根据开辟空间分类 占比特(bit)位 整数 byte 8 整数 short 16 整数 int 32 整数 long 64 浮点数 float 32 浮点数 double 64 布尔 boolean 字符 char 基本类型用
=
就直接开辟了空间:
例如:int num1 = 10; float num2 = 50.2f;
引用类型:
除了八个是基本类型,剩下的都是引用类型。如:类、接口、数组
绝大对数都用new
开辟空间。
Java提供的ArrayList类,是实现动态数组:把引用的类型以参数的形式传过去ArrayList list = new ArrayList<>(); //<>是泛型 //例如: ArrayList<String> list = new ArrayList<String>();
特例:
字符串两种方式:
String str1 = new String("hello"); String str2 = ("hello");
数组三种方式:
int[] arr1 = new int[5]; int[] arr2 = new int[]{1,5,6,8}; int[] arr2 = {1,5,6,8};
内存图
内存图可大致分为:
- 方法区:又细分为类常量池和静态常量池
- 栈区:控制哪个线程栈执行,方法的入栈出栈,控制方法的执行;有线程栈,多线程栈互不打扰。栈的空间小。
- 堆区:引用类型数据开辟空间。字符串常量池存储string赋值的数据。堆的空间大。
- 程序计数器:控制当前那个方法在调用,执行到了什么状态,变量什么时候有效什么时候无效。
- 本地方法栈
图示:
例1(画图入门):
代码
建一个Test1类:
package com.neicuntu; public class Test1 { //属性 static int xx = 20; //方法 public static void main(String[] args) { //定义变量 //基本类型 int num1 = 10; float num2 = 50.2f; //引用类型 Cat cat1 = new Cat(); Cat cat2 = new Cat(); //字符串 String str1 = new String("hello"); String str2 = "hello2"; //数组 int[] arr1 = new int[5]; int[] arr2 = new int[] {1,4,7,6}; int[] arr3 = {1,4,7,6,10}; } }
建一个Cat类:
package com.neicuntu; public class Cat { }
内存图过程:
文字解释:
- 类常量池中有Test类和Cat类,Test中有xx属性和main方法,均被
static
修饰(被修饰的会进行背景加颜色),Cat类中没有任何方法和属性; - 被
static
修饰的,加载进内存之后,会立马到静态常量池里开辟空间;所以静态常量池中开辟Test类中的xx属性的32比特空间=20,和main方法空间;main方法里的东西只有调用的时候执行; - 在线程栈中执行:线程栈只看栈顶是谁,最上面是谁就执行谁
- main方法入栈;
- 基本类型数据直接在栈区中开辟空间:开辟num1的32比特空间,存10;开辟num2的32比特空间,存50.2
- 引用类型数据:在堆区中开辟一个空间,其地址为0x11,cat1指向0x11;同理,堆区中开辟一个空间,其地址为0x22,cat2指向0x22;
new
出来的差不多是这种模板,可得出arr1、arr2、arr3均是指向堆区中的开辟出的空间地址,如图中所示;- 但有一个特殊类型:字符串
String类型
,str2是直接=
出来的数据,其单独的在堆区中有一个空间----字符串常量池中开辟空间;而str1在堆区开辟一个空间用来存储在字符串常量池中存储数据的地址,str1指向堆区的这个地址; - arr3之后,发现一个
}
,整个方法结束,方法main出栈,所有开辟的变量 (跟着方法走的变量叫局部变量) 一起出栈,堆区中的空间没有指向了,会被回收。
图示:
由此可得出规则:
- 被
static
修饰的属性或方法可直接在静态常量池中开辟空间; - 方法中的基本类型的可直接在栈区开辟空间;
- 方法中的引用类型使用
new
修饰的基本在堆区开辟空间,然后栈区指向堆区的地址; - 引用类型中特殊的
String
:如果是String str2 = "hello2";
写法,则是在堆区中的字符串常量池中开辟空间;如果是String str2 = new String("hello2");
写法,在堆区中的字符串常量池中开辟一个空间存储数据,在堆区中开辟一个空间存储字符串常量池中的地址; - 栈区中的方法执行到
}
,整个方法结束,栈区中的方法出栈,方法中的变量跟随一起出栈; - 由于栈区中的方法、变量出栈,堆区中的空间没有指向,其空间会被回收;
- 特殊:字符串常量池中开辟空间不会因为没有指向就被回收,而是等池子满了,才会清掉没有指向的空间。
例2(static修饰):
代码
建一个Test类:
package com.neicuntu; public class Test { //属性 public static int xx = 20; //静态属性 public int yy = 200; //普通属性 public String zz = new String("hello");//普通属性 //静态方法 public static void main(String[] args) { //被`static`修饰的都是静态属性、静态方法,都是直接访问 System.out.println(xx); change(); //没有被`static`修饰的为普通属性、普通方法,需要用`new`对象 Test test = new Test(); System.out.println(test.yy); System.out.println(test.zz); test.eat(); } //静态方法 public static void change() { System.out.println("change方法执行啦"); } //普通方法 public void eat() { System.out.println("eat方法执行啦"); } }
记点:
- 被`static`修饰的都是静态属性、静态方法,都是直接访问 - 没有被`static`修饰的为普通属性、普通方法,需要用`new`对象 - 所有被`static`修饰的,在内存图中都进入静态常量池中,已经被分好空间
打印结果:
20 change方法执行啦 200 hello eat方法执行啦
内存图过程:
文字解释:
- 类常量池中有Test类:xx属性、yy属性、zz属性、main方法、change方法、eat方法(背景有颜色的是有
static
修饰,背景为白色的没有被修饰); - xx属性在静态常量池中开辟32比特的空间,存20,打印20;开辟main方法和change方法的空间;
- 调用:
- main方法入栈;
- change方法入栈;
- 在栈区,change在上,先执行change方法;打印:change方法执行啦,然后change方法出栈;
- 执行main方法:main中有test变量,在堆区开辟test空间,其地址为0x11,栈区指向该地址;
- 堆区中的test空间中:yy直接开辟32比特空间,存200,打印200;zz字符串指向堆开辟空间存储字符串常量池中存储数据的地址的地址,打印hello;开辟eat方法空间,打印eat方法执行啦;
- 最后main方法执行完,main方法出栈。
图示:
例3(内容修改):
代码
在例2的代码基础上进行添加代码:
package com.neicuntu; public class Test { public static int xx = 20; //静态属性 public int yy = 200; //普通属性 public String zz = new String("hello");//普通属性 //静态方法 public static void main(String[] args) { //new一个变量test Test test = new Test(); System.out.println(test.xx); System.out.println(test.yy); System.out.println(test.zz); System.out.println("--------------------------"); //new一个变量test1 Test test1 = new Test(); System.out.println(test1.xx); System.out.println(test1.yy); System.out.println(test1.zz); //修改内容 test1.xx = 1111; test1.yy = 222; test1.zz = new String("demo"); System.out.println("==========================="); System.out.println(test.xx); System.out.println(test.yy); System.out.println(test.zz); System.out.println(test1.xx); System.out.println(test1.yy); System.out.println(test1.zz); } //静态方法 public static void change() { System.out.println("change方法执行啦"); } //普通方法 public void eat() { System.out.println("eat方法执行啦"); } }
打印结果:
20 200 hello //这是test打印的结果 -------------------------- 20 200 hello //这是test1打印的结果 =========================== 1111 200 hello //这是通过test1修改xx打印的结果 1111 222 demo //这是通过test2修改yy和zz打印的结果
内存图过程:
文字解释:
- 开辟test之前的过程与例2过程一样;
- 在堆区中开辟test1的空间,其地址为0x33,栈区test1指向该地址;
- 开始改值:
- 通过test1中找xx,改值为1111;
- 通过test1中找yy,改值为222;
- 通过test1中找zz,zz又new了一个新的值,在字符串常量池中新开盘一个空间,存储demo,其地址为0x55,堆区空间0x44指向0x55,test1中的zz指向0x44。
图示:
例4(跨类调用):
代码
建一个Cat类的文件:
package com.neicuntu; public class Cat { public static int count = 10; public static String country = "中国"; public String name = new String("猫猫"); public int age = 2; public static void run() { System.out.println("跑跑跑"); } public void eat() { System.out.println("吃吃吃"); } }
建一个Test类的文件:
package com.neicuntu; public class Test{ public static void main(String[] args) { //跨类调用 //静态方法调用 Cat.run(); System.out.println(Cat.count); //非静态方法调用 Cat cat1 = new Cat(); cat1.eat(); System.out.println(cat1.count); System.out.println(cat1.country); System.out.println(cat1.name); System.out.println(cat1.age); Cat cat2 = new Cat(); cat2.eat(); System.out.println(cat2.count); System.out.println(cat2.country); System.out.println(cat2.name); System.out.println(cat2.age); //修改值 cat2.count = 999; cat2.country = "小家"; cat2.name = "豆豆"; cat2.age = 5; //打印 System.out.println(cat1.count); System.out.println(cat1.country); System.out.println(cat1.name); System.out.println(cat1.age); System.out.println(cat2.count); System.out.println(cat2.country); System.out.println(cat2.name); System.out.println(cat2.age); } }
打印结果:
跑跑跑 10 //打印静态方法调用的结果 --------------------------------------------------------- 吃吃吃 10 中国 猫猫 2 //打印非静态方法调用,new cat1的结果 --------------------------------------------------------- 吃吃吃 10 中国 猫猫 2 //打印非静态方法调用,new cat2的结果 --------------------------------------------------------- 999 小家 猫猫 2 //修改count值、country值、name值、age值,打印new cat1的结果 --------------------------------------------------------- 999 小家 豆豆 5 //修改count值、country值、name值、age值,打印new cat2的结果
内存图过程:
文字解释:
- 类常量池中有:
- Cat类:属性:count(静态)、country(静态)、name、age;方法:run()(静态)、eat()
- Test类:方法:main()(静态)
- 方法解析好后,挂着静态的资源都会拷贝复制一份进入静态常量池里:
- Cat类:属性:count(静态)、country(静态);方法:run()(静态)
- Test类:方法:main()(静态)
- 方法入栈出栈:
- Test main()方法入栈,main()方法里的语句一句一句的执行;
- 执行Cat.run():Cat run()入栈,执行,打印跑跑跑,碰到
}
,方法结束,出栈;
- 执行打印Cat.count语句,打印10;
- 执行
Cat cat1 = new Cat();
相关的语句:- 在栈区中的Test main()方法中开辟cat1的空间;
- 在堆区中开辟cat1中内容的空间,静态的不要,只要非静态的,其地址为0x11;
- 执行
Cat cat2 = new Cat();
相关的语句,与cat1同理:
- 执行修改值的语句:
- 通过cat2修改count的值,修改为999;
- 通过cat2修改country的值,修改为小家;
- 通过cat2修改name的值,修改为豆豆;
- 通过cat2修改age的值,修改为5;
- 执行打印结果的语句,然后碰到
}
,Test main()方法结束,Test main()出栈。
图示:
例5(数值无效交换):
代码
建一个Cat类的文件:
package com.neicuntu; public class Cat { public static int count = 10; public String name = 猫猫"; public int age = 2; public static void run() { System.out.println("跑跑跑"); } public void eat() { System.out.println("吃吃吃"); } }
建一个Test类的文件:
package com.neicuntu; public class Test{ public static void main(String[] args) { //基本类型的交换 int num1 = 10; int num2 = 20; System.out.println(num1); System.out.println(num2); change1(num1, num2); System.out.println(num1); System.out.println(num2); System.out.println("=============="); //引用类型的交换 String str1 = "hello"; String str2 = "hi"; System.out.println(str1); System.out.println(str2); change2(str1, str2); System.out.println(str1); System.out.println(str2); System.out.println("=============="); //自定义的交换 Cat cat1 = new Cat(); Cat cat2 = new Cat(); System.out.println(cat1); System.out.println(cat2); change3(cat1, cat2); System.out.println(cat1); System.out.println(cat2); } public static void change1(int a,int b) { int temp = a; a = b; b = temp; } public static void change2(String a,String b) { String temp = a; a = b; b = temp; } public static void change3(Cat a,Cat b) { Cat temp = a; a = b; b = temp; } }
打印结果:
10 20 //交换前打印的结果 10 20 //交换后打印的结果 ============== hello hi //交换前打印的结果 hello hi //交换后打印的结果 ============== com.zcm.neicuntu.Cat@7de26db8 com.zcm.neicuntu.Cat@1175e2db //交换前打印的结果 com.zcm.neicuntu.Cat@7de26db8 com.zcm.neicuntu.Cat@1175e2db //交换后打印的结果
从打印的结果看,交换前后的结果没变化。
内存图过程:
文字解释:
- Cat类和Test类中的属性、方法进入方法区;
- Test main方法入栈:
- 执行开辟num1和num2的32比特空间,分别存储10、20,然后打印;
- 调change1方法:
- change1方法入栈,先执行change1方法里的内容;
- 开辟a和b的空间,change1中进行的是值传递:num1的值拷贝一份传给a,a=10,num2的值拷贝一份传给b,b=20;
temp=a
:开辟temp的空间为10;a=b
:a的值为20;b=temp
:b的值为10;- 此时,在change方法中a和b的值是交换了的;
- 但是change1方法中执行遇到
}
,change1方法整体结束,change1方法出栈,change1方法里变量跟随一起出栈; - 因此,change1方法里的改变不会影响main方法里的num1和num2的值的改变,change1方法里的交换失败。
- 开辟str1和str2的空间:在字符串常量池中开辟存储hello的数据和hi的数据,地址分别为0x11、0x22,str1指向0x11,str2指向0x22;
- 调change2方法:
- change2方法入栈,先执行change2方法里的内容;
- 过程与change1中的过程一样,是将str的只进行传递,a=0x22,b=0x11,temp=0x11;
- 执行遇到
}
,change2方法整体结束,change2方法出栈,change2方法里变量跟随一起出栈; - 因此,change2方法里的改变不会影响main方法里的str1和str2的值的改变,change2方法里的交换失败。
- 在堆区中开辟cat1和cat2的空间;
- 调change3方法:
- 过程与change1和change2过程一样
- 过程与change1和change2过程一样
图示:
为什么cat打印结果是地址呢?
打印默认调用的都是
toString()
方法,无论写没写,均调用该方法。
字符串调用:String - String- 前面String表示方法返回类型,返回的是一个String类型的数据;
- 后面String表示谁提供的这个方法,是String类提供的方法。
String - Object:
- 前面String表示方法返回类型,返回的是一个String类型的数据;
- 后面Object表示有Object类提供的方法(Object是Java中最大的类)。
Object是父类,提供一些方法,其中包含toString()方法。所有类会直接或间接继承Object类中的方法。
toString()方法可以重写。
@Override public String toString() { return "Cat [name=" + name + ", age=" + age + "]"; }
重写后,cat打印结果:
Cat [name=猫猫, age=2] Cat [name=猫猫, age=2] //交换前打印的结果 Cat [name=猫猫, age=2] Cat [name=猫猫, age=2] //交换后打印的结果
像str打印的结果,是toString()重写了;而cat打印结果,toString()直接使用的是Object。
例6(数值有效交换)
代码
建一个Cat类的文件:
package com.neicuntu; public class Cat { public static int count = 10; public String name = 猫猫"; public int age = 2; public static void run() { System.out.println("跑跑跑"); } public void eat() { System.out.println("吃吃吃"); } @Override public String toString() { return "Cat [name=" + name + ", age=" + age + "]"; } }
建一个Test类的文件:
package com.neicuntu; public class Test{ public static void main(String[] args) { //自定义的交换 Cat cat1 = new Cat(); Cat cat2 = new Cat(); cat1.name = "豆豆"; cat1.age = 5; cat2.name = "毛毛"; cat2.age = 3; System.out.println(cat1); System.out.println(cat2); change3(cat1, cat2); System.out.println(cat1); System.out.println(cat2); } public static void change(Cat a,Cat b) { int temp = a.age; a.age = b.age; b.age = temp; String str = a.name; a.name = b.name; b.name = str; } }
打印结果:
Cat [name=豆豆, age=5] Cat [name=毛毛, age=3] Cat [name=毛毛, age=3] Cat [name=豆豆, age=5]
内存图过程:
文字解释:
- 执行第一次打印cat1和cat2之前的过程如下图:
- change入栈出栈过程(进入堆区深处进行交换):
- 开辟a、b的空间,分别指向0x88和0x99;
- 开辟temp空间,将a中的age的值给temp:temp=5;
- 将b中age值给a,cat1 0x88中的age为:age=3;
- 将temp的值给b,cat2 0x99中的age为:age=5;
- 开辟str空间,将a中的name的值给str:str=0x22;
- 将b中的name值给a,cat1 0x88中的name为:name=0x33;
- 将str的值给b,cat2 0x99中的name为:那么=0x22
图示: