06-对象的内存解析

目录

  • 类加载
    • 静态块
  • 对象的内存模型
    • 对象的创建与使用
    • 类变量与实例变量的区别
    • 类方法与实例方法的区别
    • this 不能在类方法中使用
  • 对象生命周期:初始化与消亡
  • 对象消亡
    • 垃圾回收机制--finalize()
  • 参数传递

类加载与对象创建的初始化过程

类加载

类加载:类第一次被调用,由JVM虚拟机进行加载。一般来说,类只会被加载一次。

  • 类加载 早于 创建对象。

  • 类加载做什么?

    1. 初始化类变量(静态变量);

    2. 分配类方法的函数入口(静态方法);

    3. 执行静态块。

    4. 执行main()

    • 静态块:定义在类体中的代码段。语法格式:static{ 代码段.......}
      • 作用:执行一些静态资源的加载。
    • 不能以静态方式访问非静态的成员

创建对象

对象的创建:执行创建new对象。

  • 创建对象 晚于 类加载。
  • 对象的创建做什么?
    1. 初始化实例变量;
    2. 分配实例方法的函数入口;
    3. 执行对象块;
    4. 执行构造函数(super();剩余语句才是第四部分执行)

案例1:定义Person类

探讨:类加载和创建对象时,静态/非静态的变量/方法的运行情况。

public class Person {
//静态变量
	static String country ="China"; 
//实例变量
	int age; 
	String name = "张三";
//静态块
	static {
		System.out.println(country);//静态变量在类加载时初始化
		//System.out.println(this.age); //实例变量在类加载时没有初始化,所以不能操作,也不能加this关键字,此时还没有有对象。
		System.out.println("static{}这是第一个静态代码块。。");
	}
//对象块
	{ 
		//在当前代码块中没有同名的局部变量,可以不使用this访问全局变量。
		System.out.println(country);
		System.out.println(age); 
		System.out.println("{} 这是一个对象块,非静态代码块");
	}
	
	static { //多个静态块时,按照顺序执行
		System.out.println("这是第二个静态代码块。。。");
	}
//构造函数
	public Person() {
		System.out.println("Person()这是一个无参构造方法。。");
	}
	public Person(String name,int age) {
		this.name = name;
		this.age = age;
		System.out.println("Person(String name,int age)这是一个有参构造方法。。");
	}
//静态方法	
	static void show() {	//类方法,类或对象调用时才加载。
		System.out.println("show() 是静态方法。。");
	}
//实例方法
	public void fun01() { //实例方法,对象调用时才加载
		System.out.println("fun01()这是一个实例方法。。。");
	}
//自测
	public static void main(String[] args) {
		//探讨类加载和创建对象时 Person类的执行顺序:
		//1.没创建对象(初始化)前,要访问类中的变量/方法,只能是(通过类名.)静态的static修饰的变量/方法。
		//静态代码块的执行时机:在对类进行调用时执行。静态块是第一个执行且只执行一次;
		//执行顺序:静态块 > 调用的静态变量/方法(根据顺序执行)
         System.out.println("-------创建对象前----------");
		Person.show();
		System.out.println("访问类变量:"+Person.country);
		
		System.out.println("-------创建对象后----------");
		//2.创建对象(new+构造函数进行初始化)后,才可以访问非静态的变量/方法(通过对象.)
		//对象块(非静态代码块)执行时机:每次创建new对象时,都会执行一次非静态代码块和调用的构造函数.
		//执行顺序:如果类还没有被加载,则先执行静态块>再执行对象块>当前的构造函数>调用的变量/方法。
		new Person(); //创建匿名对象:没有对象名的,把整个当作一个对象。每个匿名对象都是独立的。用完就消失。
		Person p01 = new Person();
		System.out.println("p01.name="+p01.name);
//		new Person().name="李四";
//		p01.name="王五";
//		System.out.println("new Person().name="+new Person().name);
//		System.out.println("p01.name="+p01.name);
		System.out.println("new Person().name="+new Person("张柳",18).name);
		p01.fun01();
		System.out.println(p01.country);
	}
}

注意事项:

在静态方法中,不能直接使用this。this代表当前对象 ,对象没有被创建。

//实例变量在类加载时没有初始化,所以不能操作,也不能加this关键字,此时还没有有对象。

结论

  • 只会执行一次类加载:

    1. 初始化静态变量和静态方法
    2. 执行静态代码块,静态变量和静态代码块的执行顺序,根据编写先后决定;——第一次调用才执行
    3. 如果本类有main()方法,并执行本程序,则main()在静态代码块执行后才调用。
  • 如果类已经加载过,创建对象:

    1. 初始化实例变量和实例方法==> 编写顺序,初始化顺序

    2. 执行非静态代码块(对象块),先后顺序由编写顺序决定;

    3. 执行当前的构造函数;

    4. 根据调用的顺序执行/访问 方法或者变量。

    • 每次的 创建new对象,都会重复创建对象的操作(1-3步骤)。
  • 注意,写程序 编写顺序问题,执行时机 顺序先后:

    • 先声明 变量 接着去写 构造函数, 最后对象块/静态块 /方法
    • 避免出现 初始化导致的调用异常

