
06-对象的内存解析
06-对象的内存解析
目录
- 类加载
- 静态块
- 对象的内存模型
- 对象的创建与使用
- 类变量与实例变量的区别
- 类方法与实例方法的区别
- this 不能在类方法中使用
- 对象生命周期:初始化与消亡
- 对象消亡
- 垃圾回收机制--finalize()
- 参数传递
类加载与对象创建的初始化过程
类加载
类加载:类第一次被调用,由JVM虚拟机进行加载。一般来说,类只会被加载一次。
-
类加载 早于 创建对象。
-
类加载做什么?
-
初始化类变量(静态变量);
-
分配类方法的函数入口(静态方法);
-
执行静态块。
-
执行main()
- 静态块:定义在类体中的代码段。语法格式:static{ 代码段.......}
- 作用:执行一些静态资源的加载。
- 不能以静态方式访问非静态的成员
-
创建对象
对象的创建:执行创建new对象。
- 创建对象 晚于 类加载。
- 对象的创建做什么?
- 初始化实例变量;
- 分配实例方法的函数入口;
- 执行对象块;
- 执行构造函数(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关键字,此时还没有有对象。
结论
-
只会执行一次类加载:
- 初始化静态变量和静态方法
- 执行静态代码块,静态变量和静态代码块的执行顺序,根据编写先后决定;——第一次调用才执行
- 如果本类有main()方法,并执行本程序,则main()在静态代码块执行后才调用。
-
如果类已经加载过,创建对象:
-
初始化实例变量和实例方法==> 编写顺序,初始化顺序
-
执行非静态代码块(对象块),先后顺序由编写顺序决定;
-
执行当前的构造函数;
-
根据调用的顺序执行/访问 方法或者变量。
- 每次的 创建new对象,都会重复创建对象的操作(1-3步骤)。
-
-
注意,写程序 编写顺序问题,执行时机 顺序先后:
- 先声明 变量 接着去写 构造函数, 最后对象块/静态块 /方法
- 避免出现 初始化导致的调用异常
最后,当程序运行结束,对象不在被引用,则对象可垃圾回收器 销毁。
对象的内存模型
对象的创建与使用
创建对象:
- 声明一个类类型变量 ;
- new分配空间内存,构造函数初始化成员变量。
对象的内存情况
- Student s01 = new Student();
- Student s01 =null; //引用数据类型 默认值都是null
- 左边:声明一个该类类型的变量,只是一个简单的变量。——此时的s01 变量值为null,不能使用(出现空指针异常)
- 右边:通过该类的构造函数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)对象被另一个对象赋值,本身指向的对象实体地址被覆盖。即此时对象指向另一个对象的对象实体
- 2)置空,对象指向对象实体的引用,被置空null。
- 在代码段中声明对象和初始化对象,随着代码段执行完毕,指向对象实体的引用变量(对象名)消亡,此时对象实体而变为空闲。
- 隐形的, 当以上都没有发生的情况. 就是随着程序消失而消失
- 赋值操作:
- 子类可以重写 finalize()方法配置系统资源或执行其他清理。在垃圾回收器删除对象之前,对这个对象调用的。
类变量与实例变量的区别
创建对象的顺序:
1. 类加载
2. 创建对象
一个类可以有多个对象,每个对象都有专属的实体(实例变量),也有一些共同的共性(类变量)。
两者区别:
- 声明的形式:类变量static来声明;实例变量没有static声明
- 访问方式:
- 类变量可以通过【类或者对象】访问类变量,推荐通过【类名**.** 】来调用
- 实例变量只能通过【对象】访问实例变量。
- 共享性:类变量是所有对象共有的,当修改类变量,后面的对象访问时,都是修改后的类变量;实例变量是每个对象独有的,修改自己的实例变量不影响其他对象。
- 生命周期:
- 类变量(静态变量)是属于类的,在类加载时分配内存,在程序退出时释放内存;
- 实例变量是属于对象的,在创建对象时才分配内存,在该对象不存在(不被调用)时实例变量也不存在,随对象消失而消失。
类方法与实例方法的区别
- 初始化:类方法在类加载时分配入口地址;实例方法时创建对象时分配入口地址
- 访问方式:类方法可以通过【类或者对象】访问类变量,推荐通过【类名**.** 】来调用;实例方法只能通过【对象】访问实例变量。
- 类方法中不能使用this关键字,因为this表示当前对象的引用,而类方法时在类加载时初始化,此时还没有对象。
参数传递
- 方法调用时,如有参数(简单变量/对象),则参数通过实际参数进行初始化;——即参数必须有具体值。
- 参数传递分两种:值传递 + 引用传递
- 值传递(传值):
- 基本数据类型。
- 实参的值永远不发生变化。
- 引用传递(传地址):
- 引用类型的参数传递。
- 实参的内存地址不变化,但是内容有可能变化
- 值传递(传值):
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);
}
}