Java学习笔记 -- 第七章 面向对象(二):内存底层

写在开头:今天我们来学习面向对象的第二部分:内存底层的初步了解。我们通过对Java虚拟机内存底层的分析,能够更加理解java程序的执行过程与面向对象。本部分内容可能有些晦涩难懂,但是我们只是初步了解,所以可以不去细究。若对此感兴趣的小伙伴可以去查阅有关JVM的博客或者视频。还是那句话:假传万卷书,真传一案例。让我们 生死看淡,不服就干!
PS:本文是个人学习笔记,用于个人复习。文章内容引用尚学堂java300集教学。

内存底层

    Ⅰ Java虚拟机内存模型
    Ⅱ 程序执行过程
    Ⅲ 垃圾回收机制
    Ⅳ this和static

Ⅰ Java虚拟机内存模型


     从属于线程的内存区域(栈、计数器):
     JVM的内存划分中,有部分区域是线程私有的,有部分是属于整个JVM 进程;我们将这部分归为一类。
     程序计数器(Program Counter Register),在 JVM 规范中,每个线程都有自己的 程序计数器。这是一块比较小的内存空间,存储当前线程正在执行的 Java 方法的 JVM 指令 地址,即字节码的行号。如果正在执行 Native 方法,则这个计数器为空。
     Java 虚拟机栈(Java Virtal Machine Stack),同样也是属于线程私有区域,每个线 程在创建的时候都会创建一个虚拟机栈,生命周期与线程一致,线程退出时,线程的虚拟机 栈也回收。虚拟机栈内部保持一个个的栈帧,每次方法调用都会进行压栈,JVM 对栈帧的 操作只有出栈和压栈两种,方法调用结束时会进行出栈操作。该区域存储着局部变量表,编 译时期可知的各种基本类型数据、对象引用、方法出口等信息。
     本地方法栈(Native Method Stack)与虚拟机栈类似,本地方法栈是在调用本地 方法时使用的栈,每个线程都有一个本地方法栈。
     堆:
     堆(Heap),几乎所有创建的 Java 对象实例,都是被直接分配到堆上的。堆被所有的 线程所共享,在堆上的区域,会被垃圾回收器做进一步划分,例如新生代、老年代的划分。 Java 虚拟机在启动的时候,可以使用“Xmx”之类的参数指定堆区域的大小。
     方法区:
     方法区与堆一样,也是所有的线程所共享,存储被虚拟机加载的元(Meta)数据,包 括类信息、常量、静态变量、即时编译器编译后的代码等数据。 方法区是一种 java 虚拟机的规范。由于方法区存储的数据和堆中存储的数据一致,实 质上也是堆,因此,在不同的 JDK 版本中方法区的实现方式不一样。
     运行时常量池:
     这是方法区的一部分。常量池主要存放两大类常量:1. 字面量(Literal),如文本字符串、final 常量值;2. 符号引用,存放了与编译相关的一些常量,因为 Java 不像 C++那样有连接的过程, 因此字段方法这些符号引用在运行期就需要进行转换,以便得到真正的内存入口地址。
     直接内存:
     直接内存并不属于 Java 规范规定的属于 Java 虚拟机运行时数据区的一部分。Java 的 NIO 可以使用 Native 方法直接在 java 堆外分配内存,使用 DirectByteBuffer 对象作为这 个堆外内存的引用。

