笔记来自2019求知讲堂零基础Java入门编程视频教程 https://www.bilibili.com/video/av76235341
JVM的内存模型
-
栈:
①栈描述的是方法执行的内存模型,每个方法被调用时都会创建一个栈帧,用来存储局部变量、操作数方法出口等。
②JVM为每个线程创建一个栈,用于存放该线程执行方法的信息
③栈是私有的,线程之间不共享
④栈的存储特性:先进后出,后进先出
⑤是一个连续的内存空间,由系统自动分配,速度快。 -
堆
①堆用于存放创建好的对象和数组(数组也是对象)
②JVM只有一个堆,被所有线程共享
③堆是一个不连续的内存空间,分配灵活,速度慢 -
方法区
①JVM只有一个方法区,被所有线程共享
②方法区实际上也堆
③用来存放程序中不变或唯一的内容
java类实例化的内存分析
简单类对象的实例化过程
这里有一个Person类
public class Person {
public Person() {
}
int age = 1;
String name = "zhangsan";
int sex = 0;
public void showInfo() {
System.out.println("年龄:" + this.age);
System.out.println("姓名:" + this.name);
System.out.println("性别:" + this.sex);
}
public void setInfo(int age, String name, int sex) {
this.age = age;
this.name = name;
this.sex = sex;
}
}
下图描述了在执行Person p = new Person();时,JVM内存上是如何进行实例化的。
①我们要进行Person类的实例化,所以第一步是将Person.class加载到方法区
②实例化的变量名称是p,在栈中开辟空间,声明这个变量(此时变量还没有被赋值)
③执行到new Person()创建对象,我们知道所有对象时存放在堆内存中的,所以在堆内存中开辟空间,分配地址。
④在这个对象空间中先进行默认初始化,再进行显示初始化(如果有的话)。
⑤构造方法进栈(java的方法是运行在栈上的,调用时进栈,方法结束出栈),按照方法体内代码进行初始化。
⑥初始化结束后,将对象在堆内存中的地址赋给栈内存中的变量p,构造方法出栈。
子类对象的实例化过程
这里有一个Person类和Student类
public class Person {
public Person() {
int age = 100;
}
int age = 1;
String name = "zhangsan";
int sex = 0;
}
public class Student extends Person{
public Student(){
super();
int age = 500;
}
String school;
}
下图描述了在执行Student stu = new Student();时,JVM内存上是如何进行实例化的。
①在方法区加载class文件,先加载父类,后加载子类
②在栈中申请空间,声明变量stu
③在堆内存中为new Student()开辟空间分配地址
④在对象空间中,对属性进行默认初始化(包括子类和父类的属性),此时age、sex=0,school、name=null
⑤子类构造方法进栈,此时发现子类构造方法第一行调用了父类的构造方法,所以需要先执行父类构造方法
⑥父类构造方法进栈之前需要先对父类的属性进行显示初始化,所以此时age = 1
⑦父类构造方法进栈,执行初始化代码,此时age = 100
⑧父类构造方法执行完毕后,继续执行第⑤步子类构造方法之前需要先对子类属性进行显示初始化(这里没有显示初始化,age = 100),然后继续执行子类构造方法,age= 500
⑨初始化完毕之后,将对象在堆内存中的地址赋给栈内存中的变量stu,构造方法出栈。
一个乱七八糟的测试例子
主要测试:
①子类继承父类时,若子类重写了父类的方法,父类的方法就完全被覆盖了;成员变量则不会出现这样的情况,即使子类定义了和父类相同的成员变量,这个变量不会覆盖父类中的。
②子类对象的实例化过程
public class Person {
public Person() {
System.out.print("这是在Person类构造函数内的输出,");
System.out.print("此时age=" + this.age);
this.show();
System.out.println("对象是地址" + System.identityHashCode(this));
age = 100;
}
int age = 1;
String name = "zhangsan";
int sex = 0;
public void show() {
System.out.print("这是在Person类构造函数中调用的方法,");
System.out.println("此时age=" + age);
}
}
public class Student extends Person{
public Student(){
super();
System.out.print("这是在Student类构造函数内super()之后的输出,");
System.out.println("此时age=" + age);
this.show();
super.show();
age = 500;
}
int age = 1000;
String school;
public void show() {
System.out.print("这是在Student类构造函数中调用的方法,");
System.out.print("此时age=" + this.age);
System.out.print("此时name=" + name);
System.out.println("父类的age=" + super.age);
}
}
public class Test {
public static void main(String[] args) {
Student stu = new Student();
System.out.println(stu.age);
System.out.println(System.identityHashCode(stu));
}
}
/* 运行结果
这是在Person类构造函数内的输出,此时age=1这是在Student类构造函数中调用的方法,此时age=0此时name=zhangsan父类的age=1
对象是地址1361960727
这是在Student类构造函数内super()之后的输出,此时age=1000
这是在Student类构造函数中调用的方法,此时age=1000此时name=zhangsan父类的age=100
这是在Person类构造函数中调用的方法,此时age=100
500
1361960727
*/
根据上面子类实例化过程和运行结果,整理这段代码的运行情况:
①首先对父类和子类的属性进行默认初始化,此时父类的age、sex = 0,name = null,子类的age = 0,school = null;
②子类构造方法进栈,发现第一行调用了父类的构造方法;
③执行父类构造方法之前,先对父类的属性进行显示初始化,这里有定义,所以age = 1,name = “zhangsan” ,sex = 0 ;
④执行父类构造方法
//父类构造方法
public Person() {
System.out.print("这是在Person类构造函数内的输出,");
//这里的输出age=1,是父类构造方法执行前的显示初始化的结果,说明属性没有被子类同名属性的值影响
System.out.print("此时age=" + this.age);
//此处虽为this.show(),从运行结果看,实际调用的是子类的show()
this.show();
//此处输出this所指代的对象地址,发现就是子类对象stu的地址,所以上面调用的方法也是子类的方法,体现了子类对父类方法的完全覆盖。
System.out.println("对象是地址" + System.identityHashCode(this));
age = 100;
}
⑤父类构造方法中调用的show()方法(从运行结果看,调用的是子类的show()方法)
public void show() {
System.out.print("这是在Student类构造函数中调用的方法,");
//子类和父类定义了相同的属性不会相互影响,这时的子类属性只经过了默认初始化,所以age = 0;
System.out.print("此时age=" + this.age);
//根据继承关系,从父类继承了name属性,所以name = "zhangsan"
System.out.print("此时name=" + name);
//super.age 指父类中的age属性,所以值为1
System.out.println("父类的age=" + super.age);
}
⑥父类构造方法执行结束,子类构造方法继续执行之前对子类属性显式初始化,此时age= 1000;
⑦执行子类构造方法
public Student(){
super();
System.out.print("这是在Student类构造函数内super()之后的输出,");
//子类属性显示初始化之后,所以age = 1000
System.out.println("此时age=" + age);
//调用子类show()方法⑧
this.show();
//调用父类show()方法⑨
super.show();
age = 500;
}
⑧子类构造方法中的this.show();
public void show() {
System.out.print("这是在Student类构造函数中调用的方法,");
//age = 1000
System.out.print("此时age=" + this.age);
//name = "zhangsan"
System.out.print("此时name=" + name);
//父类构造方法执行完毕,所以age = 100
System.out.println("父类的age=" + super.age);
}
⑨子类构造方法中的super.show();
public void show() {
System.out.print("这是在Person类构造函数中调用的方法,");
//age = 100
System.out.println("此时age=" + age);
}
⑩对象创建完毕,执行Test类中的输出语句,最终age = 500。
垃圾回收机制(Garbage Collection)
C++等语言的垃圾回收是由程序员手动进行的,Java的垃圾回收是由系统自动进行的。
-
内存管理:java的内存管理很大程度指的是堆中的对象的管理,其中包括对象空间的分配和释放。
对象空间的分配:使用new关键字创建对象
对象空间的释放:将对象赋值为null即可。垃圾回收器将负责回收所有“不可达”对象的额内存空间。 -
垃圾回收过程
1.发现无用对象(没有被引用的对象)
2.回收无用对象占用的内存空间。 -
垃圾回收相关算法
1.引用计数法:每个对象对应一个引用计数器,当有引用指向这个对象,计数器加1,当指向该对象的引用失效,计数器减1,当计数器的值为0时,Java垃圾回收器认为该对象无用,并对其进行回收。该方法优点是算法简单,缺点是无法识别“循环引用的无用对象”。
2.引用可达法(根搜索法):程序把所有引用关系看作一张图,从一个节点GC ROOT开始,寻找对应引用节点,找到这个节点后,继续寻找这个节点的引用节点,当所有节点寻找完毕之后,剩下的节点则被认为是没有被引用到的节点,即无用节点。
分代垃圾回收机制
不同的对象的生命周期是不同的,因此不同生命周期的对象可以采用不同的回收算法,以提高回收效率。将对象分为三种状态:年轻代、年老代、永久代。同时将处于不同状态的对象放到堆中的不同区域。JVM将堆内存分为Eden、Survivor和Tenured/Old空间。
- 年轻代(Eden、Survivor):年轻代的目的是尽可能快速的收集掉生命周期短的对象,对应的是Minor GC,Minor GC采用效率较高的算法,频繁的进行操作。所有新生成的对象首先都放在Eden区,Eden区满了之后进行回收,仍然存活的对象就存放到Survivor区域,然后在Survivor1、Survivor2中循环的进行回收,超过15次回收仍然存活的对象将被放到年老代。
- 年老代:在年轻代中经历N(默认15)次垃圾回收后仍然存在的对象,就会被放入年老代区域中。因此年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,就需要启动Major GC和Full GC,进行一次大扫除,全面清理年轻代和年老代区域。
- 永久代:用于存放静态文件,如Java类,方法等。持久代对垃圾回收没有显著影响。JDK7以前,就是方法区的一种实现。JDK8以后没有永久代了,使用metaspace元数据数据空间和堆代替。
注:
程序员无权调用垃圾回收器,调用System.gc()只是申请启动Full GC垃圾回收。
Finalize方法时Java提供给程序员用来释放对象或资源的方法,但尽量少用。