jvm及垃圾回收
<h2>1 jvm组成</h2>
<p>jvm的组成
<img src="https://s2.ax1x.com/2020/03/08/3zy04s.png" alt="jvm组成" /></p>
<h3>1.1 java栈</h3>
<p>栈是存储局部变量的,也是线程独有的区域,也就是每一个线程都会有自己独立的栈区域。
java虚拟机为了区分不同方法中局部变量作用域范围的内存区域,每个方法在运行的时候都会分配一块独立的栈帧内存区域。</p>
<h4>1.1.1 栈帧</h4>
<p>栈帧内部其实不只是存放局部变量的,它还有一些别的东西,主要由四个部分组成。局部变量表、操作数栈、动态链接、方法出口。</p>
<h3>1.2 程序计数器</h3>
<p>程序计数器也是线程私有的区域,每个线程都会分配程序计数器的内存,是用来存放当前线程正在运行或者即将要运行的jvm指令码对应的地址。在线程切换时会用到。</p>
<h3>1.3 方法区</h3>
<p>在jdk1.8之前,有一个名称叫做持久带/永久代,在jdk1.8之后,oracle官方改名为元空间。存放常量、静态变量、类元信息等。</p>
<h4>1.3.1 对象组成</h4>
<p>对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构:
<img src="https://s2.ax1x.com/2020/03/08/3zywNj.png" alt="对象组成" /></p>
<ul>
<li>对象头:
<ol>
<li>第一部分markword,用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;</li>
<li>另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.</li>
<li>如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.</li>
</ol></li>
<li>实例数据:
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。</li>
<li>对象填充:
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于jvm要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。</li>
</ul>
<p>类加载其实最终是以类元信息的形式存储在方法区中的,栈、方法区、堆的关系如下图:
<img src="https://s2.ax1x.com/2020/03/08/3zyJjf.png" alt="堆、栈、方法区联系" />
math和math2都是由同一个类new出来的,当对象被new时,都会在对象头中存储一个指向类元信息的指针,这就是Klass Pointer.</p>
<h3>1.4 本地方法栈</h3>
<p>与虚拟机栈功能非常类似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地(native)方法时服务的。</p>
<h3>1.5 堆</h3>
<p>所有的对象都存储在堆区,需要进行垃圾回收。</p>
<h4>1.5.1 如何识别垃圾</h4>
<ol>
<li>
<p>引用计数法:
简单地说,就是对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收。
但是,对于两个对象互相引用这种情况,它不能识别出来是否是垃圾对象,因为引用次数至少都是1.</p>
</li>
<li>可达性分析法:
以GC Root的对象为起点,开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的都是垃圾对象。</li>
</ol>
<h4>1.5.2 垃圾回收的主要方法</h4>
<ol>
<li>
<p>标记清除法:
根据可达性算法标记出相应的可回收对象,然后进行垃圾回收。但是,这种做法会产生内存碎片。</p>
</li>
<li>
<p>标记整理法:
在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列,再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。
但是缺点也很明显:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。</p>
</li>
<li>
<p>复制回收法:
把堆等分成两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用标记法把存活的对象标记出来,然后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了。
但是,也有一个很大的缺点,有一半的内存不能使用,造成了很大的内存浪费。</p>
</li>
<li>分代收集法:
分代收集算法整合了以上算法,综合了这些算法的优点,最大程度避免了它们的缺点,所以是现代虚拟机采用的首选算法,与其说它是算法,倒不是说它是一种策略。
堆区分代如图所示:
<img src="https://s2.ax1x.com/2020/03/08/3zydEQ.png" alt="分代" />
由年轻代和老年代组成,年轻代又分为伊甸园区和survivor区,survivor区中又有from(s0)区和to(s1)区.</li>
</ol>
<p><strong>分代收集工作原理:</strong></p>
<ul>
<li>对象在新生代的分配与回收:
<ol>
<li>大部分对象在很短的时间内都会被回收,对象一般分配在 Eden 区当 Eden 区将满时,触发 Minor GC;</li>
<li>大部分对象在短时间内都会被回收, 所以经过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区(这就是为啥空间大小 Eden: S0: S1 = 8:1:1, Eden 区远大于 S0,S1 的原因),同时 GC 年龄加一(GC 年龄即发生 Minor GC 的次数),最后把 Eden 区对象全部清理以释放出空间;</li>
<li>当触发下一次 Minor GC 时,会把 Eden 区的存活对象和 S0(或S1) 中的存活对象一起移到 S1(或s0), 同时对象的年龄加1,最后清空 Eden 和 S0 的空间。</li>
<li>若再触发下一次 Minor GC,则重复上一步,只不过此时变成了 从 Eden,S1 区将存活对象复制到 S0 区,每次垃圾回收, S0, S1 角色互换;s0或s1总有一块内存为空,这里回收方法叫复制回收算法;</li>
</ol></li>
<li>对象晋升到老年代
<ol>
<li>当对象的年龄达到了我们设定的阈值(默认15),则会从S0(或S1)晋升到老年代;</li>
<li>大对象 当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代;</li>
<li>在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代;</li>
<li>s0(或s1)已满时,直接转移到老年区;</li>
</ol></li>
<li>空间分配担保
在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。</li>
<li>Stop The World
所谓的 STW, 是指在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起,会拒绝对外提供服务。
如果老年代满了,会触发 Full GC, Full GC 会同时回收新生代和老年代(即对整个堆进行GC),它会导致 Stop The World(简称 STW),造成挺大的性能开销。
现在我们应该明白把新生代设置成 Eden, S0,S1区或者给对象设置年龄阈值或者默认把新生代与老年代的空间大小设置成 1:2 都是为了尽可能地避免对象过早地进入老年代,尽可能晚地触发 Full GC</li>
</ul>
<h4>1.5.3 垃圾回收器</h4>
<p>如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 虚拟机规范并没有规定垃圾收集器应该如何实现,因此一般来说不同厂商,不同版本的虚拟机提供的垃圾收集器实现可能会有差别,一般会给出参数来让用户根据应用的特点来组合各个年代使用的收集器,主要有以下垃圾收集器</p>
<ul>
<li>在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge
<ol>
<li>Serial 收集器是工作在新生代的,单线程的垃圾收集器;</li>
<li>ParNew 收集器是 Serial 收集器的多线程版本,垃圾收集会更快,也能有效地减少 STW 的时间;</li>
<li>Parallel Scavenge 收集器也是一个使用复制算法,多线程,工作于新生代的垃圾收集器,关注的是吞吐量;</li>
</ol></li>
<li>在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old
<ol>
<li>Serial 收集器是工作于新生代的单线程收集器,与之相对地,Serial Old 是工作于老年代的单线程收集器;</li>
<li>Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,实现了「吞吐量优先」的目标;</li>
<li>CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是个很不错的选择;</li>
</ol></li>
<li>同时在新老生代工作的垃圾回收器:G1
<ol>
<li>G1 收集器是面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器.</li>
</ol></li>
</ul>