Ⅱ 程序执行过程(重点)

  为了方便我们理解Java程序执行过程中内存的变化,我们将Java虚拟机内存模型进行简化,将其简单地分为三个区域:虚拟机栈stack、堆heap、方法区moethod area
  虚拟机栈的特点如下:
    - 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变 量、操作数、方法出口等)
    - JVM 为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变 量等)
    - 栈属于线程私有,不能实现线程间的共享!
    - 栈的存储特性是“先进后出,后进先出”
    - 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
  堆的特点如下:
    - 堆用于存储创建好的对象和数组(数组也是对象)
    - JVM 只有一个堆,被所有线程共享
    - 堆是一个不连续的内存空间,分配灵活,速度慢!
  方法区(又叫静态区,也是堆)特点如下:
    - 方法区是 JAVA 虚拟机规范,可以有不同的实现。(JDK7 以前是“永久代”;JDK7 部分去除“永久代”,静态变量、字符串常量池都挪到了堆内存中;JDK8 是“元数据空间”和堆结合起来。)
    - JVM 只有一个方法区,被所有线程共享!
    - 方法区实际也是堆,只是用于存储类、常量相关的信息!
    - 用来存放程序中永远是不变或唯一的内容。(类信息【Class 对象,反射机制中会 重点讲授】、静态变量、字符串常量等)

  我们以下面这段代码为例,分析一下程序运行时内存的变化

//Person类
class Person {
	String name;
	int age;
	public void show(){
		System.out.println("姓名:"+ name +",年龄:"+age);
	}
}
//创建personl类对象并使用
public class TestPerson {
	public static void main(String[ ] args) {
		// 创建p1对象
		Person p1 = new Person();
		p1.age = 24;
		p1.name = "张三";
		p1.show();
		// 创建p2对象
		Person p2 = new Person();
		p2.age = 35;
		p2.name = "李四";
		p2.show();
	}
}

我们从main函数开始看:

