面向对象的内存分析
JAVA 虚拟机内存模型概念
Java 虚拟机的内存可以分为三个区域:栈 stack、堆 heap、方法区 method area。
虚拟机栈(简称:栈)的特点如下:
- 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变 量、操作数、方法出口等)
- JVM 为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变 量等)
- 栈属于线程私有,不能实现线程间的共享!
- 栈的存储特性是“先进后出,后进先出” 5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
堆的特点如下:
- 堆用于存储创建好的对象和数组(数组也是对象)
- JVM 只有一个堆,被所有线程共享
- 堆是一个不连续的内存空间,分配灵活,速度慢!
- 堆被所有的线程所共享,在堆上的区域,会被垃圾回收器做进一步划分,例如新生代、老年代的划分。
方法区(也是堆)特点如下:
1.方法区是 JAVA 虚拟机规范,可以有不同的实现。
i. JDK7 以前是“永久代”
ii. JDK7 部分去除“永久代”,静态变量、字符串常量池都挪到了堆内存中
iii. JDK8 是“元数据空间”和堆结合起来。
2. JVM只有一个方法区,被所有线程共享!
3. 方法区实际也是堆,只是用于存储类、常量相关的信息!
4.用来存放程序中永远是不变或唯一的内容。(类信息【Class 对象,反射机制中会 重点讲授】、静态变量、字符串常量等)
5.常量池主要存放常量:如文本字符串、final 常量值
【示例 】编写 Person 类并分析内存
public class Person {
String name;
int age;
public void show(){
System.out.println(name);
}
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();
Person p3 = p1;
Person p4 = p1;
p4.age = 80;
System.out.println(p1.age);
}
}
参数传值机制
Java 中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。(我们得到的是“原参数的复印件,而不是原件”。)
· 基本数据类型参数的传值
传递的是值的副本。 副本改变不会影响原件。
· 引用类型参数的传值
传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向 了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了 改变”。
【示例】多个变量指向同一个对象
public class test public class User {
int id; //id
String name; //账户名
String pwd; //密码
public User(int id, String name) {
this.id = id;
this.name = name;
}
public static void main(String[ ] args) {
User u1 = new User(100, "吴小美");
User u3 = u1;
System.out.println(u1.name);
u3.name="张三";
System.out.println(u1.name);
}
}
垃圾回收机制(Garbage Collection)
Java 引入了垃圾回收机制,Java 程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。
垃圾回收原理和算法
- 内存管理
Java 的内存管理很大程度就是:堆中对象的管理,其中包括对象空间的分配和释放。
对象空间的分配:使用 new 关键字创建对象即可对象空间的释放:将对象赋值 null 即可。
·
- 垃圾回收过程
任何一种垃圾回收算法一般要做两件基本事情:
-
发现无用的对象
-
回收无用对象占用的内存空间。
垃圾回收机制保证可以将“无用的对象”进行回收。
无用的对象指的就是没有任何变量引用该对象。Java 的垃圾回收器通过相关算法发现 无用对象,并进行清除和整理。 -
垃圾回收相关算法
- 引用计数法
堆中的每个对象都对应一个引用计数器,当有引用指向这个对象时,引用计数器加
1,而当指向该对象的引用失效时(引用变为 null),引用计数器减 1,最后如果该 对象的引用计算器的值为 0 时,则 Java 垃圾回收器会认为该对象是无用对象并对其 进行回收。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。
【示例】循环引用演示
代码中,s1 和 s2 互相引用对方,导致他们引用计数不为 0,但是实际已经无用,但无法被识别。
public class Student {
String name;
Student friend;
public static void main(String[ ] args) {
Student s1 = new Student();
Student s2 = new Student();
s1.friend = s2;
s2.friend = s1;
s1 = null;
s2 = null;
}
}
- 引用可达法(根搜索算法)
程序把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用 节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找 完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
通用的分代垃圾回收机制
分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此, 不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状 态:年轻代、年老代、永久代。同时,将处于不同状态的对象放到堆中不同的区域。
- 年轻代
所有新生成的对象首先都是放在 Eden 区。年轻代的目标就是尽可能快速的收集掉 那些生命周期短的对象,对应的是 Minor GC,每次 Minor GC 会清理年轻代的内存, 算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域 存放满对象后,就将对象存放到年老代区域。 - 年老代
在年轻代中经历了 N(默认 15)次垃圾回收后仍然存活的对象,就会被放到年老代 中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越 多,我们就需要启动 Major GC 和 Full GC(全量回收),来一次大扫除,全面清理年轻 代区域和年老代区域。 - 永久代
用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响。JDK7 以前就是“方法区”的一种实现。JDK8 以后已经没有“永久代”了,使用 metaspace 元数据空间和堆替代。
·Minor GC:
用于清理年轻代区域。Eden 区满了就会触发一次 Minor GC。清理无用对象,将有用 对象复制到“Survivor1”、“Survivor2”区中。
·Major GC:
用于清理老年代区域。
·Full GC: 用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
JVM 调优和 Full GC
在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能 导致 Full GC:
- 年老代(Tenured)被写满
- 永久代(Perm)被写满
- System.gc()被显式调用
- 上一次 GC 之后 Heap 的各域分配策略动态变化
开发中容易造成内存泄露的操作
内存泄漏:
指堆内存由于某种原因程序未释放,造成内存浪费,导致运行速度减慢甚至系统崩溃等。
tip:
在实际开发中,经常会造成系统的崩溃。如下这些操作我们应该注意这些使用场景。请大家学完相关内容后,回头过来温习下面的内容。不要求此处掌握相关细节。
如下四种情况时最容易造成内存泄露的场景,请大家开发时一定注意:
- 创建大量无用对象
比如:大量拼接字符串时,使用了 String 而不是 StringBuilder。
String str = "";
for (int i = 0; i < 10000; i++) {
str += i; //相当于产生了 10000 个 String 对象
}
- 静态集合类的使用
像 HashMap、Vector、List 等的使用最容易出现内存泄露,这些静态变量的生命周期 和应用程序一致,所有的对象也不能被释放。
3. 各种连接对象(IO 流对象、数据库连接对象、网络连接对象)未关闭
IO 流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网 络连接,不使用的时候一定要关闭。
4. 监听器的使用不当
释放对象时,没有删除相应的监听器
其他要点
- 程序员无权调用垃圾回收器。
- 程序员可以调用 System.gc(),该方法只是通知 JVM,并不是运行垃圾回收器。尽量 少用,会申请启动 Full GC,成本高,影响系统性能。
- Object 对象的 finalize 方法,是 Java 提供给程序员用来释放对象或资源的方法,但 是尽量少用