JAVA虚拟机 GC(垃圾回收机制) 之 如何使JVM崩溃 .

JAVA垃圾回收机制(GC)
从字面意思来看 , 用一段接地气的话来描述 GC 呢 , 就是将程序所产生的垃圾进行回收处理也就是生活中的环卫工人 , 如果还有人问为啥要处理嘞? 这个问题问得好 ! ,那如果城市不进行垃圾处理,迟早地球就得被垃圾爆满. GC 也是同样的道理, 在JVM运行的环境中(内存)也会产生垃圾,不对垃圾进行处理的话 , 随着时间的推移 ,就会发生 内存溢出

问题就来了 , 那么什么才是所谓的垃圾呢?

咱们先来看段代码 (如下图) : 在这里插入图片描述
这是一段C语言的代码 , 创建的变量不用时得程序员手动释放 , 不然这个变量就会一直占据在内存中.这只是一小段代码,当是一个项目时,手动释放,呵.走! 不如让我去天桥下捡瓶子. 所以说这时候有个能帮我们自动释放内存的机制该多好.从这段代码咱们也可以看出一点,哪些是符合GC回收的条件的.

  • 当一个对象赋值为 Null ,或者没有引用指向这个对象时 , 那么这个对象就符合垃圾回收范围.(注意 : GC 回收的是对象所占据的内存空间而不是对象本身,这俩的区别就在于后者虽然对象不存在了,但是内存依然会占据着)

在此在解释下 内存泄漏内存溢出
内存泄漏是指一个不再被程序使用的对象或变量一直占据着内存中 , 在Java中也就是某些孤儿对象被 GC 给遗漏了,导致这块内存被永久占据(得重新启动JVM) . 而内存溢出则是内存泄漏引发内存不足,或者是其他原因导致的内存分配不出 而使JVM挂掉.


GC会在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间.

那这些 “伟大环卫工人” 是如何进行垃圾回收的?
对JVM不太了解的小伙伴建议先看下博主的上一篇JVM博客 ==>> JVM详解

GC 进化过程 ---------

如何判断是一个垃圾?

一. 引用计数法 :

采用计数的方式来判断堆中的对象是否被引用 , 比如对象被引用就+1,如果为0则是无外部引用指向该对象.这样的话就会被GC给回收.但是这样有一个大毛病.下面举个例子:

//A类
public class A{  
    public B b;
}
//B类
public class B{  
    public A a;
}
//方法
public static main(String [] args){
 	B b = new B();
 	A a = new A();
 	b.a = a;
	a.b = b;
}
/*
如果出现上面的这种情况,也就是堆中的对象相互引用的情况.
这两对象无法回收,就会导致内存泄漏.
*/

在这里插入图片描述
所以对GC进行了改进.


二 . 根搜索算法 :

首先得知道什么是根 , 这种算法是根据一个堆中的实例对象是否有根来进行回收(有就不回收,无则回收), 此对象如果有来自 方法区或者栈中的引用指向它时,就表示有根.这样就解决了堆中对象互相引用问题.[如下图所示:] 1 , 2 ,4 ,6都是有根的 , 而 3 , 5 无根.所以GC会对这样无根的对象进行回收.
在这里插入图片描述


怎么清理这些垃圾?

一 . 标记清除算法 (原始,已废弃) :

在识别为垃圾后 , 直接清除释放所在的内存空间.虽然是清除了,但是这样的方式又会导致 内存碎片化[不可用的空闲内存,这些碎片之所以不能被使用,是因为负责动态分配内存的分配算法使得这些空闲的内存无法使用,这一问题的发生,原因在于这些空闲内存以小且不连续方式出现在不同的位置] 如下图所示:

在这里插入图片描述
当然啦 “环卫工人” 也要与时俱进

二 . 分段复制算法 (升级版) :

这种算法为了解决之前存在的内存碎片问题 , 所以优化的目标是将这些碎片整理成连续的内存空间,而不是这一块那一块的分布琐碎空间.(如下图:)
     首先将一块区域分为两块区域 , 咱们先分别取为 A区与 B区 , 使用时只分配一块区域 .比如先用A区进行数据存放,当A存满时,把A中不是垃圾的对象复制到B区来(B区此时是空的),复制过来的同时给这些对象分连续的空间,也就是整理,然后在对A区进行清理.此时A区空了,B区使用.以这样的方式来清理垃圾.虽然说解决了之前的问题,但是也带来了新的毛病. 假设我分了2G内存给你,你每次就只能用一半,这样就频繁的内存回收,导致JVM会有卡顿,如果对象的存活率高的话大量的对象复制移动操作效率是慢的,性能方面打折扣.

在这里插入图片描述
继续升级~

三 . 标记–整理算法 (Mark-Compact) :

它分为两个阶段 , 先标记在整理, 首先把垃圾对象打上标记,也就是找到垃圾对象,然后将垃圾对象集中的往一个方向移动,比如说把标记好的垃圾对象全都向左移动,然后左边的存活对象就集中的往右边移动,然后以这两者的边界为界限对垃圾进行回收.

标记整理算法 解决了第一种标记清除算法 内存碎片化问题,还解决了第二种分段复制算法 内存使用率低,工作量大,的问题. 但是还是有明显的Bug. 比如在整理阶段,由于移动了可用对象,需要去更新引用 . 不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于分段复制算法。

