2.1 引用计数
什么是引用计数呢?打个比方A a = new A()
,代码中 A 对象被引用 a 所持有,此时引用计数就会 +1 ,如果 a 将引用置为 null 即a = null
此时对象 A 的引用计数就会变为 0 ,GC算法检测到 A 对象引用计数为 0 就会将其回收。很简单,但引用计数存在一定弊端
场景如下:
A a = new A();
B b = new B();
a.next = b;
b.next = a;
a = null;
b = null;
执行完上述代码后 A 和 B 对象会被回收吗?看似引用都已经置为 null ,但实际上 a 和 b 的 next 分别持有对方引用,形成了一种相互持有引用的局面,导致A 和 B 即使成了垃圾对象且不能被回收。有些同学可能会说,内存泄漏太容易看出来了, a 和 b 置空前将各自的 next 置为空不就完了。嗯,这样说没错,但是在实际业务中面对庞大的业务逻辑内存泄漏是很难一眼看出的。所以JVM在后来摒弃了引用计数,采用了可达性分析。
2.2 可达性分析
可达性分析其实是数学中的一个概念,在JVM中,会将一些特殊的引用作为 GcRoot ,如果通过 GcRoot 可以访达的对象不会被当作垃圾对象。换种方式说就是,一个对象被 GcRoot 直接 或 间接持有,那么该对象就不会被当作垃圾对象。用一张图表示大概就是这个样子:
图中A、B、C、D可
以被 GcRoot 访达,所以不会被回收。E、F
不能被 GcRoot 访达,所以会被标记为垃圾对象。最典型的是G、H
,虽说相互引用,但不能被 GcRoot 访达,所以也会被标记为垃圾对象。综上所述: 可达性分析 可以解决 引用计数 中 对象相互引用 不能被回收的问题。
什么类型的引用可作为 GcRoot 呢。 大概有如下四种:
- 栈中局部变量
- 方法区中静态变量
- 方法区中常量
- 本地方法栈JNI的引用对象
注意点
千万不要把引用和对象两个概念混淆,对象是实实在在存在于内存中的,而引用只是一个变量/常量并持有对象在内存中的地址指。
下面我来通过一些代码来验证几种 GcRoot
局部变量
笔者是用Android代码进行调试,不懂Android的同学把onCreate
视为main
方法即可。
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
method();
}
private void method(){
Log.i("test","method start");
A a = new A();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i("test","method end");
}
class A{
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize A");
}
}
}
提示
- 在java中一个对象被回收会调用其
finalize
方法
- JVM中垃圾回收是在一个单独线程进行。为了更好的验证效果,在此加2000毫秒延时
打印结果如下:
17:58:57.526 method start
17:58:59.526 method end
17:58:59.591 finalize A
method 方法执行时间是2000毫秒,对象A 在 method 方法结束立即被回收。所以可以认定栈中局部变量可作为 GcRoot
本地方法区静态变量
public class MyApp extends Application {
private static A a;
@Override
public void onCreate() {
super.onCreate();
Log.i("test","onCreate");
a = new A();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
a = null;
Log.i("test","a = null");
}
}
打印结果如下:
18:12:35.988 a = new A()
18:12:38.028 a = null
18:12:38.096 finalize A
创建一个 A 对象赋值给静态变量 a , 2000毫秒后将静态变量 a 置为空。通过日志可以看出对象 A 在静态变量 a 置空后被立即回收。所以可以认定静态变量可作为 GcRoot
方法区常量与静态变量验证过程完全一致,关于native 验证过程比较复杂,感兴趣的同学可自行验证。
验证成员变量是否可作为 GcRoot
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
A a = new A();
B b = new B();
a.b = b;
a = null;
}
class A{
B b;
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize A");
}
}
class B{
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize B");
}
}
}
打印结果如下:
13:14:58.999 finalize A
13:14:58.999 finalize B
通过日志可以看出,A、B 两个对象都被回收。虽然 B 对象被 A 对象中的 b 引用所持有,但成员变量不能被作为 GcRoot, 所以B 对象不可达,进而会被当作垃圾。
上一小结描述了 GC 机制,但具体实现还是要靠算法,下面我简单描述一下几种常见的 GC算法。
3.1. 标记清除算法
获取所有的 GcRoot 遍历内存中所有的对象,如果可以被 GcRoot 就加个标记,剩下所有的对象都将视为垃圾被清除。
- 优点:实现简单,执行效率高
- 缺点:容易产生 内存碎片(可用内存分布比较分散),如果需要申请大块连续内存可能会频繁触发 GC
3.2. 复制算法
学习福利
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
CodeChina开源项目:《Android学习PDF+架构视频+面试文档+源码笔记》
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-or553eBf-1631093550285)]
CodeChina开源项目:《Android学习PDF+架构视频+面试文档+源码笔记》
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。