垃圾回收
java存在内存泄露
当长生命周期的对象引用短生命周期时,尽管短生命周期的对象不被使用,但是由于长生命周期对象持有它的应用,导致它不能被gc
例子
1、使用数据库连接池时没有手动关闭(close)
2、使用流时没有手动关闭
3. 单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
4、 循环过多或死循环,产生大量对象;
发生GC的前提
- 虚拟机空闲时
- 堆内存空间不足时
JAVA垃圾回收机制:
JAVA有一个垃圾回收线程,属于低优先级,正常情况下是不会允许的,只要在虚拟机空闲时或者堆内存不足时,才会触发执行,扫描那些没有被应用的对象并将他们添加到要回收的集合里面,进行回收。垃圾回收的最大优点就是防止了内存泄露,有效使用空闲的区域。
JAVA的两种垃圾回收机制
垃圾回收算法
-
标记清除
-
复制算法
-
标记整理
-
分代收集算法
标记清除
主要是分为两个阶段
1、标记;标记需要清除的垃圾
2、清除:将标记的垃圾清除
优点:效率高,实现简单,不需要对象移动
缺点:会产生垃圾碎片,就是不连续的空间很多,导致利用率下降
复制算法
主要是将内存区域分为 AB两半,当A使用满了之后,把存活的对象复制到B里面,然后清除A
优点:实现简单,运行高效,不需要移动对象
缺点:永远只有一半没有被使用
标记整理
主要也分为两个阶段:
1、标记
2、整理
主要思想就是,将存活的对象和标记的要回收的对象压到一端,然后对端以内的对象进行清除,这样使用和未使用就会各占一边
优点:优化了标记清除的缺点,不会产生碎片空间
缺点:仍然需要移动部分对象
分代收集算法
分代算法是当前使用最广泛的垃圾回收算法。
主要将堆内存分为新生代和老年代==(前者占1/3,后者占2/3)==
垃圾回收主要发生在新生代。
新生代里面又可以分为 Eden、From(survior0)、To(survior1)(默认配比是8:1:1)
基本对象都会在Eden区进行分配。
执行流程:
-
对象在Eden区分配,第一次GC之后,存活的对象就会进入From区。
-
当From区满了之后就会把存活的对象往TO里面放,然后清空Eden和From
-
然后From和To两个交换,就是:From–>To To–>From
这时候有个判断的细节就是当From和To每次交换的时候,存活的对象年龄都会+1,到达15岁之后,将会移入老年代
大对象直接进入老年代
为什么呢?因为大对象一般指的是字符串和数组这类,需要连续的数组。
从前面我们已经知道,对象是优先在Eden区分配的,当对象无法在Eden区存放的时候会转到From区,这时候也存不进,JVM的分配担保机制就会把这个对象直接进入到老年代了,老年代是足够存放数组这些对象的,所以不会发生Full GC
长期存活的对像进入老年区:年龄达到15岁
为什么要分为新生代和老年代?
堆里面分为新生代和老年代更加有利于在两代之中选择合适的算法。例如新生代使用了标记复制算法,老年代使用标记整理或者标记清楚算法来收集。
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
怎么判断哪些对象需要回收呢?
引用计数法
给对象一个引用计数器,每当一个地方引用这个对象的时候,计数器就会+1,当引用失效的时候-1。当计数器等于0的时候,这个对象就无法再被使用了。
个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
可达性分析
可达性分析相当于一棵树,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
垃圾收集器
serial收集器
串行收集器,是单线程的,这个单线程
不仅意味着它只会使用一条垃圾收集线程去完成垃圾收集。更重要的是他在垃圾收集工作时要暂停其它所有的工作线程
新生代采用标记-清除,老年代采取标记-整理
优点:简单并且高效,没有线程交互的额外开销
缺点:需要暂停线程,给用户带来不好的体验
ParNew收集器
serial的升级版,即可以使用多线程,除了多了一个多线程之外,其它和serial的差不多。
新生代采用标记-清除算法,老年代采取标记-整理
只有它可以和CMS收集器配合工作
Parallel Scavenge收集器
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。
CMS收集器
G1收集器
Minor GC的时候其他线程是否可以继续运行?
目前所有的新生代gc都是需要STW(Stop the World)的!==
Serial:单线程STW,复制算法
ParNew:多线程并行STW,复制算法
Parallel Scavange:多线程并行STW,吞吐量优先,复制算法
G1:多线程并发,可以精确控制STW时间,整理算法
Stop the World 总会发生,GC停顿目前而言不能避免,就算是CMS也有GC停顿的时候,重点是各个垃圾回收器对GC 停顿时间的控制,可以打印一下GC日志看看minor gc花费的时间
-XX:+PrintGCDetails
Minor GC 和Full GC什么时候发生
区分一下:
Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。
Major GC/Full GC 是老年代GC,指的是发生在老年代的GC,出现Major GC一般经常会伴有Minor GC,Major GC的速度比Minor GC慢的多。
触发MinorGC
首先jvm在进行MinorGC之前会判断老年代的大小是否大于新生代的所有对象之和
- 大于;直接进行MinorGC
- 小于 判断是否开启HandlerPromotionFailure,没有开启直接FullGC
- 如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC
- 如果大于的话,执行minorGC
触发Full GC
- 如果申请了一个大对象,新生代存不下,直接进入老年代,如果也装不下,触发Full GC
- 使用System.GC
关于类加载的一些认识
JVM虚拟机只能识别字节码文件,所以在加载进JVM之前,需要对我们的.java文件进行编译
类加载过程
- 加载:主要是通过全类名获取此类定义的字节流文件,然后根据字节流代表的静态数据结构,转换为方法区的运行时数据结构。然后在堆中生成一个java.lang.Class文件,作为方法区访问这些数据结构的入口
- 验证:主要是看类有没有父类;有没有继承了不该继承的类
- 准备:给静态变量分配内存并设置初始值
- 解析:这个阶段主要是将虚拟机里面的常量池内的符号引用转换为直接引用。直接引用的意思是:指针直接指向变量的内存地址
- 初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()
方法的过程。
双亲委派机制
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
java虚拟机有多个类加载器
- 启动类加载器:顶层的加载类,由C++实现,负责加载
%JAVA_HOME%/lib
目录下的jar包和类或者或被-Xbootclasspath
参数指定的路径中的所有类。 - 扩展加载器:主要负责加载目录
%JRE_HOME%/lib/ext
目录下的jar包和类,或被java.ext.dirs
系统变量所指定的路径下的jar包。 - 应用程序加载器:面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
- 用户自定义加载器:用户自定义的加载器
什么是双亲委派机制
即在类加载的时候,会首先判断这个类是否加载过了,如果加载过了,直接返回。否则就会尝试加载,类加载器收到请求加载之后,不会立刻自己加载,而是会把这个请求委派给父类加载器,每一层的类加载器都是如此,都会委派给顶层的类加载器,只有父类加载器无法加载的时候,子加载才会尝试加载。
总结来说:当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
如果不想使用双亲委派机制怎么办?
除了BoostrapClassLoader是用C++编写之外,其它的类加载器都是继承自ClassLaoder。
就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法
怎么自定义类加载器
载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。==
如果不想使用双亲委派机制怎么办?
除了BoostrapClassLoader是用C++编写之外,其它的类加载器都是继承自ClassLaoder。
就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法
怎么自定义类加载器
java.lang.ClassLoader
。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader
。