写在开头:今天我们来学习面向对象的第二部分:内存底层的初步了解。我们通过对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()方法结束,栈帧出栈,栈空间清空,程序结束;堆空间与方法区残留的无用信息由垃圾回收机制自动收回。
Ⅲ 垃圾回收机制
- GC基本原理
Java的内存管理很大程度指得就是:堆中对象的管理,其中包括对象空间的分配和释放。
对象空间的分配:使用 new 关键字创建对象即可
对象空间的释放:将对象赋值 null 即可。垃圾回收器将负责回收所有”不可达”对象 的内存空间
垃圾回收的过程:
第一步:发现无用的对象
第二步:回收无用对象占用的内存空间
三个要点:
- 程序员无权调用垃圾回收器
- 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
- finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用
-
回收算法
垃圾回收机制保证可以将“无用的对象”进行回收,无用的对象指得是没有任何变量引用该对象
Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理
相关算法:引用计数法;引用可达法(根搜索法) -
分代机制
原理:不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
年轻代:所有新生成的对象首先都是放在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:用于清理年轻代、年老代区域‘成本较高,会对系统性能产生影响。 -
容易造成泄露的操作
- 创建大量无用对象
- 静态集合类的使用
- 各种连接对象(IO 流对象、数据库连接对象、网络连接对象)未关闭
- 监听器的使用不当
Ⅳ this和static(重点)
-
this关键字
对象创建过程和this的本质:
构造方法是创建 Java 对象的重要途径,通过 new 关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:
(1)分配对象空间,并将对象成员变量初始化为 0 或空
(2)执行属性值的显式初始化
(3)执行构造方法
(4)返回对象的地址给相关的变量
this 的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。 因此,在构造方法中也可以使用 this 代表“当前对象”。this的最常用法:
- 在程序中产生二义性之处,应使用 this 来指明当前对象;普通方法中,this 总是指 向调用该方法的对象。构造方法中,this 总是指向正要初始化的对象。
- 使用 this 关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法 中用,并且必须位于构造方法的第一句。
- this 不能用于 static 方法中。 -
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的值,再次打印国籍。从这里就可以看出,静态属性和方法都存储于方法区,故静态方法可以调用静态属性和静态方法,但不能调用成员变量和方法。反之,成员方法可以调用静态变量和静态方法。这种关系就像汽车图纸和汽车的关系一样:汽车图纸不可能实施汽车的操作,比如启动发动机;但是汽车却可以实施图纸上的某个功能,比如打开安全气囊。