JVM实战1-对象创建过程以及回收过程

207 阅读12分钟

1:对象为什么会产生内存?为何要回收?

在计算机的世界里,内存可以粗略的认为就是画板,你的数据最终是要到画板上进行计算的,可是画板空间有限,数据倒是可以一直产生,结合实际,你大概也能猜到,画板快画不下了,就要开始把已经算过的或者没人用的数据都擦掉,这样才能进行新的运算。

2:JVM分为几块区域,都是干什么的

  • 基本都背过,号称五个区域,堆,java栈,本地方法栈,程序计数器,方法区。
  • 其实可以从实际出发,java中的多线程,每个线程总得知道自己执行的一些数据吧,比如执行到哪了?虽然你写的代码 只有一份,每个线程去执行的时候总得分开吧(老师出了一道题,两个人去画板上答题,总不能共用一块区域吧)。
  • 咱们就拿老师出题的例子吧,老师出的题,每个学生看到的都是一样的,这个题目,就没必要每个学生都要自己再写一遍吧?要知道黑板空间有限,jvm要做的,就是在有限的黑板,尽量多的做事。那好,我们把黑板最上方空出来一块,专门放大家都可能用得上的东西,这块就是方法区。每个学生都可以看做是一个线程,有时候你可能题没做完,要去上个厕所,你得记一下你做到第几道了,以便回来快速定位自己下一步要干啥,这个区域,叫程序计数器,要注意,每个学生做题速度不一样,所以这个程序计数器肯定是每个学生都要记,而且不让别的学生看(线程私有),那做题的时候,每个学生计算步骤,肯定也不能共享了,这块地方,叫java栈(虚拟机栈),还有一部分专门是native方法专用的,叫本地方法栈。剩下的一块,也是最大的,就是堆,这块就是大家进行验算的地方。在c语言里,每个人要自己申请空间,用完还得自己擦掉,在java里,是有个专门的人大概估算你要占用多少,验算完也不用立刻擦掉,啥时候这个地方快没空了再一起擦掉,也就是gc线程的垃圾自动回收。

3:类的加载过程

你写的.java代码,是要打包,运行在容器上,如果你去看一下打好的包,实际上你会发现你的.java代码变成了.class文件,毕竟机器也不认识你写的东西,最终你写的代码是要转化成机器能识别并运行的语言。当jvm加载到你的类,会进行验证(看一下你是不是符合规范),准备(给你的静态变量初始化,分配空间),解析,初始化(给静态变量赋值),如果此时发现有父类,会优先把父类加载。

  • 类加载器的概念:每一个类,都是要被加载到jvm中的,那java里大致有几种类加载器呢?
  • 1:Bootstrap ClassLoader核心类库加载器,专门加载/lib下的核心类库的
  • 2:extExtension ClassLoader 扩展类库加载器,专门加载/lib/ext下的扩展类库
  • 3:application ClassLoader 加载classpath环境变量下的,可以粗略认为就是你写的java代码
  • 4:自定义类加载器,自己实现。
  • 双亲委派模型:大概意思就是,加载一个类的时候,会先让自己父类加载,父类再让他的父类加载,直到无父类,然后开始逐级向下,知道碰到能加载的类加载器才会停止。 这样做的好处就是防止重复加载

