目录
1、内存溢出
- 误用线程池导致的内存溢出
固定大小:任务数过多,导致任务过多
带缓冲:线程数有上限,线程数耗尽了线程资源
- 查询数据量太大导致的内存溢出
- 动态生成类过多导致元空间的内存溢出
2、类加载
类加载过程的三个阶段
- 加载
- 将类的字节码载入方法区,并创建类.class 对象
- 如果此类的父类没有加载,先加载父类
- 加载是懒惰执行
- 链接
- 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
- 准备 – 为 static 变量分配空间,设置默认值
- 解析 – 将常量池的符号引用解析为直接引用
- 初始化
- 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 `<cinit>` 方法,在初始化时被调用
- static final 修饰的基本类型变量赋值,在链接阶段就已完成
- 初始化是懒惰执行
类加载器:
| **名称** | **加载哪的类** | **说明** |
| ----------------------- | --------------------- | ------------------------------ |
| Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
| Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
| Application ClassLoader | classpath | 上级为 Extension |
| 自定义类加载器 | 自定义 | 上级为 Application |
双亲委派机制
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
- 能找到这个类,由上级加载,加载后该类也对下级加载器可见
- 找不到这个类,则下级类加载器才有资格执行加载
双亲委派的目的有两点
- 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
- 让类的加载有优先次序,保证核心类优先加载
提问:
自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。
- 假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的
- 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败
- * **以上也仅仅是假设**。事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了
3、四种引用
1、强引用
- 普通变量赋值即为强引用,如 A a = new A();
- 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
2、软引用
- 例如:SoftReference a = new SoftReference(new A());
- 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
- 软引用自身需要配合引用队列来释放
- 典型例子是反射数据
3、弱引用
- 例如:WeakReference a = new WeakReference(new A());
- 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
- 弱引用自身需要配合引用队列来释放
- 典型例子是 ThreadLocalMap 中的 Entry 对象
4、虚引用
- 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
- 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
- 典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存
4、finalize
- 一般回答:它是 Object 中的一个方法,如果子类重写它,垃圾回收时此方法会被调用,可以在其中进行资源释放和清理工作
- 较为优秀的回答:将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM,从 Java9 开始就被标注为 @Deprecated,不建议被使用了
finalize 原理
- 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
- 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中
- Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
- 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
- FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法
- 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了
finalize 缺点
- 无法保证资源释放:FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了
- 无法判断是否发生错误:执行 finalize 方法时,会吞掉任意异常(Throwable)
- 内存释放不及时:重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后,第二次 gc 时才能真正释放内存
- 有的文章提到【Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread 的优先级较普通线程更高,原因应该是 finalize 串行执行慢等原因综合导致