Part1 面向对象(下)

一、面向对象的内存分析

1. 虚拟内存模型

image.png

1.1 从属于线程的区域(线程私有)

  • 程序计数器:每个线程都有自己的程序计数器,存储当前线程正在执行的JVM指令的地址。
  • 虚拟机栈:每个方法对应一个栈帧,调用方法时进栈,调用结束时出栈。该区域存储着方法中的存储局部变量、操作数、方法出口等信息。
  • 本地方法栈:在调用本地方法时使用的栈,即调用操作系统提供的方法时使用的栈。

栈是连续的内存空间,由系统自动分配,先进后出,速度快。

1.2 堆

所有创建的对象实例,都是在堆中分配内存,堆被所有的线程所共享,堆区域会被垃圾回收器做进一步的划分,划分成新生代、老年代。
一个JVM虚拟机只有一个堆,被线程共享,内存空间不连续,分配灵活,但速度慢。

1.3 方法区(静态区、元空间)

方法区是一种规范,在不同JDK版本中实现方式不一样。方法区也被所有线程所共享,实际上也是堆,存储被虚拟机加载的元数据,如类信息、常量、静态变量、运行时常量池等信息,存放程序中不变或者唯一的内容。

1.4 直接内存

直接内存不属于Java虚拟机运行时数据区的一部分,

2.程序执行过程的内存分析

  1. class Person{
  2. int age;
  3. String name;
  4. public void Study(){
  5. System.out.println("好好学习,天天向上");
  6. }
  7. }
  8. public class Test {
  9. public static void main(String[] args) {
  10. Person p1 = new Person();//调用构造器
  11. Person p2 = new Person();
  12. p1.age = 1;
  13. p1.name = "abc";
  14. p1.Study();
  15. }
  16. }

Day5 - 图2

3.参数传值机制

Java里的方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数的副本传入方法内,而实际参数本身不会受任何影响。

  1. public class Test {
  2. public static void swap(int a, int b){
  3. int tmp = a;
  4. a = b;
  5. b = tmp;
  6. System.out.println("swap方法里,a的值是" + a + ",b的值是" + b);
  7. }
  8. public static void main(String[] args) {
  9. int a = 1;
  10. int b = 2;
  11. swap(a, b);
  12. System.out.println("交换结束后,变量a的值是" + a + ",b的值是" + b);
  13. }
  14. }
  15. //结果:
  16. //在swap方法里,a的值是2,b的值是1
  17. //交换结束后,变量a的值是1,b的值是2

执行方法时,只把实参的值赋给形参,方法里操作的并不是实际的变量。对于引用类型的参数传递,一样采用的是值传递方式,但这里传递的是实参的地址,所以可以造成实参的实际改变。
Day5 - 图3

4. 垃圾回收机制(Garbage Collection,缩写为GC)

4.1 垃圾回收的概念和特征

