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个,而栈内存是一个线程对应一个栈内存

  2. 只有局部变量不存在线程安全问题,实例变量和静态变量都存在线程安全问题,原因如1.所述,只有局部变量不会被多个线程共享。

  3. 方法的代码片段以及整个类的代码片段都被存储到方法区内存中,在在类加载的是时候这些代码片段会载入

  4. 方法调用的时候,该方法所需要的内存空间在栈内存中分配。在该方法执行结束之后,该方法所属的内存空间释放

  5. 栈中主要存储的是方法体当中的局部变量

  6. 在程序执行过程中使用new运算符创建的java对象,存储在堆内存当中,对象内部有实例变量,所以实例变量存储在堆内存中

  7. 变量分类:

  • 局部变量 【方法体中声明】
  • 成员变量 【方法体外声明】
    • 实例变量 【不加static】
    • 静态变量 【加static】
  1. 静态变量存储在方法区内存
  2. 三块内存中变化最频繁的是栈内存,最先有数据的是方法区内存,垃圾回收器主要针对的是堆内存
  3. 垃圾回收器【自动垃圾回收机制,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的内存图像

  1. this是一个变量,是一个引用。this保存当前对象的内存地址,指向自身。
  2. this存储在当前对象的堆内存中
  3. this可以使用在实例方法中,也可以使用在构造方法中,不能使用在静态方法中
  4. 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));
    }
}
原文地址:https://www.cnblogs.com/zy200128/p/12633581.html