在这里插入图片描述

总结一下上面的三种算法:
三种算法首先都需要用到 根搜索算法 找到垃圾对象 ,从下面3个方面来做一个对比:

效率:复制算法 > 标记-整理算法 > 标记-清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)
内存整齐度:复制算法 = 标记/整理算法 > 标记/清除算法
内存利用率 : 标记-整理算法 = 标记-清除算法 > 复制算法

在开启GC时,或者说GC过程开始时,它们都要暂停应用程序(stop the world),这个时候可能会有卡顿现象 , 所以说尽量避免频繁的回收垃圾.

终极版~

四 . 分代收集算法 (目前正在使用)

这并不是一种全新的算法 , 首先它将内存区分为划分为多块 , 划分为三份 :
     新生代 : 这里就主要存放一些存活周期短的对象 . [例子:某一个方法的局域变量、循环内的临时变量等等]
     老年代 : 存活有一定周期,但是还得挂的对象. [例子:缓存对象、数据库连接对象、单例对象(单例模式)等等]
     永久代 : 这类对象出生后基本不挂 , 直到程序挂它才挂. [例子:String池中的对象(享元模式)、加载过的类信息等等]
=>
说人话举例:
      比如说看一场演唱会 , 对于如何处理演唱会中垃圾问题 , 先把喜欢吃东西,或者容易扔垃圾的人群集中放在一个区域 ,结束后便于集中打扫处理. 分代也是这么个理,那它是如何判断一个人扔垃圾的概率高呢?(也就是如何判断一个对象它的生命周期) ,首先把新创建的对象放在新生代这个区中,当进行一次垃圾回收后,把存活的对象转移到老年代 ,而这种多次没被GC回收的对象则被逐渐转移到永久区,而GC就着重清理新生代区,
=>
它根据各个代的特点采用最适合的收集算法,在新生代中 , 每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记 - 清理 ”或 “ 标记 - 整理” 算法来进行回收。方法区永久代,回收方法同老年代

[如图所示 : ]
在这里插入图片描述


那了解GC机制对我们实际项目开发有什么帮助吗?
当然是有帮助的啦 , 使我们能写出更高效代码 , 健壮的系统 . 说人话翻译下就是 [ 写出的代码性能高,跑得快,不容易挂,耐抗 ]

比如下面举个例子 :

public static void main (String[] args) {

	Set<Person> set = new HashSet <Person>() ;
	Person p1 = new Person ("唐僧""pwd1",25) ;
	Person p2 = new Person ("孙悟空""pwd2",26) ;
	Person p3 = new Person ("猪八戒""pwd3",27) ;
	set.add(p1) ;
	set.add (p2) ;
	set.add (p3) ;  	//现在set里面有3个对象!
	p3.setAge(2); 		//修改p3的年龄 ,此时p3的hashcode改变
	set.remove (p3) ;       //此时remove不掉,造成内存泄漏
	set.add(p3);            //重新添加,成功,现在set里面有4个对象
}

通过上面这个例子就可以看出来 , 你打出来的代码可能非你所想 .
在解释下上面这段代码:
           首先创建了一个Set集合 , 创建了三个对象 ,把这三个对象装入Set集合中 , Set里存放的元素是无序排列的,主要通过Hash算法根据你的值来得到一个随机的位置进行存放 ,此时你修改了p3的一个属性值 , 它在把你修改后的值存进去的时候由于值不同了它Hash算出来的位置也不同. [当然以上问题已不出现在目前JDK版本中]


下面来到如何使JVM崩溃环节

手把手教你干掉健壮如牛的JVM 刺激 ,有意思 , 打代码不行 难道搞炸还不简单么 , 嘿嘿 . 下面我们就写点代码分别它的 堆区 , 方法区 , 栈 这三块地方搞点事情.

1. 堆
炸堆计划 : 为了实现JVM中堆区精准爆破,先得知道这个区放的是啥,咱们知道堆中是存放一些实例对象和变量 , 那我们可以把这块区域的内存分配小一点,然后循环创建大量对象,在把这些对象装入一个静态集合中,这样就不会被GC给回收.

 static List<Person> list = new ArrayList<Person>();
 
 public static void main (String[] args) {
   for(int i=1;i<=20000000;i++) {
     Person person = new Person(i);
     list.add(person);
   }
 }

在这里插入图片描述

当你看到这条条红色字符时 - JVM它倒下了.

继续~

2 . 栈
炸栈计划 : 既然栈里头放方法那就来个递归 , 方法一直调方法.

 int count = 0;
 public static void main (String[] args) {
   JVMTest jvmTest = new JVMTest();
   jvmTest.run();
 }
 public void run() {
   System.out.println(count++);
   run();
 }

在这里插入图片描述
恭喜JVM又一次倒下了.


希望各位在工作中少出现此类问题 , 如果出现了的话 在本机我们可以使用JDK自带的监视工具 Java VisualVM ,在jdk安装的bin目录下面可以找到.
在这里插入图片描述
在这里插入图片描述

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值 , [觉得不错的话点个赞支持下] 感谢大家的支持 ! 后面还会更新相关内容,继续用通俗易懂的文字给大家讲解. (^ _ ^) (^ _ ^) (^ _ ^)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值