Java学习笔记-内存分析
一般方法调用的内存图像
例子
//只要是类名就一定是标识符
//方法调用时,参数传递是‘值传递’
public class Test{
public static void main(String[] args){
int a = 10;
int b = 20;
int retValue = sumInt(a,b);
System.out.println("retValue: " + retValue);
}
public static int sumInt(int a,int b){
int result = a + b;
int num = 3;
int retValue = divide(result, num);
return retValue
}
public static int divide(int x,int y){
int z = x / y;
return z;
}
}
类创建时的内存图像
概述
1.类体 = 属性 + 方法
- 属性描述的是 : 状态
- 方法描述的是 : 行为动作
2.各种变量的含义 - 由于变量定义在类体当中,方法体之外,这种变量称为成员变量
- 类似于学生序号这样的成员变量,必须通过创建对象去访问,所以这种成员变量有称为实例变量
- 对象又称为实例,实例变量又被称为对象变量【对象级别的变量 】
- 实例变量存在在java对象的内部,创建一个对象就有一份,100个就有100份
- 想要访问实例变量,必须要有对象,不能通过"类名"的方式访问
3.java运算符 -new - 实例化对象的语法:new 类名()
- new运算符的作用是创建对象,在JVM堆内存中开辟新的内存空间
- 方法区内存:在类加载的时候,class字节码代码片段被加载到该内存空间中
- 栈内存:方法代码片段执行的时候,会给该方法分配内存空间,在栈内存中压栈
- 堆内存:new的对象在堆内存中存储
例子
public class Students{
int age;
boolean sex;
//姓名
//Sting是一种引用数据类型,代表字符串
//name是一个实例变量
//name是一个引用
String name;
//家庭住址
//Address是一种引用数据类型,代表家庭住址
//addr是一个实例变量
//addr是一个引用
Address addr;
}
public class Address{
String city;
Stirng Street;
}
public static void Test(Stirng[] atgs){
Students s1 = new Students("Zhang San",20,true);
}
java语言中的三大变量
JVM主要包括三块内存空间,分别是:栈内存、堆内存、方法区内存
-
堆内存和方法区内存各有1个,而栈内存是一个线程对应一个栈内存
-
只有局部变量不存在线程安全问题,实例变量和静态变量都存在线程安全问题,原因如1.所述,只有局部变量不会被多个线程共享。
-
方法的代码片段以及整个类的代码片段都被存储到方法区内存中,在在类加载的是时候这些代码片段会载入
-
方法调用的时候,该方法所需要的内存空间在栈内存中分配。在该方法执行结束之后,该方法所属的内存空间释放
-
栈中主要存储的是方法体当中的局部变量
-
在程序执行过程中使用new运算符创建的java对象,存储在堆内存当中,对象内部有实例变量,所以实例变量存储在堆内存中
-
变量分类:
- 局部变量 【方法体中声明】
- 成员变量 【方法体外声明】
- 实例变量 【不加static】
- 静态变量 【加static】
- 静态变量存储在方法区内存中
- 三块内存中变化最频繁的是栈内存,最先有数据的是方法区内存,垃圾回收器主要针对的是堆内存
- 垃圾回收器【自动垃圾回收机制,GC机制】什么时候会考虑将某个java对象的内存回收呢?
- 当堆内存当中的java对象成为垃圾数据的时候,会被垃圾回收器回收
- 什么时候堆内存中的java对象会变成垃圾呢?
- 没有更多的引用指向它的时候
- 这个对象无法被访问,因为访问对象只能通过引用的方式访问
例子
public class Chinese{
String idCard;
String name;
Static String country = "中国";
public Chinese(String idCard, String name){
this.idCard = idCard;
this.name = name;
}
}
public static void main(String[] args){
Chinese c1 = new Chinese("1243434353","张三");
Chinese c2 = new Chinese("7423983949","李四");
}
关键字 -this的内存图像
- this是一个变量,是一个引用。this保存当前对象的内存地址,指向自身。
- this存储在当前对象的堆内存中
- this可以使用在实例方法中,也可以使用在构造方法中,不能使用在静态方法中
- this()这种语法只能出现在构造方法的第一行,表示当前构造方法调用本类中其他的构造方法
- 目的:如果不存在这个语法,那么就需要对每一属性进行赋初值/默认值,而有了这个语法就可以实现代码复用
关键字 -super 子类继承时的内存图像
例子:
public class SuperTest03{
public static void main(String[] args){
CreditAccount ca1 = new CreditAccount();
CreditAccount ca2 = new CreditAccount("1111", 1000.0, 0.999);
}
}
class Account{
private String actno;
private double balance;
public Account(){
//super();
//this actno = null;
//this.balance = 0.0;
}
public Account(String actno, double balance){
this.actno = actno;
this.balance = balance;
}
}
class CreditAccount extends Account{
private double credit;
public CreditAccount(){}
public CreditAccount(String actno, double balance, double credit){
//super()存在的意义在构造方法中:不是代码复用,而是可以间接的在子类中方法父类的私有属性
//但是父类的私有属性又被子类继承,是存在于子类对象中的,详见内存图描述
super(actno, balance);
this.credit = credit;
}
}
补充:子类对父类的继承
子类是可以继承父类的所有方法和属性的,包括私有方法和私有属性。
当一个子类对象被创建的时候,首先会在内存中创建一个父类对象,然后在父类对象的外部放上子类独有的属性,这个两者结合起来形成了一个子类的对象。
子类是拥有父类的私有属性和方法,但无法直接使用。
1.两种父类的私有属性的访问方法:
- 在子类构造方法可以通过super关键字访问;super(),括号中调用父类的相同参数的构造函数
- 父类中有get和set方法,子类可以通过调用this.方法名访问父类的私有属性。
2.间接访问父类的私有方法:
当子类继承父类时,子类会继承父类所有的方法(不管这个方法的访问权限是private、protected、public的);
只不过在子类自己定义的方法中无法直接去访问继承而来的private方法罢了,但是可以通过继承而来的public及protected方法来间接访问继承而来的private方法。
即可以简单的将子类的方法分为两个版本,一是继承自父类的方法(简称继承版),二是自己定义的方法(简称自定版义版);
但是不管是继承版的还是自定义版的方法,它们都是属于这个子类的。
所以当子类实例化一个对象去调用这些方法时,这些方法中的this变量肯定指向这个对象本身(只不过访问继承版的private方法时,需要绕一点弯路)
例子:
class Father {
public Father() {
System.out.println("Father");
}
private void hello(Son son) {
System.out.println("hello");
System.out.println(this==son);//输出true,说明this和son引用的是同一个对象
}
public void sayHello(Son son) {
System.out.println(this instanceof Son);
this.hello(son);
}
}
public class Son extends Father {
public static void main(String[] args) {
Son s = new Son();
s.sayHello(s);
}
}
数组的内存图像
1.Java语言中的数组是一种引用数据类型
2.数组当中可以存储基本数据类型的数据,也可以存储引用数据类型的数据
3.数组因为是引用数据类型,所以数组对象是在堆内存中的
String类相关的内存图像
1.String表示字符串类型,属于引用数据类型,不属于基本数据类型
2.java中规定,双引号括起来的字符串,是不可变的
3.在java中随便使用双引号括起来的都是String对象。例如:"abc", "def"
4.在JDK当中双引号括起来的字符串,都是直接存储在方法区的"字符串常量池"当中的
- 因为实际使用中,字符串使用的太频繁。为了执行效率,所以放到字符串常量池
5.在学习完String类后,发现之前的String类内存图像的画法是有问题的
示例1 - 静态和动态创建字符串的内存图像
1.静态创建:
- 双引号括起来的都在字符串常量池中有一份
2.动态创建:
- 不仅在字符串常量池有一份,且在new对象的时候一定在堆内存当中开辟空间
public class StringTest01 {
public static void main(String[] args) {
//这两行代码表示底层创建了3个字符串对象,都在字符串常量池中
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
String s3 = new String("xy");
}
}
在下图可以看出静态和动态创建的区别:
示例2 -类的属性是String类
public class StringTest02 {
public static void main(String[] args) {
User u = new User("张三",110);
}
}
class User{
String name;
int id;
public User() {
}
public User(String name, int id) {
this.name = name;
this.id = id;
}
}
为什么判断字符串相等需要使用equals方法?
通过String类内存图的学习,可以得出答案,如下示例程序:
public class StringTest03 {
public static void main(String[] args) {
//"hello"存储在字符串常量池中,所以不会再新创建一份了
String s1 = "hello";
String s2 = "hello";
//内存地址相同,True
System.out.println(s1 == s2);
String s3 = new String("xy");
String s4 = new String("xy");
//堆内存中对象的内存地址不同,false
System.out.println(s3 == s4);
//通过以上例子,说明字符串的比较使用equals方法才能万无一失
//System.out.println(s1.equals("testString"));
//这种写法不保险,可能出现空指针异常,推荐以下写法:
System.out.println("testString".equals(s1));
}
}