4:对象回收过程

  • jvm的分代回收:分代回收算法。jvm把内存区域分为两部分,年轻代和老年代,年轻代放快进快出的对象,老年代尽量放长时间存放的对象。年轻代分为eden区和两块Survivor区(姑且称为S0和S1)。
  • 对象从eden区出生,直到enen满了,就会进行ygc,在这之前,会先判断老年代的可用连续内存是不是比年轻代大(极端情况下年轻代对象都存活,这个时候肯定要把对象放到老年代),如果老年代空间足够,就会正常ygc(年轻代gc),如果老年代不够,就看是不是开了空间担保机制(默认都是打开的),没开就直接老年代gc一下,腾点空,然后再年轻代gc,如果开了担保机制,就看历史平均进入老年代对象是不是大于目前老年代连续内存,要是小于,就正常ygc,要是大于,就会fullgc了。
  • 那如果老年代空间都足够,年轻代就会正常gc,此时如果存活对象大于Survivor区,就把这部分对象直接放老年代,否则就把存活的对象放到S0,然后把剩下的垃圾全部清除掉。等下一次又满了,就把S0和Eden区存活的对象丢到S1,把enen和S0全部清空,一直保证有一个S区是空的。如果对象年轻代放不下了,fullgc后也没有空间了,就OOM了(内存溢出)。 有图为证: 当然,除了S区放不下存活对象会进入老年代,还有其他可能,比如设置了大对象直接进入老年代,比如有的对象经过15次(可以调整)ygc还没回收掉也会进入老年代,或者到达了动态年龄限制(比如S0有100M内存,里面50M以下都是5岁以下的,那超过5岁的都会被扔到老年代),林林总总,主要就是一个目的,把那些长期存在的放老年代,尽量减少fullgc频率。 年轻代的回收一般是复制算法(从存活的放到S0或者S1也能看出来,就是把存活对象复制一份,其他全部直接清空),老年代则一般是标记整理,把存活的对象都放一起,移动一下,腾出更多空间

5:什么样的对象才是可以回收的

一般目前主流是可达性算法分析,也就是从根节点GCRoots开始找,看引用了谁,凡是对象向上追踪调用链,追不到GCRoots节点的,都叫垃圾对象,说明可以回收。 对象1-4就不能回收,其他都可以回收,一般来说,静态变量可以当GCroots节点,还有栈中的局部变量也可以.

