Java虚拟机包括一个类加载器子系统、运行时数据区、执行引擎和本地接口库。本地接口库通过调用本地方法库与操作系统交互。如图:
JVM的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆,方法区)和直接内存。
如图:
程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指示器。
虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧中存储了局部变量表、操作数栈、动态链接、方法出口等信息。同时栈帧存储处理动态链接方法的返回值和异常分派。
线程运行及栈帧变化过程,如图:
与虚拟机栈类似,区别是虚拟机栈为Java方法服务,本地方法区为Native方法服务。
又称运行时数据区,在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是垃圾收集器进行垃圾回收的最主要的内存区域。
由于现代JVM采用分代收集算法,因此Java堆也可以细分为:新生代、老年代和永久代。
方法区也称永久代,用于存储常量、静态变量、类信息、即时编译后的机器码、运行时常量池
如图:
JVM运行时内存也叫作JVM堆,从GC的角度可以将JVM堆分为新生代、老年代和永久代。
如图:
JVM新创建的对象会存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
新生代又分为Eden区、ServivorTo区和ServivorFrom区:
(1)Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般在2KB~128KB,可通过 XX:PretenureSizeThreshold设置其大小。在Eden区内存不足时会触发MinorGC,对新生代进行一次垃圾回收。
(2)ServivorTo区:保留上一次MinorGC时的幸存者。
(3)ServivorFrom区:将上一次MinorGC的幸存者作为这一次的MinorGC被扫描者。
新生代的GC过程叫做MinorGC,采用复制算法实现,具体过程如下:
(1)把Eden区和ServivorFrom区中存活的对象复制到ServivorTo区。如果某对象的年龄达到老年代的标准(由XX:MaxTenuringThreshold 设置,默认15)则将其复制到老年代,同时把这些对象的年龄加1;如果ServivorTo区的内存空间不够,则直接将其复制到老年代;如果对象是大对象,则也直接将其复制到老年代。
(2)清空Eden区和ServivorFrom区中的对象。
(3)将ServivorTo区和ServivorFrom区互换,原来的ServivorTo区成为下一次GC时的ServivorFrom区。
老年代主要存放有长生命周期的对象和大对象。老年代的GC过程叫做MajorGC。在老年代,对象比较稳定,MajorGC不会被频繁触发。在进行MajorGC之前,JVM会进行一次MinorGC,在MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续空间分配给新创建的大对象时,才会触发MajorGC。
MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,释放内存空间,所以MajorGC的耗时较长。MajorGC的标记清除算法容易产生内存碎片。
永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
在Java8中永久代已经被元数据区取代。二者最大的区别在于:元数据区没有使用虚拟机内存,而是直接使用操作系统的本地内存。
JVM类的加载分为5个阶段:加载、验证、准备、解析、初始化。在类初始化完成后就可以使用该类的信息,一个类在不需要时可以从JVM中卸载。
如图:
类加载过程主要包含将Class文件读取到运行时区域的方法区内,在堆中创建java.lang.Class对象,并封装类在方法区的数据结构的过程。
确保被加载的类的正确性。
在方法区中为类变量分配内存空间并设置类中变量的初始值。
将常量池中的符号引用替换为直接引用。
为类的静态变量赋予正确的初始值。通过执行类构造器的<client>方法为类进行初始化。
JVM规定,只有在父类的<client>方法都执行成功后,子类中的<client>方法才可以别执行。
JVM提供3种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器。
如图:
负责加载Java_HOME/lib 目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库。
负责加载Java_HOME/lib/ext 目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库。
负责加载用户路径(classpath)上的类库。
注:我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
JVM通过双亲委派机制对类进行加载。
双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给父类去完成,其父类再接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派给启动类加载器中。若父类发现自己无法加载该类,则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常。
双亲委派机制的类加载流程,如图:
双亲委派机制的核心是保障类的唯一性和安全性。例如,在加载rt.jar包中的java.lang.Object类时,无论是哪个类加载器加载这个类,最终都将类加载请求委托给启动加载器加载。
在Java中,对象的操作是通过该对象的引用(Reference)实现的。Java中的引用类型有四种:强引用、软引用、弱引用和虚引用。
(1)强引用:在Java中最常见。把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。强引用不会被垃圾回收机制回收,因此强引用也是造成Java内存泄露的主要原因。
(2)软引用:通过SoftReference类实现。如果一个对象只有软引用,则在系统内存不足时该对象将被回收。
(3)弱引用:通过WeakReference类实现。如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收。
(4)虚引用:如果一个对象持有虚引用,那么这个对象随时可能被回收。虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。
Java中常用的垃圾回收算法有标记清除、复制、标记整理和分代收集。
标记清除算法是最基础的垃圾回收算法。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会引起大对象无法获得连续可用空间的问题。
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是存在大量内存的浪费,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。因此,该算法只在对象为“朝生夕死”状态是运行效率较高。
该算法标记阶段和标记清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用不同的垃圾回收算法,称为分代收集算法。
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,分为4个步骤:
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是用户线程一起并发执行的。CMS收集器的工作过程如图所示:
G1垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域。相对于CMS垃圾收集器,G1的优点是(1)基于标记整理算法,不产生内存碎片。(2)可以精准地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收。
G1收集器的运作大致可划分为以下几个步骤:
G1收集器的工作过程如图所示:
针对大内存堆的低延迟垃圾回收算法。ZGC主要新增了两项技术,一个是着色指针(Colored Pointer),另一个是读屏障(Load Barrier)。
ZGC 是一个并发、基于区域(region)、增量式压缩的收集器。Stop-The-World 阶段只会在根对象扫描(root scanning)阶段发生,这样的话 GC 暂停时间并不会随着堆和存活对象的数量而增加。
着色指针
着色指针是一种将信息存储在指针(或使用Java术语引用)中的技术。因为在64位平台上(ZGC仅支持64位平台),指针可以处理更多的内存,因此可以使用一些位来存储状态。 ZGC将限制最大支持4Tb堆(42-bits),那么会剩下22位可用,它目前使用了4位:finalizable,remap,mark0和mark1,以标记该指向内存的存储状态。相当于在对象的指针上标注了对象的信息。
读屏障
由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。
与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。
注:“Stop-The-World”(STW):在垃圾回收过程中经常涉及到对对象的挪动,进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”(STW),导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
原文:https://www.cnblogs.com/strong-FE/p/12132314.html