私人网站服务器免费,宁波知名seo关键词优化,品牌设计主要做哪些内容,能从源代码黑进网站修改数据吗点击蓝色“Java中文社群”关注我哟加个“星标”#xff0c;一起成长#xff0c;做牛逼闪闪的技术人JVM ≠ Japanese Videos Man写这篇的主要原因呢#xff0c;就是为了能在简历上写个“熟悉JVM底层结构”#xff0c;另一个原因就是能让读我文章的大家也写上这句话#xf… 点击蓝色“Java中文社群”关注我哟加个“星标”一起成长做牛逼闪闪的技术人JVM ≠ Japanese Videos Man写这篇的主要原因呢就是为了能在简历上写个“熟悉JVM底层结构”另一个原因就是能让读我文章的大家也写上这句话真是个助人为乐的帅小伙。。。。嗯不单单只是面向面试学习哈更重要的是构建自己的JVM 知识体系Javaer 们技术栈要有广度但是 JVM 的掌握必须有深度点赞收藏 就学会系列文章收录在 GitHub JavaKeeper N线互联网开发必备技能兵器谱直击面试反正我是带着这些问题往下读的说一下 JVM 运行时数据区吧都有哪些区分别是干什么的Java 8 的内存分代改进举例栈溢出的情况调整栈大小就能保存不出现溢出吗分配的栈内存越大越好吗垃圾回收是否会涉及到虚拟机栈方法中定义的局部变量是否线程安全运行时数据区内存是非常重要的系统资源是硬盘和 CPU 的中间仓库及桥梁承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略保证了 JVM 的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。下图是 JVM 整体架构中间部分就是 Java 虚拟机定义的各种运行时数据区域。jvm-frameworkJava 虚拟机定义了若干种程序运行期间会使用到的运行时数据区其中有一些会随着虚拟机启动而创建随着虚拟机退出而销毁。另外一些则是与线程一一对应的这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。线程私有程序计数器、栈、本地栈线程共享堆、堆外内存永久代或元空间、代码缓存下面我们就来一一解毒下这些内存区域先从最简单的入手一、程序计数器程序计数寄存器Program Counter RegisterRegister 的命名源于 CPU 的寄存器寄存器存储指令相关的线程信息CPU 只有把数据装载到寄存器才能够运行。这里并非是广义上所指的物理寄存器叫程序计数器或PC计数器或指令计数器会更加贴切并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。程序计数器是一块较小的内存空间可以看作是当前线程所执行的字节码的行号指示器。1.1 作用PC 寄存器用来存储指向下一条指令的地址即将要执行的指令代码。由执行引擎读取下一条指令。jvm-pc-counter分析进入class文件所在目录执行javap -v xx.class反解析或者通过IDEA插件Jclasslib直接查看上图可以看到当前类对应的Code区汇编指令、本地变量表、异常表和代码行偏移量映射表、常量池等信息。1.2 概述它是一块很小的内存空间几乎可以忽略不计。也是运行速度最快的存储区域在 JVM 规范中每个线程都有它自己的程序计数器是线程私有的生命周期与线程的生命周期一致任何时间一个线程都只有一个方法在执行也就是所谓的当前方法。如果当前线程正在执行的是 Java 方法程序计数器记录的是 JVM 字节码指令地址如果是执行 natice 方法则是未指定值undefined它是程序控制流的指示器分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域????????使用PC寄存器存储字节码指令地址有什么用呢为什么使用PC寄存器记录当前线程的执行地址呢????♂️因为CPU需要不停的切换各个线程这时候切换回来以后就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。????????PC寄存器为什么会被设定为线程私有的????♂️多线程在一个特定的时间段内只会执行其中某一个线程方法CPU会不停的做任务切换这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址所以为每个线程都分配了一个PC寄存器每个线程都独立计算不会互相影响。二、虚拟机栈2.1 概述Java 虚拟机栈(Java Virtual Machine Stacks)早期也叫 Java 栈。每个线程在创建的时候都会创建一个虚拟机栈其内部保存一个个的栈帧(Stack Frame对应着一次次 Java 方法调用是线程私有的生命周期和线程一致。作用主管 Java 程序的运行它保存方法的局部变量、部分结果并参与方法的调用和返回。特点栈是一种快速有效的分配存储方式访问速度仅次于程序计数器JVM 直接对虚拟机栈的操作只有两个每个方法执行伴随着入栈进栈/压栈方法执行结束出栈栈不存在垃圾回收问题栈中可能出现的异常Java 虚拟机规范允许 Java虚拟机栈的大小是动态的或者是固定不变的如果采用固定大小的 Java 虚拟机栈那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量Java 虚拟机将会抛出一个 StackOverflowError 异常如果 Java 虚拟机栈可以动态扩展并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈那 Java 虚拟机将会抛出一个OutOfMemoryError异常可以通过参数-Xss来设置线程的最大栈空间栈的大小直接决定了函数调用的最大可达深度。官方提供的参考工具可查一些参数和操作https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BGBCIEFC2.2 栈的存储单位栈中存储什么每个线程都有自己的栈栈中的数据都是以栈帧Stack Frame的格式存在在这个线程上正在执行的每个方法都各自有对应的一个栈帧栈帧是一个内存区块是一个数据集维系着方法执行过程中的各种数据信息2.3 栈运行原理JVM 直接对 Java 栈的操作只有两个对栈帧的压栈和出栈遵循“先进后出/后进先出”原则在一条活动线程中一个时间点上只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧栈顶栈帧是有效的这个栈帧被称为当前栈帧Current Frame与当前栈帧对应的方法就是当前方法Current Method定义这个方法的类就是当前类Current Class执行引擎运行的所有字节码指令只针对当前栈帧进行操作如果在该方法中调用了其他方法对应的新的栈帧会被创建出来放在栈的顶端称为新的当前栈帧不同线程中所包含的栈帧是不允许存在相互引用的即不可能在一个栈帧中引用另外一个线程的栈帧如果当前方法调用了其他方法方法返回之际当前栈帧会传回此方法的执行结果给前一个栈帧接着虚拟机会丢弃当前栈帧使得前一个栈帧重新成为当前栈帧Java 方法有两种返回函数的方式一种是正常的函数返回使用 return 指令另一种是抛出异常不管用哪种方式都会导致栈帧被弹出IDEA 在 debug 时候可以在 debug 窗口看到 Frames 中各种方法的压栈和出栈情况2.4 栈帧的内部结构每个**栈帧Stack Frame**中存储着局部变量表Local Variables操作数栈Operand Stack(或称为表达式栈)动态链接Dynamic Linking指向运行时常量池的方法引用方法返回地址Return Address方法正常退出或异常退出的地址一些附加信息jvm-stack-frame继续深抛栈帧中的五部分~~2.4.1. 局部变量表局部变量表也被称为局部变量数组或者本地变量表是一组变量值存储空间主要用于存储方法参数和定义在方法体内的局部变量包括编译器可知的各种 Java 虚拟机基本数据类型boolean、byte、char、short、int、float、long、double、对象引用reference类型它并不等同于对象本身可能是一个指向对象起始地址的引用指针也可能是指向一个代表对象的句柄或其他与此相关的位置和 returnAddress 类型指向了一条字节码指令的地址已被异常表取代由于局部变量表是建立在线程的栈上是线程的私有数据因此不存在数据安全问题局部变量表所需要的容量大小是编译期确定下来的并保存在方法的 Code 属性的maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的方法嵌套调用的次数由栈的大小决定。一般来说栈越大方法嵌套调用次数越多。对一个函数而言它的参数和局部变量越多使得局部变量表膨胀它的栈帧就越大以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间导致其嵌套调用次数就会减少。局部变量表中的变量只在当前方法调用中有效。在方法执行时虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后随着方法栈帧的销毁局部变量表也会随之销毁。参数值的存放总是在局部变量数组的 index0 开始到数组长度 -1 的索引结束槽 Slot局部变量表最基本的存储单元是Slot变量槽在局部变量表中32位以内的类型只占用一个Slot(包括returnAddress类型)64位的类型long和double占用两个连续的 Slotbyte、short、char 在存储前被转换为intboolean也被转换为int0 表示 false非 0 表示 truelong 和 double 则占据两个 SlotJVM 会为局部变量表中的每一个 Slot 都分配一个访问索引通过这个索引即可成功访问到局部变量表中指定的局部变量值索引值的范围从 0 开始到局部变量表最大的 Slot 数量当一个实例方法被调用的时候它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 Slot 上如果需要访问局部变量表中一个64bit的局部变量值时只需要使用前一个索引即可。比如访问 long 或double 类型变量不允许采用任何方式单独访问其中的某一个 Slot如果当前帧是由构造方法或实例方法创建的那么该对象引用 this 将会存放在 index 为 0 的 Slot 处其余的参数按照参数表顺序继续排列这里就引出一个问题静态方法中为什么不可以引用 this就是因为this 变量不存在于当前方法的局部变量表中栈帧中的局部变量表中的槽位是可以重用的如果一个局部变量过了其作用域那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位从而达到节省资源的目的。下图中this、a、b、c 理论上应该有 4 个变量c 复用了 b 的槽在栈帧中与性能调优关系最为密切的就是局部变量表。在方法执行时虚拟机使用局部变量表完成方法的传递局部变量表中的变量也是重要的垃圾回收根节点只要被局部变量表中直接或间接引用的对象都不会被回收2.4.2. 操作数栈每个独立的栈帧中除了包含局部变量表之外还包含一个后进先出Last-In-First-Out的操作数栈也可以称为表达式栈Expression Stack操作数栈在方法执行过程中根据字节码指令往操作数栈中写入数据或提取数据即入栈push、出栈pop某些字节码指令将值压入操作数栈其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如执行复制、交换、求和等操作概述操作数栈主要用于保存计算过程的中间结果同时作为计算过程中变量临时的存储空间操作数栈就是 JVM 执行引擎的一个工作区当一个方法刚开始执行的时候一个新的栈帧也会随之被创建出来此时这个方法的操作数栈是空的每一个操作数栈都会拥有一个明确的栈深度用于存储数值其所需的最大深度在编译期就定义好了保存在方法的 Code 属性的 max_stack 数据项中栈中的任何一个元素都可以是任意的 Java 数据类型32bit 的类型占用一个栈单位深度64bit 的类型占用两个栈单位深度操作数栈并非采用访问索引的方式来进行数据访问的而是只能通过标准的入栈和出栈操作来完成一次数据访问如果被调用的方法带有返回值的话其返回值将会被压入当前栈帧的操作数栈中并更新PC寄存器中下一条需要执行的字节码指令操作数栈中元素的数据类型必须与字节码指令的序列严格匹配这由编译器在编译期间进行验证同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证另外我们说Java虚拟机的解释引擎是基于栈的执行引擎其中的栈指的就是操作数栈栈顶缓存Top-of-stack-CashingHotSpot 的执行引擎采用的并非是基于寄存器的架构但这并不代表 HotSpot VM 的实现并没有间接利用到寄存器资源。寄存器是物理 CPU 中的组成部分之一它同时也是 CPU 中非常重要的高速存储资源。一般来说寄存器的读/写速度非常迅速甚至可以比内存的读/写速度快上几十倍不止不过寄存器资源却非常有限不同平台下的CPU 寄存器数量是不同和不规律的。寄存器主要用于缓存本地机器指令、数值和下一条需要被执行的指令地址等数据。基于栈式架构的虚拟机所使用的零地址指令更加紧凑但完成一项操作的时候必然需要使用更多的入栈和出栈指令这同时也就意味着将需要更多的指令分派instruction dispatch次数和内存读/写次数。由于操作数是存储在内存中的因此频繁的执行内存读/写操作必然会影响执行速度。为了解决这个问题HotSpot JVM设计者们提出了栈顶缓存技术将栈顶元素全部缓存在物理 CPU 的寄存器中以此降低对内存的读/写次数提升执行引擎的执行效率2.4.3. 动态链接指向运行时常量池的方法引用每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。在 Java 源文件被编译到字节码文件中时所有的变量和方法引用都作为符号引用Symbolic Reference保存在 Class 文件的常量池中。比如描述一个方法调用了另外的其他方法时就是通过常量池中指向方法的符号引用来表示的那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用jvm-dynamic-linkingJVM 是如何执行方法调用的方法调用不同于方法执行方法调用阶段的唯一任务就是确定被调用方法的版本即调用哪一个方法暂时还不涉及方法内部的具体运行过程。Class 文件的编译过程中不包括传统编译器中的连接步骤一切方法调用在 Class文件里面存储的都是符号引用而不是方法在实际运行时内存布局中的入口地址直接引用。也就是需要在类加载阶段甚至到运行期才能确定目标方法的直接引用。【这一块内容除了方法调用还包括解析、分派静态分派、动态分派、单分派与多分派这里先不介绍后续再挖】在 JVM 中将符号引用转换为调用方法的直接引用与方法的绑定机制有关静态链接当一个字节码文件被装载进 JVM 内部时如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接动态链接如果被调用的方法在编译期无法被确定下来也就是说只能在程序运行期将调用方法的符号引用转换为直接引用由于这种引用转换过程具备动态性因此也就被称之为动态链接对应的方法的绑定机制为早期绑定Early Binding和晚期绑定Late Binding。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程这仅仅发生一次。早期绑定早期绑定就是指被调用的目标方法如果在编译期可知且运行期保持不变时即可将这个方法与所属的类型进行绑定这样一来由于明确了被调用的目标方法究竟是哪一个因此也就可以使用静态链接的方式将符号引用转换为直接引用。晚期绑定如果被调用的方法在编译器无法被确定下来只能够在程序运行期根据实际的类型绑定相关的方法这种绑定方式就被称为晚期绑定。虚方法和非虚方法如果方法在编译器就确定了具体的调用版本这个版本在运行时是不可变的。这样的方法称为非虚方法比如静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法其他方法称为虚方法虚方法表在面向对象编程中会频繁的使用到动态分派如果每次动态分派都要重新在类的方法元数据中搜索合适的目标有可能会影响到执行效率。为了提高性能JVM 采用在类的方法区建立一个虚方法表virtual method table使用索引表来代替查找。非虚方法不会出现在表中。每个类中都有一个虚方法表表中存放着各个方法的实际入口。虚方法表会在类加载的连接阶段被创建并开始初始化类的变量初始值准备完成之后JVM 会把该类的方法表也初始化完毕。2.4.4. 方法返回地址return address用来存放调用该方法的 PC 寄存器的值。一个方法的结束有两种方式正常执行完成出现未处理的异常非正常退出无论通过哪种方式退出在方法退出后都返回到该方法被调用的位置。方法正常退出时调用者的 PC 计数器的值作为返回地址即调用该方法的指令的下一条指令的地址。而通过异常退出的返回地址是要通过异常表来确定的栈帧中一般不会保存这部分信息。当一个方法开始执行后只有两种方式可以退出这个方法执行引擎遇到任意一个方法返回的字节码指令会有返回值传递给上层的方法调用者简称正常完成出口一个方法的正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定在字节码指令中返回指令包含 ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn另外还有一个 return 指令供声明为 void 的方法、实例初始化方法、类和接口的初始化方法使用。在方法执行的过程中遇到了异常并且这个异常没有在方法内进行处理也就是只要在本方法的异常表中没有搜索到匹配的异常处理器就会导致方法退出。简称异常完成出口方法执行过程中抛出异常时的异常处理存储在一个异常处理表方便在发生异常的时候找到处理异常的代码。本质上方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于通过异常完成出口退出的不会给他的上层调用者产生任何的返回值2.4.5. 附加信息栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如对程序调试提供支持的信息但这些信息取决于具体的虚拟机实现。三、本地方法栈3.1 本地方法接口简单的讲一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法。为什么要使用本地方法Native Method?Java 使用起来非常方便然而有些层次的任务用 Java 实现起来也不容易或者我们对程序的效率很在意时问题就来了与 Java 环境外交互有时 Java 应用需要与 Java 外面的环境交互这就是本地方法存在的原因。与操作系统交互JVM 支持 Java 语言本身和运行时库但是有时仍需要依赖一些底层系统的支持。通过本地方法我们可以实现用 Java 与实现了 jre 的底层系统交互 JVM 的一些部分就是 C 语言写的。Suns JavaSun的解释器就是C实现的这使得它能像一些普通的C一样与外部交互。jre大部分都是用 Java 实现的它也通过一些本地方法与外界交互。比如类 java.lang.Thread 的 setPriority() 的方法是用Java 实现的但它实现调用的是该类的本地方法 setPrioruty()该方法是C实现的并被植入 JVM 内部。3.2 本地方法栈Native Method StackJava 虚拟机栈用于管理 Java 方法的调用而本地方法栈用于管理本地方法的调用本地方法栈也是线程私有的允许线程固定或者可动态扩展的内存大小如果线程请求分配的栈容量超过本地方法栈允许的最大容量Java 虚拟机将会抛出一个 StackOverflowError 异常如果本地方法栈可以动态扩展并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的本地方法栈那么 Java虚拟机将会抛出一个OutofMemoryError异常本地方法是使用C语言实现的它的具体做法是 Mative Method Stack 中登记native方法在 Execution Engine 执行时加载本地方法库当某个线程调用一个本地方法时它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区它甚至可以直接使用本地处理器中的寄存器直接从本地内存的堆中分配任意数量的内存并不是所有 JVM 都支持本地方法。因为 Java 虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果 JVM 产品不打算支持 native 方法也可以无需实现本地方法栈在 Hotspot JVM 中直接将本地方栈和虚拟机栈合二为一栈是运行时的单位而堆是存储的单位。栈解决程序的运行问题即程序如何执行或者说如何处理数据。堆解决的是数据存储的问题即数据怎么放、放在哪。四、堆内存4.1 内存划分对于大多数应用Java 堆是 Java 虚拟机管理的内存中最大的一块被所有线程共享。此内存区域的唯一目的就是存放对象实例几乎所有的对象实例以及数据都在这里分配内存。为了进行高效的垃圾回收虚拟机把堆内存逻辑上划分成三块区域分代的唯一理由就是优化 GC 性能新生带年轻代新对象和没达到一定年龄的对象都在新生代老年代养老区被长时间使用的对象老年代的内存空间应该要比年轻代更大元空间JDK1.8之前叫永久代像一些方法中的操作临时对象等JDK1.8之前是占用JVM内存JDK1.8之后直接使用物理内存JDK7Java 虚拟机规范规定Java 堆可以是处于物理上不连续的内存空间中只要逻辑上是连续的即可像磁盘空间一样。实现时既可以是固定大小也可以是可扩展的主流虚拟机都是可扩展的通过 -Xmx 和 -Xms 控制如果堆中没有完成实例分配并且堆无法再扩展时就会抛出 OutOfMemoryError 异常。年轻代 (Young Generation)年轻代是所有新对象创建的地方。当填充年轻代时执行垃圾收集。这种垃圾收集称为Minor GC。年轻一代被分为三个部分——伊甸园Eden Memory和两个幸存区Survivor Memory被称为from/to或s0/s1默认比例是8:1:1大多数新创建的对象都位于 Eden 内存空间中当 Eden 空间被对象填充时执行Minor GC并将所有幸存者对象移动到一个幸存者空间中Minor GC 检查幸存者对象并将它们移动到另一个幸存者空间。所以每次一个幸存者空间总是空的经过多次 GC 循环后存活下来的对象被移动到老年代。通常这是通过设置年轻一代对象的年龄阈值来实现的然后他们才有资格提升到老一代老年代(Old Generation)旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为主GC通常需要更长的时间。大对象直接进入老年代大对象是指需要大量连续内存空间的对象。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝元空间不管是 JDK8 之前的永久代还是 JDK8 及以后的元空间都可以看作是 Java 虚拟机规范中方法区的实现。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分但是它却有一个别名叫 Non-Heap非堆目的应该是与 Java 堆区分开。所以元空间放在后边的方法区再说。4.2 设置堆内存大小和 OOMJava 堆用于存储 Java 对象实例那么堆的大小在 JVM 启动的时候就确定了我们可以通过 -Xmx 和 -Xms 来设定-Xmx 用来表示堆的起始内存等价于 -XX:InitialHeapSize-Xms 用来表示堆的最大内存等价于 -XX:MaxHeapSize如果堆的内存大小超过 -Xms 设定的最大内存 就会抛出 OutOfMemoryError 异常。我们通常会将 -Xmx 和 -Xms 两个参数配置为相同的值其目的是为了能够在垃圾回收机制清理完堆区后不再需要重新分隔计算堆的大小从而提高性能默认情况下初始堆内存大小为电脑内存大小/64默认情况下最大堆内存大小为电脑内存大小/4可以通过代码获取到我们的设置值当然也可以模拟 OOMpublic static void main(String[] args) {//返回 JVM 堆大小long initalMemory Runtime.getRuntime().totalMemory() / 1024 /1024;//返回 JVM 堆的最大内存long maxMemory Runtime.getRuntime().maxMemory() / 1024 /1024;System.out.println(-Xms : initalMemory M);System.out.println(-Xmx : maxMemory M);System.out.println(系统内存大小 initalMemory * 64 / 1024 G);System.out.println(系统内存大小 maxMemory * 4 / 1024 G);
}查看 JVM 堆内存分配在默认不配置 JVM 堆内存大小的情况下JVM 根据默认值来配置当前内存大小默认情况下新生代和老年代的比例是 1:2可以通过 –XX:NewRatio 来配置新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1可以通过-XX:SurvivorRatio来配置若在JDK 7中开启了 -XX:UseAdaptiveSizePolicyJVM 会动态调整 JVM 堆中各个区域的大小以及进入老年代的年龄此时 –XX:NewRatio 和 -XX:SurvivorRatio 将会失效而 JDK 8 是默认开启-XX:UseAdaptiveSizePolicy在 JDK 8中不要随意关闭-XX:UseAdaptiveSizePolicy除非对堆内存的划分有明确的规划每次 GC 后都会重新计算 Eden、From Survivor、To Survivor 的大小计算依据是GC过程中统计的GC时间、吞吐量、内存占用量java -XX:PrintFlagsFinal -version | grep HeapSizeuintx ErgoHeapSizeLimit 0 {product}uintx HeapSizePerGCThread 87241520 {product}uintx InitialHeapSize : 134217728 {product}uintx LargePageHeapSizeThreshold 134217728 {product}uintx MaxHeapSize : 2147483648 {product}
java version 1.8.0_211
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)$ jmap -heap 进程号4.3 对象在堆中的生命周期在 JVM 内存模型的堆中堆被划分为新生代和老年代新生代又被进一步划分为Eden区和Survivor区Survivor区由From Survivor和To Survivor组成当创建一个对象时对象会被优先分配到新生代的Eden区此时 JVM 会给对象定义一个对象年轻计数器-XX:MaxTenuringThreshold当 Eden 空间不足时JVM 将执行新生代的垃圾回收Minor GCJVM 会把存活的对象转移到 Survivor 中并且对象年龄 1对象在 Survivor 中同样也会经历 Minor GC每经历一次 Minor GC对象年龄都会1如果分配的对象超过了-XX:PetenureSizeThreshold对象会直接被分配到老年代4.4 对象的分配过程为对象分配内存是一件非常严谨和复杂的任务JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题并且由于内存分配算法和内存回收算法密切相关所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。new 的对象先放在伊甸园区此区有大小限制当伊甸园的空间填满时程序又需要创建对象JVM 的垃圾回收器将对伊甸园区进行垃圾回收Minor GC将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区然后将伊甸园中的剩余对象移动到幸存者 0 区如果再次触发垃圾回收此时上次幸存下来的放到幸存者 0 区如果没有回收就会放到幸存者 1 区如果再次经历垃圾回收此时会重新放回幸存者 0 区接着再去幸存者 1 区什么时候才会去养老区呢 默认是 15 次回收标记在养老区相对悠闲。当养老区内存不足时再次触发 Major GC进行养老区的内存清理若养老区执行了 Major GC 之后发现依然无法进行对象的保存就会产生 OOM 异常4.5 GC 垃圾回收简介Minor GC、Major GC、Full GCJVM 在进行 GC 时并非每次都对堆内存新生代、老年代方法区区域一起回收的大部分时候回收的都是指新生代。针对 HotSpot VM 的实现它里面的 GC 按照回收区域又分为两大类部分收集Partial GC整堆收集Full GC部分收集不是完整收集整个 Java 堆的垃圾收集。其中又分为目前只有 G1 GC 会有这种行为目前只有 CMS GC 会有单独收集老年代的行为很多时候 Major GC 会和 Full GC 混合使用需要具体分辨是老年代回收还是整堆回收新生代收集Minor GC/Young GC只是新生代的垃圾收集老年代收集Major GC/Old GC只是老年代的垃圾收集混合收集Mixed GC收集整个新生代以及部分老年代的垃圾收集整堆收集Full GC收集整个 Java 堆和方法区的垃圾4.6 TLAB什么是 TLAB Thread Local Allocation Buffer?从内存模型而不是垃圾回收的角度对 Eden 区域继续进行划分JVM 为每个线程分配了一个私有缓存区域它包含在 Eden 空间内多线程同时分配内存时使用 TLAB 可以避免一系列的非线程安全问题同时还能提升内存分配的吞吐量因此我们可以将这种内存分配方式称为快速分配策略OpenJDK 衍生出来的 JVM 大都提供了 TLAB 设计为什么要有 TLAB ?堆区是线程共享的任何线程都可以访问到堆区中的共享数据由于对象实例的创建在 JVM 中非常频繁因此在并发环境下从堆区中划分内存空间是线程不安全的为避免多个线程操作同一地址需要使用加锁等机制进而影响分配速度尽管不是所有的对象实例都能够在 TLAB 中成功分配内存但 JVM 确实是将 TLAB 作为内存分配的首选。在程序中可以通过 -XX:UseTLAB 设置是否开启 TLAB 空间。默认情况下TLAB 空间的内存非常小仅占有整个 Eden 空间的 1%我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。一旦对象在 TLAB 空间分配内存失败时JVM 就会尝试着通过使用加锁机制确保数据操作的原子性从而直接在 Eden 空间中分配内存。4.7 堆是分配对象存储的唯一选择吗随着 JIT 编译期的发展和逃逸分析技术的逐渐成熟栈上分配、标量替换优化技术将会导致一些微妙的变化所有的对象都分配到堆上也渐渐变得不那么“绝对”了。 ——《深入理解 Java 虚拟机》逃逸分析逃逸分析(Escape Analysis)是目前 Java 虚拟机中比较前沿的优化技术。这是一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域当一个对象在方法中被定义后对象只在方法内部使用则认为没有发生逃逸。当一个对象在方法中被定义后它被外部方法所引用则认为发生逃逸。例如作为调用参数传递到其他地方中称为方法逃逸。例如public static StringBuffer craeteStringBuffer(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb;
}StringBuffer sb是一个方法内部变量上述代码中直接将sb返回这样这个 StringBuffer 有可能被其他方法所改变这样它的作用域就不只是在方法内部虽然它是一个局部变量称其逃逸到了方法外部。甚至还有可能被外部线程访问到譬如赋值给类变量或可以在其他线程中访问的实例变量称为线程逃逸。上述代码如果想要 StringBuffer sb不逃出方法可以这样写public static String createStringBuffer(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();
}不直接返回 StringBuffer那么 StringBuffer 将不会逃逸出方法。参数设置在 JDK 6u23版本之后HotSpot 中默认就已经开启了逃逸分析如果使用较早版本可以通过-XXDoEscapeAnalysis显式开启开发中使用局部变量就不要在方法外定义。使用逃逸分析编译器可以对代码做优化栈上分配将堆分配转化为栈分配。如果一个对象在子程序中被分配要使指向该对象的指针永远不会逃逸对象可能是栈分配的候选而不是堆分配同步省略如果一个对象被发现只能从一个线程被访问到那么对于这个对象的操作可以不考虑同步分离对象或标量替换有的对象可能不需要作为一个连续的内存结构存在也可以被访问到那么对象的部分或全部可以不存储在内存而存储在 CPU 寄存器JIT 编译器在编译期间根据逃逸分析的结果发现如果一个对象并没有逃逸出方法的话就可能被优化成栈上分配。分配完成后继续在调用栈内执行最后线程结束栈空间被回收局部变量对象也被回收。这样就无需进行垃圾回收了。常见栈上分配的场景成员变量赋值、方法返回值、实例引用传递代码优化之同步省略消除线程同步的代价是相当高的同步的后果是降低并发性和性能在动态编译同步块的时候JIT 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否能够被一个线程访问而没有被发布到其他线程。如果没有那么 JIT 编译器在编译这个同步块的时候就会取消对这个代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫做同步省略也叫锁消除。public void keep() {Object keeper new Object();synchronized(keeper) {System.out.println(keeper);}
}如上代码代码中对 keeper 这个对象进行加锁但是 keeper 对象的生命周期只在 keep()方法中并不会被其他线程所访问到所以在 JIT编译阶段就会被优化掉。优化成public void keep() {Object keeper new Object();System.out.println(keeper);
}代码优化之标量替换标量Scalar是指一个无法再分解成更小的数据的数据。Java 中的原始数据类型就是标量。相对的那些的还可以分解的数据叫做聚合量AggregateJava 中的对象就是聚合量因为其还可以分解成其他聚合量和标量。在 JIT 阶段通过逃逸分析确定该对象不会被外部访问并且对象可以被进一步分解时JVM不会创建该对象而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。这个过程就是标量替换。通过 -XX:EliminateAllocations 可以开启标量替换-XX:PrintEliminateAllocations 查看标量替换情况。public static void main(String[] args) {alloc();
}private static void alloc() {Point point new Point1,2;System.out.println(point.xpoint.x; point.ypoint.y);
}
class Point{private int x;private int y;
}以上代码中point 对象并没有逃逸出alloc()方法并且 point 对象是可以拆解成标量的。那么JIT 就不会直接创建 Point 对象而是直接使用两个标量 int x int y 来替代 Point 对象。private static void alloc() {int x 1;int y 2;System.out.println(point.xx; point.yy);
}代码优化之栈上分配我们通过 JVM 内存分配可以知道 JAVA 中的对象都是在堆上进行分配当对象没有被引用的时候需要依靠 GC 进行回收内存如果对象数量较多的时候会给 GC 带来较大压力也间接影响了应用的性能。为了减少临时对象在堆内分配的数量JVM 通过逃逸分析确定该对象不会被外部访问。那就通过标量替换将该对象分解在栈上分配内存这样该对象所占用的内存空间就可以随栈帧出栈而销毁就减轻了垃圾回收的压力。总结关于逃逸分析的论文在1999年就已经发表了但直到JDK 1.6才有实现而且这项技术到如今也并不是十分成熟的。其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的这其实也是一个相对耗时的过程。一个极端的例子就是经过逃逸分析之后发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。虽然这项技术并不十分成熟但是他也是即时编译器优化技术中一个十分重要的手段。五、方法区方法区Method Area与 Java 堆一样是所有线程共享的内存区域。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分但是它却有一个别名叫 Non-Heap非堆目的应该是与 Java 堆区分开。运行时常量池Runtime Constant Pool是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外还有一项信息是常量池Constant Pool Table用于存放编译期生成的各种字面量和符号引用这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中这种特性被开发人员利用得比较多的是 String.intern()方法。受方法区内存的限制当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。方法区的大小和堆空间一样可以选择固定大小也可选择可扩展方法区的大小决定了系统可以放多少个类如果系统类太多导致方法区溢出虚拟机同样会抛出内存溢出错误JVM 关闭后方法区即被释放5.1 解惑你是否也有看不同的参考资料有的内存结构图有方法区有的又是永久代元数据区一脸懵逼的时候方法区method area只是JVM规范中定义的一个概念用于存储类信息、常量池、静态变量、JIT编译后的代码等数据并没有规定如何去实现它不同的厂商有不同的实现。而永久代PermGen是 Hotspot 虚拟机特有的概念 Java8 的时候又被元空间取代了永久代和元空间都可以理解为方法区的落地实现。永久代物理是堆的一部分和新生代老年代地址是连续的受垃圾回收器管理而元空间存在于本地内存我们常说的堆外内存不受垃圾回收器管理这样就不受 JVM 限制了也比较难发生OOM都会有溢出异常Java7 中我们通过-XX:PermSize 和 -xx:MaxPermSize 来设置永久代参数Java8 之后随着永久代的取消这些参数也就随之失效了改为通过-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 用来设置元空间参数存储内容不同元空间存储类的元信息静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中如果方法区域中的内存不能用于满足分配请求则 Java 虚拟机抛出 OutOfMemoryErrorJVM 规范说方法区在逻辑上是堆的一部分但目前实际上是与 Java 堆分开的Non-Heap所以对于方法区Java8 之后的变化移除了永久代PermGen替换为元空间Metaspace永久代中的 class metadata 转移到了 native memory本地内存而不是虚拟机永久代中的 interned Strings 和 class static variables 转移到了 Java heap永久代参数 PermSize MaxPermSize - 元空间参数MetaspaceSize MaxMetaspaceSize5.2 设置方法区内存的大小jdk8及以后元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定替代上述原有的两个参数默认值依赖于平台。Windows 下-XX:MetaspaceSize 是 21M-XX:MaxMetaspacaSize 的值是 -1即没有限制与永久代不同如果不指定大小默认情况下虚拟机会耗尽所有的可用系统内存。如果元数据发生溢出虚拟机一样会抛出异常 OutOfMemoryError:Metaspace-XX:MetaspaceSize 设置初始的元空间大小。对于一个 64 位的服务器端 JVM 来说其默认的 -XX:MetaspaceSize 的值为20.75MB这就是初始的高水位线一旦触及这个水位线Full GC 将会被触发并卸载没用的类即这些类对应的类加载器不再存活然后这个高水位线将会重置新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足那么在不超过 MaxMetaspaceSize时适当提高该值。如果释放空间过多则适当降低该值如果初始化的高水位线设置过低上述高水位线调整情况会发生很多次通过垃圾回收的日志可观察到 Full GC 多次调用。为了避免频繁 GC建议将 -XX:MetaspaceSize 设置为一个相对较高的值。5.3 方法区内部结构方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。类型信息对每个加载的类型类 class、接口 interface、枚举 enum、注解 annotationJVM 必须在方法区中存储以下类型信息这个类型的完整有效名称全名包名.类名这个类型直接父类的完整有效名对于 interface或是 java.lang.Object都没有父类这个类型的修饰符publicabstractfinal 的某个子集这个类型直接接口的一个有序列表域Field信息JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序域的相关信息包括域名称、域类型、域修饰符public、private、protected、static、final、volatile、transient 的某个子集方法Method信息JVM 必须保存所有方法的方法名称方法的返回类型方法参数的数量和类型方法的修饰符publicprivateprotectedstaticfinalsynchronizednativeabstract 的一个子集方法的字符码bytecodes、操作数栈、局部变量表及大小abstract 和 native 方法除外异常表abstract 和 native 方法除外每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引栈、堆、方法区的交互关系5.4 运行时常量池运行时常量池Runtime Constant Pool是方法区的一部分理解运行时常量池的话我们先来说说字节码文件Class 文件中的常量池常量池表常量池一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外还包含一项信息那就是常量池表Constant Pool Table包含各种字面量和对类型、域和方法的符号引用。为什么需要常量池一个 java 源文件中的类、接口编译后产生一个字节码文件。而 Java 中的字节码需要数据支持通常这种数据会很大以至于不能直接存到字节码里换另一种方式可以存到常量池这个字节码包含了指向常量池的引用。在动态链接的时候用到的就是运行时常量池。如下我们通过jclasslib 查看一个只有 Main 方法的简单类字节码中的 #2 指向的就是 Constant Pool常量池可以看作是一张表虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。运行时常量池在加载类和结构到虚拟机后就会创建对应的运行时常量池常量池表Constant Pool Table是 Class 文件的一部分用于存储编译期生成的各种字面量和符号引用这部分内容将在类加载后存放到方法区的运行时常量池中JVM 为每个已加载的类型类或接口都维护一个常量池。池中的数据项像数组项一样是通过索引访问的运行时常量池中包含各种不同的常量包括编译器就已经明确的数值字面量也包括到运行期解析后才能够获得的方法或字段引用。此时不再是常量池中的符号地址了这里换为真实地址运行时常量池相对于 Class 文件常量池的另一个重要特征是动态性Java 语言并不要求常量一定只有编译期间才能产生运行期间也可以将新的常量放入池中String 类的 intern() 方法就是这样的当创建类或接口的运行时常量池时如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值则 JVM 会抛出 OutOfMemoryError 异常。5.5 方法区在 JDK6、7、8中的演进细节只有 HotSpot 才有永久代的概念jdk1.6及之前有永久代静态变量存放在永久代上jdk1.7有永久代但已经逐步“去永久代”字符串常量池、静态变量移除保存在堆中jdk1.8及之后取消永久代类型信息、字段、方法、常量保存在本地内存的元空间但字符串常量池、静态变量仍在堆中移除永久代原因http://openjdk.java.net/jeps/122为永久代设置空间大小是很难确定的。在某些场景下如果动态加载类过多容易产生 Perm 区的 OOM。如果某个实际 Web 工程中因为功能点比较多在运行过程中要不断动态加载很多类经常出现 OOM。而元空间和永久代最大的区别在于元空间不在虚拟机中而是使用本地内存所以默认情况下元空间的大小仅受本地内存限制对永久代进行调优较困难5.6 方法区的垃圾回收方法区的垃圾收集主要回收两部分内容常量池中废弃的常量和不再使用的类型。先来说说方法区内常量池之中主要存放的两大类常量字面量和符号引用。字面量比较接近 java 语言层次的常量概念如文本字符串、被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念包括下面三类常量类和接口的全限定名字段的名称和描述符方法的名称和描述符HotSpot 虚拟机对常量池的回收策略是很明确的只要常量池中的常量没有被任何地方引用就可以被回收判定一个类型是否属于“不再被使用的类”需要同时满足三个条件该类所有的实例都已经被回收也就是 Java 堆中不存在该类及其任何派生子类的实例加载该类的类加载器已经被回收这个条件除非是经过精心设计的可替换类加载器的场景如 OSGi、JSP 的重加载等否则通常很难达成该类对应的 java.lang.Class 对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法Java 虚拟机被允许堆满足上述三个条件的无用类进行回收这里说的仅仅是“被允许”而并不是和对象一样不使用了就必然会回收。是否对类进行回收HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制还可以使用 -verbose:class 以及 -XX:TraceClassLoading 、-XX:TraceClassUnLoading 查看类加载和卸载信息。在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能以保证永久代不会溢出。参考与感谢算是一篇学习笔记共勉主要来源宋红康 JVM 教程《深入理解 Java 虚拟机 第三版》https://docs.oracle.com/javase/specs/index.htmlhttps://www.cnblogs.com/wicfhwffg/p/9382677.htmlhttps://www.cnblogs.com/hollischuang/p/12501950.html
往期推荐
2万字看完这篇才敢说自己真的懂线程池2020-11-16 面试官你说说互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景2020-11-13 25 张图1.4 w字彻底搞懂分布式事务原理2020-11-10 关注我每天陪你进步一点点喜欢就点个在看再走吧