当前位置: 首页 > 后端技术 > Java

【JVM知识汇总-1】JVM内存模型

时间:2023-04-01 14:58:45 Java

【JVM知识汇总-1】JVM内存模型【JVM知识汇总-2】HotSpot虚拟机对象【JVM知识汇总-3】垃圾回收策略与算法【JVM知识汇总】-4]HotSpot垃圾收集器【JVM知识汇总-5】内存分配与回收策略【JVM知识汇总-6】JVM性能调优【JVM知识汇总-7】class文件结构【JVM知识汇总-8】classLoadingTiming【JVM【知识总结-9】类加载过程【JVM知识总结-10】类加载器JVM内存结构图程序计数器(PCRegister)定义程序计数器是一个小的内存空间(逻辑),是当前字节码指令所在的地址线程正在执行。如果当前线程正在执行na??tive方法,那么此时程序计数器是Undefined。功能字节码解释器通过改变程序计数器顺序读取指令,从而实现代码的流程控制。在多线程的情况下,程序计数器会记录当前线程的执行位置,这样当线程切换回来的时候,就知道上次线程执行到哪里了。它的特点是内存空间较小。线程私有,每个线程都有自己的程序计数器。生命周期:随着线程的创建而创建,随着线程的销毁而销毁。是唯一不会发生OutOfMemoryError的内存区域。Java虚拟机栈(Javastack)定义Java虚拟机栈是描述Java方法运行过程的内存模型。Java虚拟机为每一个即将运行的Java方法创建一个称为“栈帧”的区域,用于存储方法运行过程中的一些信息,如:局部变量表操作数栈动态链接方法退出信息pushstackpop当进程执行一个Java方法时,首先创建一个栈帧,将栈帧压入栈顶,然后将程序计数器执行指向这个栈帧。堆栈顶部的堆栈帧是活动堆栈帧。操作数栈只能使用这个活动栈帧的局部变量。当在这个栈帧中调用另一个方法时,会再次创建相应的栈帧,新创建的栈帧被压入栈顶,成为当前活跃的栈帧。当方法运行过程中需要创建局部变量时,局部变量的值被存储在栈帧中的局部变量表中。方法结束后,当前栈帧被移除,栈帧的返回值在新的活动栈帧的操作数栈中编入一个操作数。如果没有返回值,则新活动堆栈帧中操作数堆栈上的操作数不变。由于Java虚拟机栈对应线程,数据不被线程共享(即线程私有),所以不需要关心数据的一致性,也不会有同步锁的问题。局部变量表定义为一个数值数组,主要用于存放方法参数和定义在方法体内的局部变量。数据类型包括各种基本数据类型、对象引用和返回地址类型。局部变量表容量的大小是在编译时确定的。最基本的存储灵药是slot,32位类型占一个slot,64位类型(long和double)占两个slot。【解释】slotJVM虚拟机为局部变量表中的每个slot分配了一个访问索引,通过该索引可以成功访问局部变量表中指定的局部变量值。如果当前栈帧是由构造函数或实例方法创建的,那么对象引用this会被存放在索引为0的槽中,其余的参数列表会继续按顺序排列。栈帧中局部变量表中的槽位可以重复。如果一个局部变量已经超过了它的作用域,那么在它的作用域之后声明的新局部变量可能会重用过期局部变量的槽,从而达到节约资源的目的。操作数栈顶缓存技术:由于操作数存放在内存中,频繁的内存读写操作会影响执行速度,将栈顶元素全部缓存在物理CPU的寄存器中,减少读取次数和写入内存,提高执行引擎的执行效率。每个操作数栈都会有一个明确的栈深度用于存储值,最大深度是在编译时定义的。32bit类型占用一个栈单元深度,64bit类型占用两个栈深度操作数栈。它没有使用访问索引方法进行数据访问,而是通过标准的push和pop操作仅偏转一次数据访问。方法调用的静态链接:当一个字节码文件被加载到JVM中时,如果被调用的目标方法在编译时已知并且在运行时保持不变,此时调用者的符号引用被转换为直接引用的过程称为静态链接。动态链接:如果在编译时无法确定被调用方法,只能在运行时将被调用方法符号引用转换为直接引用。这种引用转换过程是动态的,所以称为动态链接。比如一个被子类重写的方法,只有在执行时才能确定是父类的方法还是子类的方法。方法绑定早期绑定:被调用的目标方法在编译时已知,运行时保持不变后期绑定:被调用的方法在编译时无法确定,只能在程序运行时根据实际类型绑定相关方法。非虚方法:如果方法的具体调用版本是在编译时确定的,那么这个版本在运行时是不可变的。此类方法称为非虚拟方法。静态方法、私有方法、final方法、实例构造函数和父方法都是非虚方法,除了这些是虚方法。虚方法表:在面向对象编程中,经常使用动态分配。如果每个动态分配过程都要在类的方法元数据中寻找合适的目标,可能会影响执行效率。因此JVM为了提高性能,在类的方法区采用虚方法表,使用索引表代替查找。每个类都有一个虚方法表,里面存放了每个方法的计时入口。虚方法表将在类加载的链接阶段创建和初始化。准备好类的变量初始值后,JVM还会初始化类的方法。方法重写的本质是找到操作数栈顶的第一个元素执行的对象的实际类型,记为C。如果在Type中找到了同时匹配描述符和常量池中的简单名称的方法C、执行访问权限检查。如果通过,则返回该方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。否则,按照继承关系从下到上依次对C的各个父类进行上一步的查找验证过程。如果找不到合适的方法,则抛出java.lang.AbstractMethodError异常。Java中的任何普通方法都具有虚函数的特性(运行时确认,具有后期绑定的特性),在C++中使用关键字virtual显式定义。如果不想让一个方法要求Java程序中函数的特性,可以使用关键字final来标记这个方法。Java虚拟机栈的特点是运行速度极快,仅次于PC寄存器的局部变量表是随着栈帧的创建而创建的,其大小在编译时就确定了,只需要预定的大小创建时分配。局部变量表的大小在方法运行时不会改变。Java虚拟机栈中会出现两个异常:StackOverFlowError和OutOfMemoryError。StackOverFlowError如果Java虚拟机栈的大小不允许动态扩展,那么当线程请求栈深度超过当前Java虚拟机栈的最大深度时,抛出StackOverFlowError异常。如果OutOfMemoryError允许动态扩展,那么当线程请求栈时内存用完,就不能动态扩展,抛出OutOfMemoryError异常。Java虚拟机栈也是线程私有的,线程创建时创建,线程结束时销毁。当发生StackOverFlowError时,可能还有很多内存空间。本地方法栈(C栈)定义本地方法栈是为JVM运行本地方法准备的空间。由于很多native方法都是用C语言实现的,所以通常称为C栈。它类似于Java虚拟机栈实现的功能,只不过本地方法栈是一种描述本地方法运行过程的内存模型。栈帧变化过程当本地方法执行时,也会在本地方法栈上创建一个栈帧,用于存放方法的局部变量、操作数栈、动态链接、方法退出信息等。方法执行后,相应的栈帧也会被弹出,内存空间也会被释放。还会抛出StackOverFlowError和OutOfMemoryError异常。如果Java虚拟机本身不支持native方法,或者不依赖传统栈本身,那么native方法栈可能不提供。如果支持本地方法栈,一般在线程创建的时候就分配栈。堆定义堆是用来存放对象的内存空间,几乎所有的对象都存放在堆中。特点线程共享,整个Java虚拟机只有一个堆,所有线程访问同一个堆。程序计数器、Java虚拟机栈、本地方法栈都对应一个线程。在虚拟机启动时创建。它是垃圾收集的主要场所。堆可以分为新生代(Eden区:FromSurvivor,ToSurvivor)和老年代。Java虚拟机规范规定堆可以处理物理上不连续的内存空间,但逻辑上应该认为是连续的。关于Survivors0和s1区域:复制后有exchange,谁空谁给。不同生命周期的对象存放在不同的区域,这样可以根据不同的区域使用不同的垃圾回收算法,更有针对性。堆的大小可以固定也可以扩展,但是对于主流的虚拟机来说,堆的大小是可伸缩的,所以当一个线程请求分配内存,但是堆已满,无法扩展内存时,会抛出OutOfMemoryError异常.Java堆使用的内存不需要是连续的。由于堆是所有线程共享的,所以访问时需要注意同步问题,方法和对应的属性需要保持一致。新生代和老年代的生命周期比新生代长。新生代和老年代的空间比例默认是1:2。JVM参数XX:NewRatio=2表示新生代占1,老年代占2,新生代占整个堆。的1/3。在Hotspot中,Eden空间与另外两个Survivor空间的默认比例是8:1:1。Eden区几乎所有的Java对象都是新释放的,Eden区容纳不下的大对象会直接进入老年代。在对象分配过程中,新的对象首先被放入伊甸园区域,并且大小是有限的。如果创建新对象时Eden区满了,会触发MinorGC销毁Eden中不再被其他对象引用的对象,再加载新的对象。Eden区的对象放在Eden区。需要注意的是,如果Survivor区满了不会触发MinorGC,但是Survivor区会在Eden空间满的时候被MinorGC清理掉。将Eden中剩余的对象移动到Survivor区,再次触发垃圾回收。这时候上次从Survivor出来的对象就放在了Survivor0区域。如果他们没有被回收,他们将在Survivor1区域接收。再次经过垃圾回收后,Survivor会被放回Survivor0区,以此类推。默认值为15个周期。如果超过15次,Survivor会被转移到老年区。JVM参数-XX:MaxTenuringThreshold=15设置的比较频繁,新生代手机很少在老年代收集,在永久区和元空间几乎不收集。FullGC/MajorGC的触发条件说明调用System.gc()、老年代空间不足、方法区空间不足都会触发FullGC。同时回收新生代和老年代。FullGC的STW时间最长,应该避免。.在MajorGC发生之前,MinorGC会先被触发。如果老年代仍然没有足够的空间,就会触发MajorGC。STW时间比MinorGC长。逃逸分析Scalarreplacementscalar是不可分解的量。java的基本数据类型是标量。标量的反义词是可以进一步分解的量,这种量称为聚合量。在Java中,对象是可以进一步分解的集合。替换过程,通过逃逸分析判断对象是否会被外部访问,当对象可以进一步分解时,JVM不会创建对象,而是将对象的成员变量分解为本方法使用的若干成员变量代替。这些被替换的成员变量在栈帧或寄存器上分配空间。对象和数组并不都分配在堆上。随着JIT编译的发展和逃逸分析技术的逐渐成熟,栈上的分配和标量替换优化技术会带来一些变化。所有对象都分配在堆上。渐渐地,它变得不那么绝对了。这是一种能够有效降低Java内存堆分配压力的分析算法。通过逃逸分析,JavaHotspot编译器可以分析一个新对象引用的使用范围,从而决定是否将这个对象分配到堆上。当一个对象定义在方法中时,它可能被外部方法引用。如果作为调用参数传递到其他地方,则称为方法逃逸。再比如给类变量或实例变量赋值,可以在其他线程中使用,这称为线程逃逸。使用逃逸分析,编译器可以优化代码如下:同步遗漏:如果发现一个对象只能从一个线程访问,那么这个对象的操作不需要考虑同步。将堆分配转换为堆栈分配:如果对象是在子例程中分配的,则如果指向该对象的指针永远不会逃逸,则该对象可能是堆栈分配而不是堆分配的候选对象。对象分离或标量替换:有些对象可能不需要作为一个连续的内存结构来访问,因此部分或全部对象可能不会存储在内存中,而是存储在CPU寄存器中。publicstaticStringBuffercreateStringBuffer(Strings1,Strings2){StringBuffers=newStringBuffer();s.append(s1);s.append(s2);returns;}s是一个方法内部变量,在上面的方块代码中直接Returnings,这个StringBuffer对象可能会被其他方法修改,导致它的作用域不只在方法内部,虽然是局部变量,但还是逃逸了在方法之外,称为方法逃逸。也有可能被外部访问,比如赋值给类的变量或者实例变量可以在其他线程中访问,这就叫做线程逃逸。在编译过程中,如果JIT进行转义分析,发现有些对象没有转义方法,那么就有可能堆内存分配优化为栈内存分配。jvm参数-XX:+DoEscapeAnalysis开启逃逸分析,-XX:-DoEscapeAnalysis关闭逃逸分析。从jdk1.7开始就默认开启了逃逸分析。TLABTLAB的全称是ThreadLocalAllocationBuffer,即线程局部分配缓冲区,属于伊甸区。这是线程专有的内存分配区,线程私有,默认开启(当然也不是绝对的,也要看是哪种类型的虚拟机)。堆是全局共享的。同时可能有多个线程在堆上申请空间,但是每次对象分配都需要同步进行(虚拟机使用失败重试的CAS来保证更新操作的原子性。)但是效率下降了一点.所以使用TLAB来避免多线程冲突。在为对象分配内存时,每个线程使用自己的TLAB,可以同步线程,提高对象分配效率。当然,并不是所有的对象都能在TLAB中成功分配内存。如果失败,将使用锁定机制来维护操作的原子性。JVM调优参数-XX:+UseTLAB使用TLAB,-XX:+TLABSize设置TLAB的大小。四种引用方式强引用:创建一个对象,并将这个对象赋值给一个引用变量。普通new对象的变量引用都是强引用。当有引用变量指向它时,它永远不会被垃圾回收。即使jvm抛出OOM,您可以为null分配一个引用,它指向的对象将被垃圾回收。软引用:如果一个对象存在软引用,并且内存空间足够,垃圾回收期是不会回收他的。如果内存空间不足,就会回收这些对象的内存。只要垃圾收集器不收集它,该对象就可以被程序使用。将对象交给SoftReference,成为软引用。弱引用:非必要的对象。JVM在进行垃圾回收时,无论内存是否充足,弱引用关联的对象都会被回收。将对象交给WeakReference成为弱引用。幻影引用:幻影引用不决定对象的生命周期。如果一个对象只持有一个虚引用,它随时可能被垃圾回收器回收,就像没有引用一样。将对象传递给PhantomReference以成为虚拟引用。方法区方法的定义方法区的定义在Java虚拟机规范中是正确的逻辑部分。方法区存储了以下信息:虚拟机加载的类的信息常量、静态变量、实时编译器编译的代码、线程共享的方法区的特性。方法区是堆的逻辑部分,所以和堆一样,也是线程共享的。整个虚拟机只有一个方法区。永久一代。方法区的信息一般需要长期存在,是堆的一个逻辑分区。因此,方法区使用划分堆的方法被称为“永久代”。内存回收效率低下。方法区中的信息一般需要长期存在,只有少量信息在回收后可能会失效。主要回收目标是:常量池的回收;类的卸载。Java虚拟机规范对方法区的要求比较宽松。和堆一样,允许固定大小,允许动态扩展,不允许垃圾回收。运行时常量池方法区存放:类信息,常量,静态变量,以及即时编译器编译出来的代码。常量存储在运行时常量池中。类被虚拟机加载后,.class文件中的常量存放在方法区的运行时常量池中。并且在运行时,可以将新的常量添加到常量池中。进入String类的intern()方法,在运行时将字符串常量添加到常量池中。直接内存(堆外内存)直接内存是Java虚拟机以外的内存,但也可以被Java使用。操作直接内存在NIO中引入了基于通道和缓冲的IO方法。它可以通过调用本地方法直接在Java虚拟机外部分配内存,然后通过存储在堆中的DirectByteBuffer对象直接操作内存,而无需在操作前先将外部内存中的数据复制到堆中,从而提高改进数据操作的效率。directmemory的大小不受Java虚拟机控制,但既然是内存,当内存不足时会抛出OutOfMemoryError异常。与直接内存和堆内存相比,直接内存申请空间消耗更多的性能。直接内存读IO性能优于普通堆内存。直接内存动作链:本地IO>直接内存>本地IO堆内存动作链:本地IO>直接内存>非直接内存>直接内存>本地IO参考自:https://doocs.gitee.io/jvm/01-jvm-memory-structure.html#%E6%96%B9%E6%B3%95%E7%9A%84%E8%B0%83%E7%94%A8