面试——虚拟机篇(二)

目录

1、内存溢出

2、类加载

类加载过程的三个阶段

双亲委派机制

提问:

3、四种引用

1、强引用

2、软引用

3、弱引用

4、虚引用

 4、finalize

finalize 原理

finalize 缺点


1、内存溢出

  • 误用线程池导致的内存溢出

        固定大小:任务数过多,导致任务过多        

        

        带缓冲:线程数有上限,线程数耗尽了线程资源

        

  • 查询数据量太大导致的内存溢出
  • 动态生成类过多导致元空间的内存溢出

2、类加载

类加载过程的三个阶段

  • 加载
  1.  将类的字节码载入方法区,并创建类.class 对象
  2.  如果此类的父类没有加载,先加载父类
  3. 加载是懒惰执行
  • 链接
  1.  验证 – 验证类是否符合 Class 规范,合法性、安全性检查
  2. 准备 – 为 static 变量分配空间,设置默认值
  3. 解析 – 将常量池的符号引用解析为直接引用
  • 初始化
  1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 `<cinit>` 方法,在初始化时被调用
  2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
  3. 初始化是懒惰执行
类加载器:

| **名称**                | **加载哪的类**         | **说明**                       |
| ----------------------- | --------------------- | ------------------------------ |
| Bootstrap ClassLoader   | JAVA_HOME/jre/lib     | 无法直接访问                    |
| Extension ClassLoader   | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为  null  |
| Application ClassLoader | classpath             | 上级为 Extension                |
| 自定义类加载器           | 自定义                | 上级为 Application              |

双亲委派机制

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

  • 能找到这个类,由上级加载,加载后该类也对下级加载器可见
  • 找不到这个类,则下级类加载器才有资格执行加载

双亲委派的目的有两点

  1. 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
  2. 让类的加载有优先次序,保证核心类优先加载

提问:

自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。

  • 假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的
  • 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败
  • * **以上也仅仅是假设**。事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了

3、四种引用

1、强引用

  1. 普通变量赋值即为强引用,如 A a = new A();
  2. 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收

2、软引用

  1. 例如:SoftReference a = new SoftReference(new A());
  2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
  3. 软引用自身需要配合引用队列来释放
  4. 典型例子是反射数据

3、弱引用

  1. 例如:WeakReference a = new WeakReference(new A());
  2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
  3. 弱引用自身需要配合引用队列来释放
  4. 典型例子是 ThreadLocalMap 中的 Entry 对象

4、虚引用

  1. 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
  2. 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
  3. 典型例子是 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 串行执行慢等原因综合导致
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值