最后,当程序运行结束,对象不在被引用,则对象可垃圾回收器 销毁。

对象的内存模型

对象的创建与使用

创建对象:

  1. 声明一个类类型变量 ;
  2. new分配空间内存,构造函数初始化成员变量。

对象的内存情况

  1. Student s01 = new Student();
  2. Student s01 =null; //引用数据类型 默认值都是null
  3. 左边:声明一个该类类型的变量,只是一个简单的变量。——此时的s01 变量值为null,不能使用(出现空指针异常)
  4. 右边:通过该类的构造函数new一个对象,从内存中申请开辟的内存空间。通过赋值运算符 = ,把右边对象的内存空间首地址,赋予 左边这个简单变量。此时,左边的变量指向右边对象的引用,该变量是引用变量。——引用数据类型:引用一个数据类型指向一个对象,指向对象的变量就是引用变量。

JVM内存结构


博客:https://www.cnblogs.com/frankyou/p/9041734.html

  • 类装载器在JVM中负责装载.class文件
  • 当JVM使用类装载器定位class文件,并将其输入到内存中时,会提取class文件的类型信息,并将这些信息存储到方法区中。同时放入方法区中的还有该类型中的类变量、方法表等。
  • Java 程序在运行时创建的所有类型对象和数组都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。但是堆中开辟对象的空间并没有任何人工指令可以回收,而是通过JVM的垃圾回收器负责回收
  • 对于一个运行的Java而言,每一个线程都有一个PC寄存器。当线程执行Java程序时,PC寄存器的内容总是下一条将被执行的指令地址
  • 每启动一个线程,JVM都会为它分配一个Java栈,用于存放方法中的局部变量,操作数以及异常数据等。当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧。并将该栈帧压入Java栈中,方法执行完毕时,JVM会弹出该栈帧并释放掉。
  • 运行Java的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,他要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片级指令直接执行字节码,或者间接通过JIT(即时编译器)执行编译过的本地代码。


案例2:通过画图来解析 一个对象内存图。(画图工具)


这是一个比较好的博客内存分析:https://blog.csdn.net/qq_40086115/article/details/94439319

class Person {
//静态变量
	static String country ="China"; 
//实例变量
	int age; 
	String name = "张三";
	
    public void eat(){
        System.out.println("一天吃三餐..");
    }
    public static void test(){
         System.out.println("静态方法..");
    }
}


public class TestPerson{   //主类
    public static void main(String[] args){
        Person p01 =new Person();  //创建 Person类对象
        p01.eat();
    }
}

对象消亡——垃圾回收器

垃圾回收器:是Java虚拟机自动调用的。用于回收堆区分配的对象。(堆区内存不够的情况下调用)

内存释放:当一个对象的引用不存在(即对象不再被调用),Java会认为该对象不再需要,它所占的内存会被释放掉——等待垃圾回收器回收。

所以,Java自动回收垃圾,无需开发人员控制。如果想强制垃圾回收,可以调用: System.gc();

重写Object 类的finalize()方法

方法原型来自父类Object。Object类是Java语言中的根类(超类/父类),它所描述的所有方法子类都可以使用,所有类在创建对象的时候,最终找的父类就是Object。

当对象被释放时,将自动调用对象的finalize()方法。

案例3:定义TestGC类

探讨对象的消亡情况

public class TestGC {
	
	static void testFun() {
		TestGC obj1 = new TestGC();
		System.out.println("obj1=" + obj1);
		TestGC obj2 = new TestGC();
		System.out.println("obj2=" + obj2);
	}
	