当程序创建对象、数组等引用类型实体时,系统会在堆内存中会它们分配内存,当这块内存不再被任何引用变量引用时,这块内存就变成了垃圾,等待垃圾回收机制进行回收。垃圾回收机制具有如下特征:

  • 垃圾回收机制只回收堆内存中的对象,不回收任何物理资源(如数据库连接、网络IO等)
  • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候运行。当对象永久地失去引用后,系统会在合适的时候回收它所占的内存。
  • 在垃圾回收机制回收任何对象之前,总会先调用finalize()方法,该方法可能导致对象重新复活(让一个引用变量指向这个对象),从而导致垃圾回收机制取消回收。

    4.2 对象的3种状态

    根据对象被引用变量所引用的状态,可把对象分为如下3种状态:

  • 可达状态:有一个以上的引用变量引用它,则这个对象处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。

  • 不可达状态:当对象无任何引用变量引用它,且系统已经调用所有对象的finalize()方法后该对象没有恢复可达状态,则这个对象永久地失去引用,变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
  • 可恢复状态:如果某个对象不再有任何引用变量引用它,它就处于可恢复状态。在垃圾回收机制运行时,系统会调用所有可恢复状态的对象的finalize()方法进行资源清理,该方法可能导致对象重新复活(让一个引用变量指向这个对象),则这个对象会再次变为可达状态,否则变为不可达状态。

    4.3 垃圾回收算法

  • 引用计数法:堆中的每个对象设置一个引用计数器,如果引用计数器的值为0,则这个对象是无用对象,垃圾回收机制对其进行回收。优点是算法简单,缺点是“循环引用的无用对象”无法识别。

    1. //循环引用演示
    2. public class Student {
    3. Student s;
    4. public static void main(String[] args) {
    5. Student s1 = new Student();
    6. Student s2 = new Student();
    7. //形成循环引用
    8. s1.s = s2;
    9. s2.s = s1;
    10. s1 = null;//释放s1引用
    11. s2 = null;//释放s2引用
    12. }
    13. }
    14. //s1,s2互相引用对方,导致引用计数器不为0,但此时这两个对象不能被外部访问。

    image.png

  • 引用可达法(根搜索算法):把所有引用关系看作一张图,以其中一个节点为根,遍历整张图,寻找这个节点的引用节点,找到后,继续寻找这个节点的引用节点,当所有的引用节点找完后,剩下的节点就是无引用的节点,对其回收。

    4.4 分代垃圾回收机制(大学问,可以学完Java SE后回来深究)

    分代垃圾回收机制是基于这样一个事实:不同的对象的生命周期是不一样的。因此不同生命周期的对象可以采用不同的回收算法,以提高回收效率。根据对象的生命周期将对象分为三种状态:年轻代、年老代、永久代,将处于不同状态的对象放在堆中的不同区域。JVM将堆内存空间划分为:Eden、Survivor和Tenured/Old空间。

  • 年轻代:所有的新生对象都是放在Eden区,保存生命周期较短的对象。年轻代分三个区:Eden区、Survivor1区、Survivor2区(一般而言)。Eden区可容纳的对象较少,当Eden区满时,会调用Minor GC,清理Eden区,清理后仍然存活的对象会被复制到Survivor1区,当Survivor1区满时,又会调用Minor GC,清理Survivor1区,存活的对象被复制到Survivor2区,当Survivor2区满时,里面如果是从Survivor1区复制过来的对象则放到Tenured区(年老区),如果不是,则会被复制到Survivor1区。需要注意的是,Survivor区的两个区域是对称的,没有先后之分,对象在这两个区域反复复制。同时,Survivor区是可以设置多个的,增加对象在年轻代的存在时间,减少放到Tenured区的可能。

  • 年老代:在年轻代中经历了N次(15次)垃圾回收仍然存活的对象会被放到年老代中,因此年老代保存生命周期较长的对象。当年老代满时,会调用Major GC和Full GC,Full GC会对整个堆区都会进行清理,Major GC只会清理年老区,因此Full GC和Major GC的时空开销比Minor GC要大。
  • 永久代:存放静态文件,如类信息、方法等,与垃圾回收关系不大。

分代垃圾回收博客:https://www.cnblogs.com/donleo123/p/14567139.html
Major GC和Full GC的区别:https://www.zhihu.com/question/41922036
Major GC和Full GC的区别:https://blog.csdn.net/zl1zl2zl3/article/details/88654850

4.5 JVM调优和Full GC(大学问,可以学完Java SE后回来深究)

在对JVM调优的过程中,很大一部分工作就是对Full GC的调节,即什么时候调用Full GC清理内存。导致调用Full GC的原因:

  • 年老代(Tenured)被写满。
  • 永久代(Perm)被写满。
  • 显式调用System.gc()。
  • 上一次GC之后,堆的各域分配策略动态变换。

    4.6 实际开发中容易造成内存泄漏的操作

  • 创建大量无用的对象。比如需要大量拼接字符串时,使用String而不是StringBulider。

  • 静态集合类的使用。像HashMap、Vector、List的使用最容易内存泄漏,这些静态变量的声明周期和应用程序一致,所有的对象也不能被释放。
  • 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭,IO流对象、数据库连接对象、网络连接对象和硬盘或网络进行连接,不使用的时候一定要关闭。
  • 监听器的使用。释放对象时,没有删除相应的监听器。

    4.7 其他要点

  • 程序员无权调用垃圾回收器。

  • 调用System.gc()时,只是通知建议系统进行垃圾回收,并不是运行垃圾回收器(所以不一定会真正执行)。尽量少使用,会申请Full GC,成本高,影响系统性能。
  • finalize()是系统提供给程序员释放对象或资源的方法,但尽量不要使用,应该交给垃圾回收机制来管理。

    二、处理对象

    1.打印对象和toString方法

    1.1 打印对象

    ```java package com.sundegan;

class Person{ private String name; public Person(String name){ this.name = name; } } public class Test { public static void main(String[] args) { Person p = new Person(“孙悟空”); System.out.println(p);//打印p所引用的对象 } } //结果;com.sundegan.Person@776ec8df

  1. System.out.println()只能输出字符串,而Person实例是一个内存中的对象,为什么能打印输出?当使用该方法输出Person实例对象时,实际上输出的是Person对象的toString()方法的返回值,也就是说下面两行代码的效果完全一样,使用println时省略了toString
  2. ```java
  3. System.out.println(p);
  4. System.out.println(p.toString());

