1.如何判断对象可以回收
- 引用计数法
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因是引用计数法无法解决对象的循环引用问题。
- 可达性分析法
扫描堆中的对象,看是否能够沿着 GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
GC Root包括:
- java虚拟机栈当前活动栈帧中的本地变量表中的引用的对象 (栈中当前正在使用的局部变量直接引用的对象)
- 方法区中的静态变量和常量引用的对象
- 正在使用的锁对象
GC Root对象通过引用链无法到达的对象就是垃圾对象
2.引用
A.强引用(Strong References)
我们使用的大部分引用实际上都是强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
强引用的这种特点会导致出现内存泄漏的问题,所以我们可以手动切断这些对象与强引用之间的关联(设null),让JVM可以去回收它。
public static void f1() {
List<Byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new Byte[CAP]);
System.out.println(i + "----------------------");
}
}
B.软引用(Soft References)
软引用是个对象,被软引用对象引用的(包装的)对象,即该对象被软引用
如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用,软引用可用来实现内存敏感的高速缓存
。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
//软引用
public static void f2() {
//SoftReference软引用对象 是 被强引用的,但是Byte[]是被包装在软引用对象当中的,当内存不够的时候,Byte会被GC清除,但是软引用对象不会。
List<SoftReference<Byte[]>> list = new ArrayList<>();
//引用队列--泛型是软引用对象所引用的对象---实现当软引用对象内包装的对象被回收(那么软引用对象也就失去了意义,但是由于它是强引用,所以需要手动删除,主要是因为这时候还有引用指向它,非空。),软引用对象也回收的目的。
ReferenceQueue<Byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
//当软引用对象内包装的对象被回收,就把软引用存进引用队列
SoftReference<Byte[]> s = new SoftReference<>(new Byte[CAP], queue);//关联了一个引用队列
list.add(s);
print_(list);
}
//把引用队列的内容清掉,因为他们所包装的对象已经被清除了,队列里都是没意义的软引用对象。
Reference ref = null;
while ((ref = queue.poll()) != null) {
list.remove(ref);
}
print_(list);
}
//打印
public static void print_(List<SoftReference<Byte[]>> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(i + "----" + list.get(i) + "----" + list.get(i).get());
}
}
C.弱引用(Weak References)
如果一个对象只具有弱引用。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
//弱引用
public static void f3() {
//SoftReference软引用对象 是 被强引用的,但是Byte[]是被包装在软引用对象当中的,当内存不够的时候,Byte会被GC清除,但是软引用对象不会。
List<WeakReference<Byte[]>> list = new ArrayList<>();
//引用队列--泛型是软引用对象所引用的对象---实现当软引用对象内包装的对象被回收(那么软引用对象也就失去了意义,但是由于它是强引用,所以需要手动删除,主要是因为这时候还有引用指向它,非空。),软引用对象也回收的目的。
ReferenceQueue<Byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
//当软引用对象内包装的对象被回收,就把软引用存进引用队列
WeakReference<Byte[]> s = new WeakReference<>(new Byte[CAP], queue);//关联了一个引用队列
list.add(s);
print_1(list);
if (i == 2) {
System.gc();//手动gc
}
}
//把引用队列的内容清掉,因为他们所包装的对象已经被清除了,队列里都是没意义的软引用对象。
Reference ref = null;
while ((ref = queue.poll()) != null) {
list.remove(ref);
}
print_1(list);
}
//打印
public static void print_1(List<WeakReference<Byte[]>> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(i + "----" + list.get(i) + "----" + list.get(i).get());
}
}
D.虚引用(Phantom References)
虚引用与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动
。虚引用与软引用和弱引用的一个区别在于:虚引用必须
和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
特别注意,在程序程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢(OutOfMemory)等问题的产生。
public static void f4() {
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Byte[]> phantomReference = new PhantomReference<>(new Byte[CAP], queue);
System.out.println(phantomReference.get());//null,他就是个垃圾,没必要拿到,如果拿到就违背了垃圾的意思
}
E.终结器引用(Final References)
没有强引用引用对象时,虚拟机创建终结器引用,垃圾回收时将终结器引用加入引用队列,注意此时没有进行垃圾回收,一个优先级很低的线程Finalizer查看引用队列是否有终结器引用,找到这个对象调用finallize()方法就可以回收垃圾。不推荐使用,又慢又复杂。
3.垃圾回收算法
A.标记清除
原理:将没有被GC Root直接或间接引用的对象进行标记,然后进行清除,清除时无需清理字节内容,只需要记录清除内容的起始和结束地址,放入空闲地址列表中即可。
优点:速度快
缺点:容易产生内存碎片
B.标记整理
原理:将没有被GC Root直接或间接引用的对象进行标记,然后进行整理,将被引用的对象放在一起,这时需要更新引用对象
优点:有更多的连续内存空间
缺点:速度慢
C.复制
原理:将内存空间分为为FROM和TO相等的两部分,将没有被GC Root直接或间接引用的对象进行标记,将FROM中的引用对象拷贝到TO,拷贝完成后,交换FROM和TO两个区域的名称。
优点:不会有内存碎片 , 效率较高
缺点:需要占用双倍内存空间
4.分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代(只要新生代from和伊甸园中任何一个空间不足都称作新生代空间不足。)空间不足时,触发 minor gc(新生代垃圾回收),伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from 和 to两个区域的名称
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行,当对象寿命超过阈值(15次)时,会晋升至老年代。
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,stop the world的时间更长,对老年代进行垃圾回收,具体算法为标记整理算法。
5.垃圾回收器
STW:世界停止时间,即 用户线程停止。
1)串行
- 单线程
- 堆内存较小,适合个人电脑
2) 吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
- 让单位时间内,STW 的时间最短 ,垃圾回收时间占比最低,这样就称吞吐量高
3) 响应时间优先(CMS并发标记清理算法)
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让单次 STW 的时间最短
- 初始标记需要STW,只标记GC Root对象,时间很短
- 并发标记,可以和其它用户线程并发执行
- 重新标记,需要STW,因为并发标记阶段用户线程会有新的对象引用产生,对整个内存区做标记
- 并发清理,并发清理所有垃圾,在此期间,用户线程会产生新的浮动垃圾
注意:如果产生垃圾速度快于清理速度-->full清除
4)G1
适用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 超大堆内存,会将堆划分为多个大小相等的 Region
- 整体上是 标记+整理 算法,两个区域之间是 复制 算法
4.1 Young Collection
- stop the world
4.2 Young Collection + CM
- 在 Young GC 时会进行 GC Root 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW)
4.3 Mixed Collection
会对 E、S、O 进行全面垃圾回收
- 最终标记(Remark)会 STW
- 拷贝存活(Evacuation)会 STW
总结:
- SerialGC(串行垃圾回收)
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC(并行垃圾回收,吞吐量优先)
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS(并发标记清理垃圾回收,响应时间优先)
- 新生代内存不足发生的垃圾收集 - minor gc
- 可以设置老年代内存占用阈值,进行垃圾回收
垃圾产生速度高于垃圾回收速度,发生full gc
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足:可以设置老年代内存占用阈值,进行垃圾回收
老年代占比超过阈值,触发并发标记和混合收集;
在垃圾收集过程中,回收垃圾速度大于产生垃圾的速度,处于并发垃圾收集阶段,不触发full gc;
在垃圾收集过程中,回收垃圾的速度小于产生垃圾的速度,并发收集失败,退化为串行收集,触发full gc
6.类加载(过程)
类加载包括:加载、链接和初始化三个步骤,其中链接又包括验证、准备和解析三个阶段。主要由类加载器完成。
- 加载:
将类的字节码载入方法区中,生成大的Class实例。 - 链接:
链接分为三个阶段:验证,准备,解析。
- 验证:验证类是否符合 JVM规范,安全性检查,确保被加载类的正确性。
- 准备:为
类的
静态变量分配内存【并设置默认值 0 】。
static变量分配空间和赋值是两个不同步骤,其中分配空间在准备阶段完成(即设置默认值),而赋值在初始化阶段完成。
- 如果static变量是final修饰的基本类型或者字符串被赋值字面量,那么编译阶段值就确定了,赋值在准备阶段完成。
- 如果 static 变量是 final 的,但属于引用类型(除String字面量外),那么赋值也会在初始化阶段完成。
- 解析:把类中的符号引用转换为直接引用。
- 初始化:
注意:类的初始化仅有一次:在类第一次被使用时会初始化---如果有继承关系:父类的一切在先。
静态们:类属性—在类的初始化完成
实例们:对象属性—在对象的初始化完成
类加载:把静态的初始化
静态变量们先赋默认值0,实例变量们是对象属性,不是类属性,当作看不见,视为空气。然后从头到尾按顺序走一遍,该赋值赋值,该调方法调方法,忽略非静态的方法和属性。
对象加载:把实例的初始化
实例变量们先赋默认值0,静态变量们不变(由于类加载在对象的前边,所以静态的已经初始化过了。),视为空气。然后从头到尾按顺序走一遍,该赋值赋值,该调方法调方法,忽略静态的方法和属性。注意构造方法最后执行
。
7. 类加载器
A.启动类加载器
B.扩展类加载器
C.双亲委派模式
:
一个类加载器加载类时,若存在上级类加载器,则委托给上级类加载器,若上级类加载器依然有上级,就再向上委托,直到启动类加载器,若启动类加载器能够加载则完成加载。否则,按原路线返回,返回给下级加载,若下级能加载则加载,否则再向下返回加载,直到返回起始调用加载的类加载器,若没有合适的类加载器则报异常。
注意:loadClass中包含了双亲委派机制的实现,而后面的加载是通过findClass去实现的。所以,如果我们自己实现一个类加载,而想保持双亲委派机制,就一般不重写loadClass,而去重写findClass;如果想破坏双亲委派机制,那么就重写loadClass。
为什么要设计双亲委派机制?
-
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改 避免类的重复加载:
-
当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性