public static void main(String[ ] args){
	Person p1 = new Person();

进入main函数,压入一个main函数栈帧,函数参数为args,指向null。Person p1是个类引用,此时main栈帧中创建一个引用指向null。同时方法区开始加载Person类信息

	Person p1 = new Person();

new Person() 是一个方法,调用该方法,栈空间压入一个该方法的栈帧;在堆中创建Person对象,对象所有的属性和方法从方法区的类信息获取。此时默认调用无参构造方法给新建对象属性赋默认初值,方法指向类信息中的方法信息。Person对象创建好后,p1指向该对象的地址。

	p1.age = 24;
	p1.name = "张三";

new Person()方法结束,栈帧出栈。执行赋值语句。

	p1.show();

执行show()方法,栈空间压入栈帧。show()方法自带参数this,该参数指向自身对象地址。(关于this指针后边会详细介绍)

p1.show()方法执行完,栈帧出栈。接下来进入对象p2的创建。创建p2流程和p1完全一样。我们直接跳到执行完p2.show(),p2.show()栈帧出栈。

接下来main()方法结束,栈帧出栈,栈空间清空,程序结束;堆空间与方法区残留的无用信息由垃圾回收机制自动收回。

Ⅲ 垃圾回收机制

  1. GC基本原理
      Java的内存管理很大程度指得就是:堆中对象的管理,其中包括对象空间的分配和释放。
        对象空间的分配:使用 new 关键字创建对象即可
        对象空间的释放:将对象赋值 null 即可。垃圾回收器将负责回收所有”不可达”对象 的内存空间

  垃圾回收的过程:
    第一步:发现无用的对象
    第二步:回收无用对象占用的内存空间

  三个要点:
    - 程序员无权调用垃圾回收器
    - 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
    - finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用

  1. 回收算法
      垃圾回收机制保证可以将“无用的对象”进行回收,无用的对象指得是没有任何变量引用该对象
      Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理
      相关算法:引用计数法;引用可达法(根搜索法)

  2. 分代机制
      原理:不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

      年轻代:所有新生成的对象首先都是放在Eden区;年轻代的目标就是尽可能快速的收集掉那些生命周期短的现象,对应的是Minor GC,每次Minor GC会清理年轻代的内存,算法采取效率较高的复制算法,频繁的操作,但是会浪费内存空间;当“年轻代”区域放满后,就将对象放到年老区。
      年老代:在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中;年老代中存放的都是一些生命周期较长的对象;年老代对象越来越多,我们就需要启动Major GC和Full GC,来一次大扫除,全面清理年轻代区域和年老代区域。
      永久代:用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响;JDK7以前就是“方法区”的一种实现;JDK8以后已经没有“永久代”了,使用metaspace元数据空间和堆替代。
      三种GC:
        - Minor GC:用于清理年轻代区域,Eden区满了就会触发一次Minor GC;清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中。
        - Major GC:用于清理年老代区域
        - Full GC:用于清理年轻代、年老代区域‘成本较高,会对系统性能产生影响。

  3. 容易造成泄露的操作
      - 创建大量无用对象
      - 静态集合类的使用
      - 各种连接对象(IO 流对象、数据库连接对象、网络连接对象)未关闭
      - 监听器的使用不当

Ⅳ this和static(重点)

  1. this关键字
      对象创建过程和this的本质:
        构造方法是创建 Java 对象的重要途径,通过 new 关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:
        (1)分配对象空间,并将对象成员变量初始化为 0 或空
        (2)执行属性值的显式初始化
        (3)执行构造方法
        (4)返回对象的地址给相关的变量
        this 的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。 因此,在构造方法中也可以使用 this 代表“当前对象”。

      this的最常用法:
        - 在程序中产生二义性之处,应使用 this 来指明当前对象;普通方法中,this 总是指 向调用该方法的对象。构造方法中,this 总是指向正要初始化的对象。
        - 使用 this 关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法 中用,并且必须位于构造方法的第一句。
        - this 不能用于 static 方法中。

  2. static关键字
      在类中,用 static 声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周 期和类相同,在整个应用程序执行期间都有效。它有如下特点:
        - 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。
        - 对于该类的所有对象来说,static 成员变量只有一份。被该类的所有对象共享!
        - 一般用“类名.类属性/方法”来调用。(也可以通过对象引用或类名(不需要实例化访问静态成员。)
        - 在 static 方法中不可直接访问非 static 的成员。

下面我们稍微修改下之前的代码(加入this和static),再来感受一下程序运行时内存的变化

//Person类
	class Person {
	String name;
	int age;
	static String nationality;	//静态变量:国籍
	
	//有参构造函数
	public Person(String name, int age){
		this.name = name;
		this.id = id;
	}
	
	public void show(){
		System.out.println("姓名:"+ name +",年龄:"+age);
		
	//静态方法:打印国籍
	public static void printNationality(){
		System.out.println(nationality);
	}
	}
public class TestPerson {
    public static void main(String[] args) {
        // 创建p1对象
        Person p1 = new Person("张三", 24);
        p1.show();

        p1.printNationality();
        p1.nationality = "Chinese";
        p1.printNationality();
    }
}

同样,我们从main函数开始分析。

public static void main(String[] args) {
        // 创建p1对象
        Person p1 = new Person("张三", 24);
        p1.show();

进入main函数,向栈空间压入栈帧。Person p1,方法区开始加载Person类相关信息,包括代码结构,静态属性,静态方法等等。接下来为对象分配空间,并初始化成员变量。属性值显式初始化,调用构造方法,向栈空间压入构造方法的栈帧。(如下图)
构造方法结束,出栈。调用p1.show(),压栈。(此过程图略)

在这里插入图片描述

	p1.printNationality();
	p1.nationality = "Chinese";
	p1.printNationality();

调用静态方法,打印国籍;修改nationality的值,再次打印国籍。从这里就可以看出,静态属性和方法都存储于方法区,故静态方法可以调用静态属性和静态方法,但不能调用成员变量和方法。反之,成员方法可以调用静态变量和静态方法。这种关系就像汽车图纸和汽车的关系一样:汽车图纸不可能实施汽车的操作,比如启动发动机;但是汽车却可以实施图纸上的某个功能,比如打开安全气囊。

在这里插入图片描述

发布了11 篇原创文章 · 获赞 3 · 访问量 908

猜你喜欢

转载自blog.csdn.net/IceTiger_/article/details/105352329
今日推荐