Java-从JVM的角度重新理解面向对象(OOP)
背景
在初次学习面向对象的时候,我时时为那些抽象的概念所迷茫,如今从Jvm虚拟机运行原理重新理解面向对象,属实打开了一扇新世界的大门,理解了Java面向对象的内存机制,一切面向对象的概念在我眼中犹如透明
目录
什么是JVM
JVM虚拟机机制是Java跨平台兼容性的支撑,在任何安装了JVM虚拟机的平台,你的JAVA代码就能在任意平台无限切换,JVM虚拟机一般是在主机内存中专门开辟一块空间,在这个空间内处理Java代码
JVM的常见内存结构
在运行java程序时,JVM会将不同类型的数据放入不同类型的数据结构,本文涉及的仅有栈、堆
在jvm中,内存被分为堆与栈
- 栈:容量小,存取速度快,适合存储生命周期短的数据,存储变量(局部变量)与方法,先进后出(想象成一个瓶子,最先扔进去的一定在栈底,它一定最后才能出来)
- 堆:容量大,存取速度慢,适合存储生命周期长的数据,堆适合存储对象,对象一般在参数中是引用数据类型传递
从JVM角度看面向对象多场景
学会了JVM的内存机制,你再去看看继承、接口、多态、static、等等东西,它们在你眼里就是透明的了,我们不用再去琢磨那些乱七八糟的概念,眼里直接就有这些概念的背后逻辑,因为Java概念本身依赖就是JVM虚拟机机制
以下介绍了面向对象主要概念的JVM视角,看完你便能够举一反三,理解面向对象其他那些犄角旮旯的内容
程序运行
我们的Java程序未编译是一个.java文件,经过编译是一个.class文件,存储在硬盘中
这时如何我运行一个.class文件,且看内存如何执行
- 开辟一块内存空间,称JVM
- 将.class文件中的数据提取到JVM
- CPU调度运行
常量与变量判定
当数据加载到内存中时,我们按着运行期数据的是否变化把数据分成常量和变量
常量有如:圆周率Π、重力系数(9.8)
变量有如:mp3文件,其在内存中运行是时刻变化的
方法调用(值传递)的内存结构
示例代码
以下面这段代码为例,讲解jvm内存的运行机制
public class test01 {
public static void main(String[] args) {
int i = 1 ;
print(i);
}
public static void print(int i ){
System.out.println(i);
}
}
方法调用(值传递)内存结构图
- 在内存中,开辟一块JVM的空间
- 在JVM中,开辟栈空间与数据共享区空间
- 将类与类中方法调入数据共享区
- 在栈中开辟一块内存空间,主方法入
- 主方法空间中,开辟一块空间,主方法中的数据(int i = 1 )入
- 主方法调用print方法,在主方法空间中开辟一块空间,print方法入
- print方法空间中,未其方法的参数开辟一块空间(几个参数几块空间),参数入
- 将主方法中的int空间的值传给print方法中int空间
经过以上分析,很显然,主方法传入的实参与print方法的实参是两块不同的内存空间,这也从底层解释了值传递后,在方法中改变参数的值,并不会影响主方法中的参数值
引用数据类型的内存结构
数组
数组作为参数去传递是一个典型的引用数据类型传递的过程
我们说引用数据类型一般存储在堆中
示例代码
public class test01 {
public static void main(String[] args) {
int[] arr = {1,2,3} ;
}
且看主方法的执行在内存中如何体现
引用数据类型内存结构图
- JVM中的栈中开辟一块空间,主方法(main方法)入
- 主方法中的数组入堆,形成地址号
- 主方法空间内开辟一块空间,存储堆中数组的地址
以上解释了引用数据类型数据传递后,在方法中改变参数的值,将直接影响主方法中的参数值
因为实参与形参在内存中都是一块空间
对象
传递对象示例代码
实体类
public class Dog {
/*属性*/
private String name ;
private float height ;
/*构造器*/
public Dog(String name, float height) {
this.name = name;
this.height = height;
}
}
测试类
public class test01 {
public static void main(String[] args) {
/*创建两条狗*/
Dog dog = new Dog("小白",5) ;
Dog dog1 = new Dog("小黄",3) ;
}
}
传递对象内存结构图
道理与数组一样,不赘述
匿名对象的内存结构
匿名对象是指没有被引用的对象,由于没有栈中的变量来引用,所以就会被回收掉,所以匿名对象是无意义的,实际开发尽量避免匿名对象
匿名对象代码示例
public class test01 {
public static void main(String[] args) {
/*创建两条狗*/
new Dog("小白",5) ;
new Dog("小黄",3) ;
}
}
匿名对象内存结构图
有关This关键字的内存结构
this简介
在每一个对象的方法中都包含一个this关键字,代表调用当前方法的对象的自身(简记为对象的自身)(简记为方法被谁调用谁就是this)
this的本质其实就是一块内存空间的地址
如下所示
public class test01 {
public static void main(String[] args) {
/*创建一条狗*/
Dog dog = new Dog("小白",5) ;
System.out.println(dog);
}
}
打印的结果是Dog@1eec35
这个东东就是this,代表这条狗的地址
this的特点
- this只能在类的对象方法中使用(所谓类的对象方法,可以记成不带static的方法)
- this可以在方法内区分同名的类的属性和参数名,有this的一定是属性,没有this的一定是方法的参数名。
如狗的实体类构造器
public Dog(String name, float height) {
this.name = name;
this.height = height;
}
this.name代表狗对象的属性
无this的name代表传递过来参数
this的内存结构图
示例代码
实体类代码
public class Dog {
/*属性*/
private String name ;
private float height ;
/*构造器*/
public Dog(String name, float height) {
this.name = name;
this.height = height;
}
/*吃翔*/
public void eatShit(){
System.out.println(this.name+"在吃翔") ;
}
}
测试类代码
public class test01 {
public static void main(String[] args) {
/*创建两条狗*/
Dog dog = new Dog("小白",5) ;
Dog dog1 = new Dog("小黄",3) ;
dog.eatShit();
dog1.eatShit();
}
}
小白在吃翔
小黄在吃翔
且看其内存结构图如下
0x9999与0x9988就是this
这里还要说一下,所有方法(类方法、对象方法)都存放在资源共享区,方法中涉及的属性依据数据类型存放在堆或栈中
类的继承的内存结构
示例代码
/**
* 父类
*/
class Father{
String name;
int age;
public void sleep(){
System.out.println("爸爸在睡觉");
}
}
/**
* 子类
*/
class Son extends Father{
public void playGameGame(){
System.out.println(name+"正在玩耍"+" 年龄:"+age);
}
}
/*子类*/
class Daughter extends Father{
public void drinkMilk(){
System.out.println(name+"正在喝奶"+" 年龄:"+age);
}
}
class ExtendsDemo2{
public static void main(String[] args){
Son son = new Son();
son.name = "儿子";
son.age = 3;
son.playGameGame();
Daughter dau = new Daughter();
dau.name = "女儿";
dau.age = 4;
dau.drinkMilk();
}
}
结果:
儿子正在玩耍 年龄:3
女儿正在喝奶 年龄:4
内存结构图
大概说一下要点,以女儿为例
-
在创建女儿对象时,发现女儿类继承自父亲类,女儿对象入堆,并在女儿对象的空间内开辟一块空间,将其所继承的父亲类的name属性存入该空间
-
dau.name = "女儿"; dau.age = 4;
设置女儿对象的name和age属性,首先在女儿对象的空间内查找该属性,查询不到,到父亲空间内查询,这时就查到了,将所查设置为所设
- 在执行对象方法drinkMilk时,发现出现name属性,首先在女儿对象的空间内查找该属性,查询不到,到父亲空间内查询,这时就查到了