提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
呵呵~奇怪的小知识:
现在的手机之所以变快,是因为手机的内存增加,应用程序会缓存在内存中,就不用每次都从外存读取进来了,就会觉得打开应用很快。
这些不必要的缓存会在内存空间不足时被清除,这就是软弱引用的原理。
一、System.gc()
显示的调用垃圾回收器进行一次Full GC。
这次调用会附带免责声明【有点“调不成功你别怪我”的意思。】,不一定保证调用成功【准确的说,一定会调用成功,只不过可能要等一会才会GC.。但是往往这“等一会”的时间就已经程序结束了,就感觉好像没有GC】。
通常为了保证调用成功,会让该线程沉睡一会。
System.gc(); 实际上等于 Runtime.getRuntime().gc();
实际上前者在底层调用的也是后者。
上回说到每次GC时会有概率触发finalize()方法。
而System.runFinalization()会使得finalize()得到执行,并且附带的让FullGC也能强制执行。
case:
各种场景下对象是否被GC的验证:
public class Demo1 {
public static void main(String[] args) {
method1();
}
public static void method1() {
int[] arr = new int[50 * 1024 * 1024];
System.gc();
}
public static void method2() {
int[] arr = new int[50 * 1024 * 1024];
arr = null;
System.gc();
}
public static void method3() {
{
int[] arr = new int[50 * 1024 * 1024];
}
System.gc();
}
public static void method4() {
{
int[] arr = new int[50 * 1024 * 1024];
}
int value = 0;
System.gc();
}
public static void method5() {
method1();
System.gc();
}
}
method1:
不执行GC。
因为数组的引用没有断掉。
method2:
执行GC,数组的引用断掉了。
method3:
3和4尤其注意,已经犯错。
3中,对象的作用区域在一个局部区域,看似出了局部就没有了,实则不是。
在该方法的局部变量表中还保存这这个变量的槽位【slot】,只有像4那样显示的使用新的局部变量去占用这个槽位,才能彻底抹消这个数组变量的亡灵。
因此,3的数组变量死而不僵,不会被GC。
method4:
槽位被占据,对象彻底消亡,GC。
method5:
调用method1(),method1()执行时GC不会成功,但是随着方法栈帧出栈,没有引用指向该对象,方法返回到method5()时,引用消失,GC会执行成功。
二、内存溢出及内存泄漏
2.1 内存溢出
内存溢出现在经过GC的不断优化,已经极少溢出了
在真正的OOM之前,会首先进行Full GC,竭尽全力解决这件事才会考虑溢出的事情。
因此,若真的出现溢出,往往是使用者的原因,往往不外乎两个原因:
(1)硬件不够或者设置空间不足导致JVM内存不足,承载不了大型应用;
(2)程序有问题,有内存泄漏或者有些生命本该短暂的对象被强行续命。
从这里也能看出,内存泄漏是造成内存溢出的原因。
由于JDK8之后方法区的优化,从空间狭小的永久代转化到了用户内存空间,导致方法区现在已经几乎不会发生溢出了。
前面提到,在OOM之前总会带来一次Full GC,这个说法也不是绝对的。
不会垃圾回收的场合:当对象实在太大,虚拟机认为,就算整个堆区都腾出来给你用也盛放不下,就不会执行Full GC,直接报OOM。
2.2 内存泄漏
内存泄漏有两个层面的含义:
-
严格意义:一些已经使用不到的对象,仍然有引用连向他,导致他所连的对象【因为它是用不上的,他引用的对象也是用不上的】也会被保留,这样的程序执行多次系统很有可能就崩溃了。
-
宽泛:生命周期超过他应有的长度的对象【本应该作为局部变量,被声明为全局,甚至类变量,导致在内存存活时间过长】
内存泄漏导致内存溢出
官网的示意图:
蓝色框起来的对象都属于用不上的对象,只因为箭头指向的那一个引用,导致GC无法消除这一大块的对象。
两个内存泄漏的例子:
-
单例模式引用外部对象【即不是这个单例引用应该指向的单例实例对象】
Runtime:代表运行时数据区的单例实例
本来Runtime runtime指向的应该是Runtime类实例,但是我将这个runtime引用指向了一个外部对象(杂七杂八对象),就会导致这个杂七杂八对象生命无限延长; -
该close()的资源没有回收
一般来说,只有难以回收的对象才会显示的提供close()方法,并告知程序员必须要进行回收。
若出现未回收,这样的对象本来就是重量级的,就会一直占用内存空间,就是一种内存泄漏。
三、STW
即Stop the World,指的是当GC发生时,要求全部的用户线程停止工作【其实不是全部,只要引用不发生变化就行】,等待GC完成。
一致性:要求一段时间内该快照的所有对象的引用不发生变化
类比:警察办案:
警察必须要封锁区域才能开始办案,其他人不能在这段时间进入干扰。
所有GC都有STW,但是所有的GC都在尽量缩短每次STW的时间,并保证程序高效率运行而努力。
自动进行?
case:
证明gc的stw对用户线程的影响:
/**
* 验证gc的STW对用户线程的影响
*/
public class Demo2 {
// 模拟用户线程
static class PrintThread extends Thread{
public final long start = System.currentTimeMillis();
@Override
public void run() {
try {
while (true) {
long t = System.currentTimeMillis() - start;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class GCThread extends Thread{
List<byte[]> list = new ArrayList<>();
@Override
public void run() {
while (true) {
for (int i = 0; i < 1000; i++) {
byte[] bytes = new byte[1024];
list.add(bytes);
}
if (list.size() > 10000) {
// 清空引用,调用gc
list.clear();
System.gc();
}
}
}
}
public static void main(String[] args) {
new PrintThread().start();
// new GCThread().start();
}
}
未启用GC线程的打印
大抵上是每隔一秒到1.1秒打印一次;
启动GC程序经常进行FullGC的打印。
间隔一直在上升【似乎也影响不大,可能是GC太优秀了吧】
四、垃圾回收的并行与并发
程序的并发是指同一个cpu的各个线程拥有时间片,时间片耗尽cpu快速切换,让人感觉好像几个线程同时在执行一样,实际同一个时刻只有一个线程在执行
并行(parallel)是指同一时刻真的就有几个同时执行。并行是作用在几个CPU上的,每个线程无需抢占CPU,每个都能独占一个CPU。
一个形象的比喻:
一场篮球赛就好像一个java应用,球员就是线程,篮球就是CPU。
同一场篮球赛中,看起来好像所有球员都在执行,但是同一个时刻,只有一名球员可以享有控球权。这就是并发执行。
注意:
垃圾回收的并行是说几个GC线程的并行执行【GC和用户线程是不可能并行执行的】
垃圾回收的并发,就是之前说的增量算法与分代算法那样,将GC的操作分成小片,每次执行一点,和用户程序轮流切换执行,这样虽然cpu利用率降低,且仍然会STW,但是用户线程的感受会好很多,不会一卡一卡的。
五、安全点
执行GC时要求用户线程都能停止,即STW。
但是,并不是所有的程序都能够随随便便就停止的,需要进入他能安全停止的位置才行。
这种位置就叫做安全点。
常常使用执行时间较长的指令作为安全点【中断了一会也没事】,从下面看上去都像是PC寄存器值发生突变的位置,应该是指令地址的切换需要时间比较长吧。
安全点的数量多少的设定非常有意义:
多了:跳转频繁,性能下降;
少了:GC时间长,可能OOM。
GC检查安全点的机制:
(1)抢占式:
不管所有人到没到,我先叫他们都停下,谁没到和我说一声,继续放他跑会。
看起来实现起来很困难,而且只准个别跑不是又给其他线程带来等待时间?
现在不采用了。
(2)主动式
每个进程有一个中断标识,到达安全点就分出一部分经历循环检查这个标志是否更新,若为true自己就停止了。
关于安全点的类比:
类似古代的驿站,到了驿站才休息,之前只能一直赶路。
安全区域的引入是因为处于中断或者睡眠中的线程无法走到安全点停下,GC会将一片引用不会发生变化的区域视为安全区域,处于这个区域的线程醒来时要根据当前GC有没有结束选择继不继续运行。
这节有丶抽象,需要关注的点有两处:
- 为什么要引入安全区域?
- 判断进入安全点的时机【主动中断】
六、引用分类:
需求:
期望有这样一类对象:
内存够保存在内存中,提高访问速度,内存不够就回收
jdk1.2时,提出强软弱虚四种引用。
Reference相关类都处于 java.lang.ref下。
虽然都在谈垃圾回收,但是这四种都不是真的垃圾,而是实实在在的引用。
一旦引用不在了,才是真正真正的垃圾回收。
这几种引用的回收只是内存不够的一种后手。
强:死也不回收【即使发生OOM,你也没资格回收我】【钉子户型】
内存将溢出就回收【内存不足】【有事好商量型】
GC时发现就回收【透明人型】
丝毫不妨碍GC,甚至就是为了标注这个对象的GC是否发生才设立的【为虎作伥型】
6.1 强引用
A a = new A();
A b = a;
这都是强引用。
由于强引用不可被回收,是造成内存泄漏的主要原因。
99%使用的引用都是强引用。
6.2 软引用
二次回收:先回收不可触及对象
作为高速缓存
弱引用的效果展示:
public class Demo3 {
static class User{
private String name;
private String sex;
public User(String name, String sex) {
this.name = name;
this.sex = sex;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
}
public static void main(String[] args) {
// 方式一,先声明强引用,再转化为弱引用
User user = new User("Tom", "Male");
SoftReference<User> user1 = (SoftReference<User>) new SoftReference<>(user);
user = null;
// 方式二:直接一步声名弱引用
SoftReference<User> user2 = new SoftReference<>(new User("Jerry", "Male"));
try {
int[] ints = new int[1024 * 1024 * 100000];
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
System.out.println(user1.get());
System.out.println(user2.get());
}
}
}
可见,
因为堆空间不足,这两个软引用对象被直接清除。
6.3 弱引用
弱引用是效果更弱一层的引用。
他只能在两次GC之间使用,若原对象已经没有了强引用,下次GC时无论内存是否充足都会删除这个引用所指的对象。
和软引用的区别:
软:内存不足
弱:一旦GC就回收
因此,弱引用比软引用更容易被回收
有时候因为GC的延迟执行,导致弱引用也能存活时间长,因此也可以作为缓存引用使用。
case:弱引用的生命周期
/**
* 弱引用的生命周期
*/
public class Demo4 {
public static void main(String[] args) {
Object obj = new Object();
WeakReference<Object> weakObj = new WeakReference<>(obj);
obj = null;
System.out.println(weakObj.get()); //没有GC,就还存在
System.gc();
System.out.println(weakObj.get()); //进行GC,不会存在了
}
}
执行一次GC时,必定就会被清除。
6.4 虚引用
虚引用的引用和没有没多大区别。
若一个对象只有虚引用,则就是一个无主对象,既不能使用也不能得到【get()将得到null】。
会在下次GC直接销毁。
虚引用的创建需要关联一个引用队列,方便GC进程从队列中查看哪些需要得到GC信息。
构造方法必须要传递一个引用队列
通过这个队列的对象来记录其回收信息
case:
演示弱引用的生命周期,以及如何通过队列获取回收对象。
/**
* 弱引用的使用
*/
public class Demo5 {
private static ReferenceQueue<Demo5> referenceQueue = null;
private static PhantomReference<Demo5> newDemo5 = null;
private static Demo5 demo5 = null;
/**
* 作为守护线程,时刻监视弱引用关联对象的销毁
*/
static class WeakThread extends Thread{
@Override
public void run() {
try {
Reference<? extends Demo5> remove = referenceQueue.remove();
Demo5 demo5 = remove.get();
System.out.println(demo5 + " in queue");
System.out.println(remove + " in queue");//存在对象,说明虚引用指向的对象的GC已经被记录
} catch (Exception e) {
}
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用finalize(), 爷已经复活啦");
demo5 = this; //无主对象关联静态变量
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new WeakThread();
thread.setDaemon(true); //程序刚开始执行就启动守护线程
thread.start();
demo5 = new Demo5();
referenceQueue = new ReferenceQueue<>();
newDemo5 = new PhantomReference<>(demo5, referenceQueue);
System.out.println(newDemo5.get() + " Before GC"); //虚引用不能获取原对象,即使当前还没GC也不行
demo5 = null; //让原对象具有被回收的资格
// 第一次回收
System.out.println("第一次GC开始");
System.gc();
Thread.sleep(1000); //暂停等待GC线程得到执行
System.out.println(demo5 + " first GC");//demo5直接被复活
System.out.println(newDemo5.get() + " first GC");
System.out.println("第二次GC开始");
demo5 = null;
System.gc();
Thread.sleep(1000);
System.out.println(demo5 + " second GC");
System.out.println(newDemo5.get() + " second GC");
}
}
null Before GC
第一次GC开始
调用finalize(), 爷已经复活啦
com.peng.chapter10.Demo5@312b1dae first GC
null first GC
第二次GC开始
null in queue
java.lang.ref.PhantomReference@21d71448 in queue
null second GC
null second GC
Process finished with exit code 0
守护线程的作用是用户线程全部结束,他便会结束
可见:
- 虚引用不能取得原引用对象
- finalize()保证了指向静态变量的对象可以复活。
6.5 终结器引用
不属于程序编写的引用而是由Finalizer线程自动调用的引用。
内部会附加一个引用对象,
调用时Finalizer会调用这个引用对象的finalize(),第二次GC回收该对象。