JVM相关知识
- 内存结构
- 第一部分:
-
- 编译器(将.java源文件编译为.class文件)
- 类加载器
- 类加载实际上包括三部分:
- 懒加载
- 链接
- 懒初始化
- 类加载实际上包括三部分:
- 第二部分:
- 方法区(线程公有):方法区实际上是JVM的一种规范,它在HotSpot虚拟机(HSDB,Hotspot Debugger调试工具)中,在1.7之间存在于永久代中,在1.8中存在于元空间中,使用本地内存对这些信息进行存储
- 堆内存(线程公有):jvm调优的重点区域,jvm参数 -Xmx -Xms -Xmn .主要分为伊甸园、幸存区(from、to)、老年代. Minor GC、Mixed GC、Full GC
- 程序计数器(线程私有):程序计数器是唯一一个不存在内存溢出或内存泄漏的区域
- 虚拟机栈(线程私有):虚拟机栈采用了栈的思想,分为多个栈帧,每一个栈帧对应一个方法。
- 虚拟机栈中存在的内存泄漏有循环终止条件没有设计好,或者单个栈帧运行所需的空间足够大
- 本地方法栈(线程私有):本地方法栈对应本地方法接口,调用本地方法中的参数。
- 第三部分:执行引擎
- 解释器:将.class字节码文件解释为计算机运行的机器码
- 即时编译器(JIT):即时编译器会对热点代码的执行进行优化,并将优化后的存放在缓存中
- GC垃圾回收器:JVM调优的根本,Gc大大地解放了程序员自身对于内存的管理,但仍存在一些问题。
- 第四部分: 本地方法接口(操作系统、C++实现)
- 内存模型
- 内存模型实际上是jvm在多线程下和cpu之间交互的一种设计思想,主要体现为操作系统中的主内存和每个线程之间的高速缓冲区.第一次会从操作系统中读到高速缓存区中,下一次则直接从高速缓冲区中读取,但是进行写操作需要更新主内存后即时地更新(或删除)高速缓冲区的数据
- 类加载器、双亲委派机制
- 类加载器包括引导类加载器(根类加载器 / jre/lib包下)、扩展类加载器(/jre/lib/ext)、应用程序类加载器(/classpath)、自定义类加载器。
- 双亲委派模型指的是加载类时,向上抛出,逐个加载直到能够加载则停止
- 垃圾回收GC
- 3种垃圾回收算法
- 标记清除(速度快、会有内存碎片产生、现在的垃圾回收器已经弃用)
- 标记整理(速度慢、没有内存碎片产生)
- 标记复制(没有内存碎片产生、但是内存占用较大,且其中一份内存始终不存储数据)
- 3种垃圾回收器
- Parallel并行,注重吞吐量
- CMS(Concurrent Modify Sweep)垃圾回收(使用标记清除),该回收器已经被弃用,注重响应时间。
- G1垃圾回收器,同时注重了吞吐量和响应时间
- 把堆内存分为很多大小相等的内存空间(64份???),每一份都能作为伊甸园、幸存区(合并了from to)、老年代
- 3种垃圾回收算法
- 四种引用
- GC root、可达性分析、三色标记算法(标记过的、没有被标记的,正在进行分析标记的)
- 强引用:被强引用,即普通的new一个对象的引用在垃圾回收时,是不会被回收的。(包括直接饮用的其内部的成员变量)
- 软引用: 在第一次垃圾回收时,不会被回收,当再次垃圾回收的时候,就会被回收
- 弱引用: 在第一次进行垃圾回收的时候,就会被回收。
- 虚引用:在第一次进行垃圾回收的时候,就会被回收。
- 引用队列:对于软引用、弱引用和虚引用来说,其创建的中间引用对象都可以加入到引用队列中,然后开启一个单独的线程处理该引用队列。其中虚引用是必须强制使用引用队列的。
- ThreadLocal的内存泄漏和弱引用:
- ThreadLocal中的key被设计为弱引用,因此其key可以被进行垃圾回收,但是回收之后,我们就存在无法根据key去手动remove其对应的value,而value却是强引用,此时就存在内存泄漏问题。
- 其解决思路是:
- 1.在使用完毕后,及时的手动移除value
- 2.将弱引用加入到引用队列中,其中存储的是ThreadLocalMap中的Entry对象,通过将Entry对象置为null,实现key和value都被回收。
- 3.Jdk9中对于2的实现原理进行了优化和封装,使用Cleaner对象(运行时是一个守护线程)
- GC root、可达性分析、三色标记算法(标记过的、没有被标记的,正在进行分析标记的)
- finalize方法剖析
-
- 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 串行执行慢等原因综合导致
- finalize
-
- 内存溢出的几种情况
- 典型情况
- 误用线程池导致的内存溢出
- 使用了不推荐的线程池创建
- 固定线程数的线程池(Executors.newFixedThreadPool,其中的工作队列是近似没有上限的(Integer的最大值))
- 带有缓存的线程池(Executors.newCachedThreadPool,其中的核心线程数为0,全部都是救济线程(Integer的最大值)
- 单次查询数据量太大(禁止调用查询全表的数据接口)导致的内存溢出,进行分页查询(limit)
- 动态生成类导致的内存溢出(动态生成的类使用了自定义的类加载器,其中的静态成员变量是强引用,不能被垃圾回收,应该将静态变量修改为成员变量)
- 使用了不推荐的线程池创建
- 误用线程池导致的内存溢出
- 典型情况