6:几种常见垃圾收集器组合

  • 各种收集器

  • 共七种

  • Serial,ParNew,Parallel Scavenge,

  • CMS,Serial Old,Parallel Old

  • G1

  • 目前jdk1.9之前默认是Parallel Scavenge(年轻代)+Parallel Old(老年代) jdk1.9已经开始默认用G1收集器了。 不过1.8前,大多数用的都是cms+parnew组合

  • 先从正经的最原始收集器开始说。在还是单核为主的时候,收集器都是单线程处理的,Serial+Serial Old,比较简单,年轻代gc的时候先暂停用户全部线程,然后用复制算法开始清理,老年代也类似,暂停用户线程,不允许创建对象,然后开始标记-整理-清除把垃圾对象回收。思路简单,不过有stw过程(stop the word),用户体验不好,有时候回收的很慢。

  • 后来多核cpu出现后,出现并行收集器,也就是上面的多线程版本Parallel Scavenge+Parallel Old,就是在暂停用户线程的时候,多个cpu一起清理垃圾,加快速度,这套组合的特点就是吞吐量优先,也就是你可以设置他的吞吐量,大概有多久是不可用的,他会自适应调节eden和s区大小。

  • 接下来就是ParNew+cms组合,这套收集器的特点就是对响应时间有要求,其实可以这么想,10个碗要刷,10个人急等着吃饭,你是选择一次洗一个,然后尽快让一个人先吃,还是一次洗完,大家一起吃。一次洗一个,时间快,一次洗10个,效率高。换言之,优化gc时间,就是响应时间优先,减少gc次数,就是吞吐量优先。这套组合有个特别的地方,就是cms在老年代gc的时候不会完全暂停用户线程。直接上图 第一步先把GCRoots直接引用的对象标记,虽然暂停了用户线程,不过这一步很快,追踪完立刻恢复用户线程,多线程并发跟用户线程一起跑,把刚才第一步追踪的对象继续往下深追,这一步涉及的对象比较多,不过不影响用户线程,虽然有点耗cpu,紧接着再次暂停,把第二步新产生的对象和已经不再被引用的对象标记出来,这一步也很快,第四步就是并发的清除对象,这个时候有可能新进来一部分对象,叫浮动垃圾,由于此时没有暂停,新进来的对象可能大于剩余空间(毕竟清除还没结束),所以cms处理器要设置一个阈值,默认92%,给浮动垃圾留一部分空间,不过如果真的发生了还是不够的情况,cms就会变成seral old处理,不玩花里胡哨的,全部暂停,单线程处理oldGc。

  • 剩下的就是G1处理器 这个处理器跟上面的似乎有点不同。上面的好得明面上也固定了老年代和年轻代。G1则选择了新的形式Region,把内存分成了很多小块,摒弃了固定的年轻代和老年代,都可以互相转化。

  • region最多有2048个,每个的大小必须是2的倍数,如1,2,4,8等,假如你的堆内存是4096,那每个region就是2M,大小相等。此时会默认新生代占比5%,当然也可以用-XX:G1NewSizePercent指定,随着对象越来越多,新生代可以扩大至60%。也能用-XX:G1MaxNewSizePercent指定,此时会发生ygc,回收完新生代的region会减少。不过还是有enen和S区的概念,比例也是可以设置的。不过这个垃圾回收就比较特殊了,虽然也会有stw的过程,不过这个过程可以指定,这里举个例子,我看到十个碗要刷,现在可能只有三个人要吃饭,而且规定我三分钟要让这三个人吃饭,我扫了一眼这十个碗,默默标记了一下,有一个碗烂了一个坑,刷的时候小心翼翼,肯定得1分钟才能刷完,2号碗就不错,滑溜溜还没油,十秒我就能刷完,好了,不知道各位发现没有,他说三分钟要吃饭,那我肯定要选择一个最优策略,我不用把所有碗都刷了,只需要在他指定时间内刷就行,当然,他要说1秒刷完,完全没有意义(在g1里默认200ms),不过你要说一分钟,我有可能10秒就刷完了,那剩下的时间我是咋办?还是我先把中等难度的,正好一分钟能刷3个的刷了?所以,别看g1可以设置的东西少了,如果不明白原理,调优更难了。如果一个大对象,也就是超过region区50%的对象,现在就不会被分入老年代了,他属于一个特殊区域。新生代gc或者mixgc的时候会顺便把他也回收了。额,啥叫mixGC?可以类比fullgc,不过这里又有点特殊,毕竟你指定了停顿时间,所以g1还是会择优,想出一个指定时间内最大范围内回收垃圾的路径,可能同时回收年轻代,老年代和大对象。老年代达到总内存45%会触发mixgc,-XX:InitiatingHeapOccupancyPercent调节。mixgc的过程前三步跟cms比较类似,初始标记-并发标记-重新标记 不过下一步,也会暂停,执行混合回收,这个时候会计算大概需要释放多少Region,不过不会一次结束,而是会分多次执行,默认8次-XX:G1MixedGCCountTarget指定。不过如果空余region达到总内存5%,则不用再执行下面的次数了。-XX:G1HeapWastePercent可以指定剩余内存比例。不过如果单个Region有85%的存活对象(-XX:G1MixedGCLiveThresholdPercent),就不用了清除了,毕竟混合回收也是基于复制算法的,你基本所有对象都在活着,还复制个毛线..这里再举个栗子,混合回收阶段,我算好要洗8个碗就好了,正好分8次洗,这样我每次刷一个碗就能对外提供,每次都等一秒,而不是8秒不等,一次等8秒。。在我刷完3个之后,发现剩余的空碗已经占了总碗个数的5%,得,这次回收就可以结束了。所谓的回收,就是把没人吃的剩饭倒掉,有人吃的都倒一个新碗,等把剩饭倒到新碗里,之前的碗不就自然空出来了嘛。

  • 其实g1的核心原理也就是把原来一次要做的事,分成几次做,尤其是很大堆内存,半小时不用gc,一gc就停顿30秒,这不扯犊子吗。

学无止境 下次继续研究