Part1 面向对象(下)
一、面向对象的内存分析
1. 虚拟内存模型
1.1 从属于线程的区域(线程私有)
- 程序计数器:每个线程都有自己的程序计数器,存储当前线程正在执行的JVM指令的地址。
- 虚拟机栈:每个方法对应一个栈帧,调用方法时进栈,调用结束时出栈。该区域存储着方法中的存储局部变量、操作数、方法出口等信息。
- 本地方法栈:在调用本地方法时使用的栈,即调用操作系统提供的方法时使用的栈。
1.2 堆
所有创建的对象实例,都是在堆中分配内存,堆被所有的线程所共享,堆区域会被垃圾回收器做进一步的划分,划分成新生代、老年代。
一个JVM虚拟机只有一个堆,被线程共享,内存空间不连续,分配灵活,但速度慢。
1.3 方法区(静态区、元空间)
方法区是一种规范,在不同JDK版本中实现方式不一样。方法区也被所有线程所共享,实际上也是堆,存储被虚拟机加载的元数据,如类信息、常量、静态变量、运行时常量池等信息,存放程序中不变或者唯一的内容。
1.4 直接内存
2.程序执行过程的内存分析
class Person{int age;String name;public void Study(){System.out.println("好好学习,天天向上");}}public class Test {public static void main(String[] args) {Person p1 = new Person();//调用构造器Person p2 = new Person();p1.age = 1;p1.name = "abc";p1.Study();}}
3.参数传值机制
Java里的方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数的副本传入方法内,而实际参数本身不会受任何影响。
public class Test {public static void swap(int a, int b){int tmp = a;a = b;b = tmp;System.out.println("swap方法里,a的值是" + a + ",b的值是" + b);}public static void main(String[] args) {int a = 1;int b = 2;swap(a, b);System.out.println("交换结束后,变量a的值是" + a + ",b的值是" + b);}}//结果://在swap方法里,a的值是2,b的值是1//交换结束后,变量a的值是1,b的值是2
执行方法时,只把实参的值赋给形参,方法里操作的并不是实际的变量。对于引用类型的参数传递,一样采用的是值传递方式,但这里传递的是实参的地址,所以可以造成实参的实际改变。
4. 垃圾回收机制(Garbage Collection,缩写为GC)
4.1 垃圾回收的概念和特征
当程序创建对象、数组等引用类型实体时,系统会在堆内存中会它们分配内存,当这块内存不再被任何引用变量引用时,这块内存就变成了垃圾,等待垃圾回收机制进行回收。垃圾回收机制具有如下特征:
- 垃圾回收机制只回收堆内存中的对象,不回收任何物理资源(如数据库连接、网络IO等)
- 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候运行。当对象永久地失去引用后,系统会在合适的时候回收它所占的内存。
在垃圾回收机制回收任何对象之前,总会先调用finalize()方法,该方法可能导致对象重新复活(让一个引用变量指向这个对象),从而导致垃圾回收机制取消回收。
4.2 对象的3种状态
根据对象被引用变量所引用的状态,可把对象分为如下3种状态:
可达状态:有一个以上的引用变量引用它,则这个对象处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
- 不可达状态:当对象无任何引用变量引用它,且系统已经调用所有对象的finalize()方法后该对象没有恢复可达状态,则这个对象永久地失去引用,变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
可恢复状态:如果某个对象不再有任何引用变量引用它,它就处于可恢复状态。在垃圾回收机制运行时,系统会调用所有可恢复状态的对象的finalize()方法进行资源清理,该方法可能导致对象重新复活(让一个引用变量指向这个对象),则这个对象会再次变为可达状态,否则变为不可达状态。
4.3 垃圾回收算法
引用计数法:堆中的每个对象设置一个引用计数器,如果引用计数器的值为0,则这个对象是无用对象,垃圾回收机制对其进行回收。优点是算法简单,缺点是“循环引用的无用对象”无法识别。
//循环引用演示public class Student {Student s;public static void main(String[] args) {Student s1 = new Student();Student s2 = new Student();//形成循环引用s1.s = s2;s2.s = s1;s1 = null;//释放s1引用s2 = null;//释放s2引用}}//s1,s2互相引用对方,导致引用计数器不为0,但此时这两个对象不能被外部访问。

引用可达法(根搜索算法):把所有引用关系看作一张图,以其中一个节点为根,遍历整张图,寻找这个节点的引用节点,找到后,继续寻找这个节点的引用节点,当所有的引用节点找完后,剩下的节点就是无引用的节点,对其回收。
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()。
-
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
System.out.println()只能输出字符串,而Person实例是一个内存中的对象,为什么能打印输出?当使用该方法输出Person实例对象时,实际上输出的是Person对象的toString()方法的返回值,也就是说下面两行代码的效果完全一样,使用println时省略了toString。```javaSystem.out.println(p);System.out.println(p.toString());
1.2 toString()方法
toString()方法是Object类里的一个实例方法,所有对象都是Object类的实例,都具有toString()方法。不仅如此,所有的Java对象都可以和字符串进行连接,当Java对象和字符串连接时,系统自动调用toString()方法的返回值和字符串连接。
//下面两句代码等效var pStr = p + "";var pStr = p.toString() + "";
toString()方法是一个非常特殊的方法,它是一个“自我描述”的方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息,用来告诉外界该对象具有的状态信息。Object类提供的toString()方法总是返回该对象的“类名+@hashCode”,这个返回值并不能真正实现“自我描述”的功能,通常希望对这个方法进行重写。
class Person{private int age;private String name;public Person(int age, String name){this.age = age;this.name = name;}@Overridepublic String toString() {return "Person{" +"age=" + age +", name='" + name + '\'' +'}';}}public class Test {public static void main(String[] args) {Person p = new Person(23, "孙悟空");System.out.println(p);//打印p所引用的对象,输出描述信息}}//Apple{age = 23,name = 孙悟空}
大部分时候,重写toString()方法总是返回该对象的所有令人感兴趣的信息所组成的字符串。通常可返回如下格式的字符串:
类名{field1 = 值1, field2 = 值2,...}
注:可以使用快捷键Alt+insert快速插入重写的toString()方法。
Sting类的toSting()方法已经被改写过了,所以在使用println打印字符串引用变量的时候,返回的是对应的字符串。
public class Test {public static void main(String[] args) {String str = "abcde";System.out.println(str);//默认使用了toSting()方法}}//abcde
2.==和equals方法
2.1 ==判断
==用于判断两个变量是否相等,当两个变量都是基本数据类型,且都是数值类型(不一定要求数据类型严格相同),只要两个变量的值相等,则返回true。当两个变量是引用类型时,只要当两个引用变量都指向同一个对象时,才返回true,==不可用于比较类型上没有父子关系的两个对象。
2.2 equals()方法
很多时候,程序判断两个引用变量是否相等时,也希望有一种类似于“值判断”的判断规则,而不严格要求两个引用变量都指向同一个对象。比如判断两个字符串相等,只需要两个字符串的内容相同即可认为相等。此时,就引入了equals()方法。
equals()方法是Object类的一个实例方法,因此所有的对象都可以调用这个方法判断与其他引用变量是否相等,但直接使用这个方法和使用==判断没有区别,同样需要这两个引用变量指向同一个对象才返回true。因此Object类提供的equals方法没有太大实际意义,需要我们根据自己需求重写equals方法。equals方法要求是同一个类的两个对象,而不能一个是子类对象,一个是父类对象。
//equals演示:假设张三的小名为三三,我们如果要判断这两个人是否是同一个人,//只需要判断id是否相等,id相等就是同一个人,这里就需要对equals重写class Person {private int id;private String name;public Person(int id, String name){this.id = id;this.name = name;}//重写equals方法,只判断id@Overridepublic boolean equals(Object o) {if (this == o) return true;//用到了反射基础,判断两个对象是否为同一个类的对象,而不能用instanofif (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return id == person.id;//判断id是否相等,只要id相等就认为这两个引用变量相等}@Overridepublic int hashCode() {return Objects.hash(id);}}public class Test {public static void main(String[] args) {Person p1 = new Person(1001, "张三");Person p2 = new Person(1001, "三三");System.out.println(p1.equals(p2));//判断p1和p2的id是否相等}}//结果:true
注:使用Alt+insert可以快速插入重写的equals方法,根据自己需要选定要判断的参数。
Alt+insert ——> 