	//重写Object父类中的finalize()
	//在垃圾回收器删除对象之前,自动调用对象finalize()
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("对象被回收了 finalize() this="+this);
	}
	
	public static void main(String[] args) {
		TestGC tgc1 = new TestGC();
		System.out.println(tgc1); //day06_objectMemory.TestGC@7852e922  
		TestGC tgc2 = new TestGC();
        System.out.println(tgc2);//4e25154f
		boolean flag = tgc1==tgc2;
		System.out.println(flag);
		tgc1 = tgc2; //tgc2的引用地址 赋给tgc1,此时tgc1原来的引用,在堆中的对象实体,就没有再被引用了。
		//7852e922 此时是无用的对象实体。等待垃圾回收器回收。
		System.out.println(tgc1);//4e25154f
        tgc1 =null; //不在指向任何对象实体。即该变量变为一个简单变量,没有分配空间。
		//System.out.println(tgc1.name);
		testFun();
		// System.gc() 运行垃圾回收器。调用 System.gc()暗示着 Java 虚拟机做了一些努力来回收无用对象。
		System.gc();
	}
}
  • 注意:
  1. 有几种方式可以使对象的实体变为空闲状态,即无用状态:
    • 赋值操作:
      • 1)对象被另一个对象赋值,本身指向的对象实体地址被覆盖。即此时对象指向另一个对象的对象实体
      • 2)置空,对象指向对象实体的引用,被置空null。
    • 在代码段中声明对象和初始化对象,随着代码段执行完毕,指向对象实体的引用变量(对象名)消亡,此时对象实体而变为空闲。
    • 隐形的, 当以上都没有发生的情况. 就是随着程序消失而消失
  2. 子类可以重写 finalize()方法配置系统资源或执行其他清理。在垃圾回收器删除对象之前,对这个对象调用的。

类变量与实例变量的区别

创建对象的顺序:

  1. 类加载	
  2. 创建对象 

一个类可以有多个对象,每个对象都有专属的实体(实例变量),也有一些共同的共性(类变量)。

两者区别

  1. 声明的形式:类变量static来声明;实例变量没有static声明
  2. 访问方式:
    • 类变量可以通过【类或者对象】访问类变量,推荐通过【类名**.** 】来调用
    • 实例变量只能通过【对象】访问实例变量。
  3. 共享性:类变量是所有对象共有的,当修改类变量,后面的对象访问时,都是修改后的类变量;实例变量是每个对象独有的,修改自己的实例变量不影响其他对象。
  4. 生命周期:
    • 类变量(静态变量)是属于类的,在类加载时分配内存,在程序退出时释放内存;
    • 实例变量是属于对象的,在创建对象时才分配内存,在该对象不存在(不被调用)时实例变量也不存在,随对象消失而消失。

类方法与实例方法的区别

  1. 初始化:类方法在类加载时分配入口地址;实例方法时创建对象时分配入口地址
  2. 访问方式:类方法可以通过【类或者对象】访问类变量,推荐通过【类名**.** 】来调用;实例方法只能通过【对象】访问实例变量。
  3. 类方法中不能使用this关键字,因为this表示当前对象的引用,而类方法时在类加载时初始化,此时还没有对象。

参数传递

  1. 方法调用时,如有参数(简单变量/对象),则参数通过实际参数进行初始化;——即参数必须有具体值。
  2. 参数传递分两种:值传递 + 引用传递
    1. 值传递(传值):
      1. 基本数据类型。
      2. 实参的值永远不发生变化。
    2. 引用传递(传地址):
      1. 引用类型的参数传递。
      2. 实参的内存地址不变化,但是内容有可能变化
public class TestParam {
	int var;
	
	public void testVule(int aa) { //aa=10
		System.out.println("testVule()中形参aa="+aa);
		aa++; //aa=11
		System.out.println("testVule()中改变后的形参aa="+aa);
	}
	
	public static void change(TestParam tparam1, TestParam tparam2) { //形参
		System.out.println("------change()-------");
		System.out.println("tparam1地址:"+tparam1);
		System.out.println("tparam2地址:"+tparam2);
		System.out.println("tparam1.var="+tparam1.var +",tparam2.var="+tparam2.var);
		tparam1 = tparam2;
		System.out.println("改变tparam1地址后:tparam1地址="+tparam1);
		tparam1.var = 30;
		System.out.println("tparam1.var="+tparam1.var +",tparam2.var="+tparam2.var);
	}
	
	public static void main(String[] args) {
		//值传递:不考虑实参的内存地址,实参值永远不变化
		//基本数据类型采用值传递
		TestParam tp1 = new TestParam();
		tp1.var=10;
		System.out.println("值传递前tp1.var="+tp1.var);
		tp1.testVule(tp1.var); //10
		System.out.println("值传递后tp1.var="+tp1.var); //10


		System.out.println("-----------分隔符-----------");
		//引用传递:实参的内存地址不变化,但是内容有可能变化
		TestParam tp2 = new TestParam();
		tp2.var =20;
		System.out.println("引用传递前tp1地址:"+ tp1);
		System.out.println("引用传递前tp2地址:"+ tp2);
		System.out.println("引用传递前tp1.var="+ tp1.var + ",tp2.var"+tp2.var);
		change(tp1, tp2);
		System.out.println("引用传递后tp1地址:"+ tp1);
		System.out.println("引用传递后tp2地址:"+ tp2);
		System.out.println("引用传递后tp1.var="+ tp1.var + ",tp2.var"+tp2.var);
	}
}