1.2 toString()方法

toString()方法是Object类里的一个实例方法,所有对象都是Object类的实例,都具有toString()方法。不仅如此,所有的Java对象都可以和字符串进行连接,当Java对象和字符串连接时,系统自动调用toString()方法的返回值和字符串连接。

  1. //下面两句代码等效
  2. var pStr = p + "";
  3. var pStr = p.toString() + "";

toString()方法是一个非常特殊的方法,它是一个“自我描述”的方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息,用来告诉外界该对象具有的状态信息。Object类提供的toString()方法总是返回该对象的“类名+@hashCode”,这个返回值并不能真正实现“自我描述”的功能,通常希望对这个方法进行重写。

  1. class Person{
  2. private int age;
  3. private String name;
  4. public Person(int age, String name){
  5. this.age = age;
  6. this.name = name;
  7. }
  8. @Override
  9. public String toString() {
  10. return "Person{" +
  11. "age=" + age +
  12. ", name='" + name + '\'' +
  13. '}';
  14. }
  15. }
  16. public class Test {
  17. public static void main(String[] args) {
  18. Person p = new Person(23, "孙悟空");
  19. System.out.println(p);//打印p所引用的对象,输出描述信息
  20. }
  21. }
  22. //Apple{age = 23,name = 孙悟空}

大部分时候,重写toString()方法总是返回该对象的所有令人感兴趣的信息所组成的字符串。通常可返回如下格式的字符串:

  1. 类名{field1 = 1, field2 = 2,...}

注:可以使用快捷键Alt+insert快速插入重写的toString()方法。
Sting类的toSting()方法已经被改写过了,所以在使用println打印字符串引用变量的时候,返回的是对应的字符串。

  1. public class Test {
  2. public static void main(String[] args) {
  3. String str = "abcde";
  4. System.out.println(str);//默认使用了toSting()方法
  5. }
  6. }
  7. //abcde

2.==和equals方法

2.1 ==判断

==用于判断两个变量是否相等,当两个变量都是基本数据类型,且都是数值类型(不一定要求数据类型严格相同),只要两个变量的值相等,则返回true。当两个变量是引用类型时,只要当两个引用变量都指向同一个对象时,才返回true,==不可用于比较类型上没有父子关系的两个对象。

2.2 equals()方法

很多时候,程序判断两个引用变量是否相等时,也希望有一种类似于“值判断”的判断规则,而不严格要求两个引用变量都指向同一个对象。比如判断两个字符串相等,只需要两个字符串的内容相同即可认为相等。此时,就引入了equals()方法。
equals()方法是Object类的一个实例方法,因此所有的对象都可以调用这个方法判断与其他引用变量是否相等,但直接使用这个方法和使用==判断没有区别,同样需要这两个引用变量指向同一个对象才返回true。因此Object类提供的equals方法没有太大实际意义,需要我们根据自己需求重写equals方法。equals方法要求是同一个类的两个对象,而不能一个是子类对象,一个是父类对象。

  1. //equals演示:假设张三的小名为三三,我们如果要判断这两个人是否是同一个人,
  2. //只需要判断id是否相等,id相等就是同一个人,这里就需要对equals重写
  3. class Person {
  4. private int id;
  5. private String name;
  6. public Person(int id, String name){
  7. this.id = id;
  8. this.name = name;
  9. }
  10. //重写equals方法,只判断id
  11. @Override
  12. public boolean equals(Object o) {
  13. if (this == o) return true;
  14. //用到了反射基础,判断两个对象是否为同一个类的对象,而不能用instanof
  15. if (o == null || getClass() != o.getClass()) return false;
  16. Person person = (Person) o;
  17. return id == person.id;//判断id是否相等,只要id相等就认为这两个引用变量相等
  18. }
  19. @Override
  20. public int hashCode() {
  21. return Objects.hash(id);
  22. }
  23. }
  24. public class Test {
  25. public static void main(String[] args) {
  26. Person p1 = new Person(1001, "张三");
  27. Person p2 = new Person(1001, "三三");
  28. System.out.println(p1.equals(p2));//判断p1和p2的id是否相等
  29. }
  30. }
  31. //结果:true

注:使用Alt+insert可以快速插入重写的equals方法,根据自己需要选定要判断的参数。
Alt+insert ——> image.pngimage.png