当前位置: 首页 > news >正文

自学考试 网页制作与网站建设06627同城网

自学考试 网页制作与网站建设06627,同城网,展示型网站有哪些功能,wordpress静态文件nginx配置前文#xff1a; 《Java8之类的加载》 《Java8之类加载机制class源码分析》 写在开头#xff1a;本文为学习后的总结#xff0c;可能有不到位的地方#xff0c;错误的地方#xff0c;欢迎各位指正。 JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数…前文 《Java8之类的加载》 《Java8之类加载机制class源码分析》 写在开头本文为学习后的总结可能有不到位的地方错误的地方欢迎各位指正。 JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途以及创建和销毁的时间有的区域随着虚拟机进程的启动而存在有些区域则依赖用户线程的启动和结束而建立和销毁。如下图所示展示效果为JDK1.6版本的模型 可以从图中看出JVM内存结构可以分为线程内与线程外两部分线程内的是单个线程 线程私有程序计数器、虚拟机栈、本地方法区线程共享堆、方法区, 堆外内存Java7的永久代或JDK8的元空间、代码缓存 目录 一、程序计数器 二、Java 虚拟机栈 1、概要 2、 栈帧的内部结构 2.1、局部变量表 槽 Slot 2.2、操作数栈 栈顶缓存Top-of-stack-Cashing 2.3、动态链接 JVM 是如何执行方法调用的 2.4、方法返回地址 3、栈的运行原理 4、栈内异常 三、本地方法栈 四、堆 1、概要 2、堆内存划分 年轻代 老年代 3、设置堆内存大小和 OOM 4、查看 JVM 堆内存分配 5、TLAB 5.1、TLAB是什么Thread Local Allocation Buffer 5.2、为什么要有 TLAB ? 6、堆外对象分配 6.1、逃逸分析 6.2、代码优化之同步省略消除 6.3、代码优化之标量替换 6.4、代码优化之栈上分配 七、方法区 1、概述 2、方法去、永久代与元空间 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 方法区内存设置与垃圾回收 3、运行时常量池 4、字符串常量池 JDK 1.7 为什么要将字符串常量池移动到堆中 5、JDK演进过程 八、直接内存 九、内存分配过程 1、流程 类加载检查 分配内存 内存分配并发问题 初始化零值 设置对象头 执行 init 方法 2、样例演示 3、对象的内存布局 4、对象的访问定位 句柄 直接指针 十、OutOfMemoryError 什么是 OutOfMemoryError 堆空间溢出 分析方法 内存泄漏 内存溢出 GC 开销超过限制 永久代、元空间不足 直接内存溢出 StackOverflowError 一、程序计数器 程序计数器Program Counter Register用来存储指向下一条指令的地址即将要执行的指令代码。 因为CPU需要不停的切换各个线程这时候切换回来以后就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。 由于多线程在一个特定的时间段内只会执行其中某一个线程方法CPU会不停的做任务切换这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址所以为每个线程都分配了一个PC寄存器每个线程都独立计算不会互相影响。 如果线程正在执行的是一个 Java 方法这个计数器记录的是正在执行的虚拟机字节码指令的地址如果正在执行的是 Native 方法这个计数器值则为空Undefined 程序计数器是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域         二、Java 虚拟机栈 1、概要 Java 虚拟机栈(Java Virtual Machine Stacks)与程序计数器一样也是线程私有的它的生命周期和线程相同随着线程的创建而创建随着线程的死亡而死亡。 每个 Java 方法在执行的同时都会创建一个栈帧Stack Frame用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 方法调用的数据需要通过栈进行传递每一次方法调用都会有一个对应的栈帧被压入栈中每一个方法调用结束后都会有一个栈帧被弹出。栈由一个个栈帧组成而每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似两者都是先进后出的数据结构只支持出栈和入栈两种操作。 2、 栈帧的内部结构 栈帧的内部结构每个栈帧Stack Frame中存储着 局部变量表Local Variables操作数栈Operand Stack(或称为表达式栈)动态链接Dynamic Linking指向运行时常量池的方法引用方法返回地址Return Address方法正常退出或异常退出的地址一些附加信息 2.1、局部变量表 主要存放了编译期可知的各种数据类型boolean、byte、char、short、int、float、long、double、对象引用reference 类型它不同于对象本身可能是一个指向对象起始地址的引用指针也可能是指向一个代表对象的句柄或其他与此对象相关的位置。                                        ​​​​​​ 槽 Slot 局部变量表最基本的存储单元是 Slot变量槽在局部变量表中32 位以内的类型只占用一个 Slot(包括returnAddress类型)64 位的类型long和double占用两个连续的 Slotbyte、short、char 在存储前被转换为intboolean也被转换为int0 表示 false非 0 表示 truelong 和 double 则占据两个 Slot。 JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引通过这个索引即可成功访问到局部变量表中指定的局部变量值索引值的范围从 0 开始到局部变量表最大的 Slot 数量当一个实例方法被调用的时候它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 Slot 上如果需要访问局部变量表中一个 64bit 的局部变量值时只需要使用前一个索引即可。比如访问 long 或 double 类型变量不允许采用任何方式单独访问其中的某一个 Slot。 如果当前帧是由构造方法或实例方法创建的那么该对象引用 this 将会存放在 index 为 0 的 Slot 处其余的参数按照参数表顺序继续排列栈帧中的局部变量表中的槽位是可以重用的如果一个局部变量过了其作用域那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位从而达到节省资源的目的。 局部变量表中的变量也是重要的垃圾回收根节点只要被局部变量表中直接或间接引用的对象都不会被回收。 2.2、操作数栈 操作数栈主要用于保存计算过程的中间结果同时作为计算过程中变量临时的存储空间。 每个独立的栈帧中除了包含局部变量表之外还包含一个后进先出Last-In-First-Out的操作数栈也可以称为表达式栈Expression Stack操作数栈在方法执行过程中根据字节码指令往操作数栈中写入数据或提取数据即入栈push、出栈pop某些字节码指令将值压入操作数栈其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如执行复制、交换、求和等操作。 每一个操作数栈都会拥有一个明确的栈深度用于存储数值其所需的最大深度在编译期就定义好了保存在方法的 Code 属性的 max_stack 数据项中栈中的任何一个元素都可以是任意的 Java 数据类型 32bit 的类型占用一个栈单位深度64bit 的类型占用两个栈单位深度操作数栈并非采用访问索引的方式来进行数据访问的而是只能通过标准的入栈和出栈操作来完成一次数据访问如果被调用的方法带有返回值的话其返回值将会被压入当前栈帧的操作数栈中并更新 PC 寄存器中下一条需要执行的字节码指令。 栈顶缓存Top-of-stack-Cashing 基于栈式架构的虚拟机所使用的零地址指令更加紧凑但完成一项操作的时候必然需要使用更多的入栈和出栈指令这同时也就意味着将需要更多的指令分派instruction dispatch次数和内存读/写次数。由于操作数是存储在内存中的因此频繁的执行内存读/写操作必然会影响执行速度。为了解决这个问题HotSpot JVM 设计者们提出了栈顶缓存技术将栈顶元素全部缓存在物理 CPU 的寄存器中以此降低对内存的读/写次数提升执行引擎的执行效率。 2.3、动态链接 在 Java 源文件被编译到字节码文件中时所有的变量和方法引用都作为符号引用Symbolic Reference保存在 Class 文件的常量池中。比如描述一个方法调用了另外的其他方法时就是通过常量池中指向方法的符号引用来表示的那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。 JVM 是如何执行方法调用的 方法调用不同于方法执行方法调用阶段的唯一任务就是确定被调用方法的版本即调用哪一个方法暂时还不涉及方法内部的具体运行过程。Class 文件的编译过程中不包括传统编译器中的连接步骤一切方法调用在 Class文件里面存储的都是符号引用而不是方法在实际运行时内存布局中的入口地址直接引用。也就是需要在类加载阶段甚至到运行期才能确定目标方法的直接引用。 在 JVM 中将符号引用转换为调用方法的直接引用与方法的绑定机制有关 静态链接当一个字节码文件被装载进 JVM 内部时如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接动态链接如果被调用的方法在编译期无法被确定下来也就是说只能在程序运行期将调用方法的符号引用转换为直接引用由于这种引用转换过程具备动态性因此也就被称之为动态链接 2.4、方法返回地址 用来存放调用该方法的 PC 寄存器的值Java 方法有两种返回方式一种是 return 语句正常返回一种是抛出异常。不管哪种返回方式都会导致栈帧被弹出方法退出后都返回到该方法被调用的位置。也就是说 栈帧随着方法调用而创建随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。 方法正常退出时调用者的 PC 计数器的值作为返回地址即调用该方法的指令的下一条指令的地址。而通过异常退出的返回地址是要通过异常表来确定的栈帧中一般不会保存这部分信息。 3、栈的运行原理 JVM 直接对 Java 栈的操作只有两个对栈帧的压栈和出栈遵循“先进后出/后进先出”原则 在一条活动线程中一个时间点上只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧栈顶栈帧是有效的这个栈帧被称为当前栈帧Current Frame与当前栈帧对应的方法就是当前方法Current Method定义这个方法的类就是当前类Current Class。 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。 如果在该方法中调用了其他方法对应的新的栈帧会被创建出来放在栈的顶端称为新的当前栈帧。不同线程中所包含的栈帧是不允许存在相互引用的即不可能在一个栈帧中引用另外一个线程的栈帧。 如果当前方法调用了其他方法方法返回之际当前栈帧会传回此方法的执行结果给前一个栈帧接着虚拟机会丢弃当前栈帧使得前一个栈帧重新成为当前栈帧Java。 方法有两种返回函数的方式一种是正常的函数返回使用 return 指令另一种是抛出异常不管用哪种方式都会导致栈帧被弹出。 4、栈内异常 栈空间虽然不是无限的但一般正常调用的情况下是不会出现问题的。不过如果函数调用陷入无限循环的话就会导致栈中被压入太多栈帧而占用太多空间导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候就抛出 StackOverFlowError 错误。 除了 StackOverFlowError 错误之外栈还可能会出现OutOfMemoryError错误这是因为如果栈的内存大小可以动态扩展 如果虚拟机在动态扩展栈时无法申请到足够的内存空间则抛出OutOfMemoryError异常。 简单总结一下程序运行中栈可能会出现两种错误 StackOverFlowError 若栈的内存大小不允许动态扩展那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候就抛出 StackOverFlowError 错误。OutOfMemoryError 如果栈的内存大小可以动态扩展 如果虚拟机在动态扩展栈时无法申请到足够的内存空间则抛出OutOfMemoryError异常。 另外可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小栈的大小直接决定了函数调用的最大可达深度。在 JDK 1.4 中默认为 256K而在 JDK 1.5 默认为 1M  java -Xss2M HackTheJava 三、本地方法栈 本地方法栈Native Method Stack 与虚拟机栈的作用相似。 二者的区别在于虚拟机栈为 Java 方法服务本地方法栈为 Native 方法服务。本地方法并不是用 Java 实现的而是由 C 语言实现的。 注意本地方法栈也会抛出 StackOverflowError 异常和 OutOfMemoryError 异常。 四、堆 1、概要 堆heap是Java虚拟机所管理的内存中最大的一块Java 堆是所有线程共享的一块内存区域在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例几乎所有的对象实例以及数组都在这里分配内存。 之所以说是几乎是因为随着JIT编译器的发展与逃逸分析技术逐渐成熟栈上分配、标量替换优化技术使得所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析如果某些方法中的对象引用没有被返回或者未被外面使用也就是未逃逸出去那么对象可以直接在栈上分配内存。 2、堆内存划分 Java 堆是垃圾收集器管理的主要区域因此也被称作 GC 堆Garbage Collected Heap。从垃圾回收的角度由于现在收集器基本都采用分代垃圾收集算法所以 Java 堆还可以细分为新生代和老年代再细致一点有Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存或者更快地分配内存。 在 JDK 7 版本及 JDK 7 版本之前堆内存被通常分为下面三部分 新生代内存(Young Generation)老生代(Old Generation)永久代(Permanent Generation) 下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代中间一层属于老年代最右边一层属于永久代。 JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代元空间使用的是本地内存。 下面介绍方法区这部分内容时会讲解 年轻代 年轻代是所有新对象创建的地方。当填充年轻代时执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园Eden Memory和两个幸存区Survivor Memory被称为from/to或s0/s1默认比例是8:1:1。 大多数新创建的对象都位于 Eden 内存空间中当 Eden 空间被对象填充满时执行Minor GC并将所有幸存者对象移动到一个幸存者空间中。Minor GC 检查幸存者对象并将它们移动到另一个幸存者空间。 所以每次一个幸存者空间总是空的经过多次 GC 循环后存活下来的对象被移动到老年代。可以通过设置参数 -XX:MaxTenuringThreshold来干预筛选的年龄阈值然后他们才有资格提升到老一代。 Hotspot 遍历所有对象时按照年龄从小到大对其所占用的大小进行累积当累积的某个年龄大小超过了 survivor 区的一半时取这个年龄和 MaxTenuringThreshold 中更小的一个值作为新的晋升年龄阈值。 uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {//survivor_capacity是survivor空间的大小 size_t desired_survivor_size (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); size_t total 0; uint age 1; while (age table_size) { total sizes[age];//sizes数组是每个年龄段对象大小 if (total desired_survivor_size) break; age; } uint result age MaxTenuringThreshold ? age : MaxTenuringThreshold;... } 老年代 老年代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GCMajor GC通常需要更长的时间。 大对象直接进入老年代大对象是指需要大量连续内存空间的对象判断标准是超过了-XX:PetenureSizeThreshold。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝。 3、设置堆内存大小和 OOM Java 堆用于存储 Java 对象实例那么堆的大小在 JVM 启动的时候就确定了我们可以通过 -Xmx 和 -Xms 来设定 -Xms 用来表示堆的起始内存等价于 -XX:InitialHeapSize-Xmx 用来表示堆的最大内存等价于 -XX:MaxHeapSize 如果堆的内存大小超过 -Xmx 设定的最大内存 就会抛出 OutOfMemoryError 异常。我们通常会将 -Xmx 和 -Xms 两个参数配置为相同的值其目的是为了能够在垃圾回收机制清理完堆区后不再需要重新分隔计算堆的大小从而提高性能 默认情况下初始堆内存大小为电脑内存大小/64默认情况下最大堆内存大小为电脑内存大小/4 4、查看 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 的大小。 5、TLAB 5.1、TLAB是什么Thread Local Allocation Buffer JVM 为每个线程在 Eden 空间内分配了一个私有缓存区域使得多线程同时分配内存时可以避免一系列的非线程安全问题同时还能提升内存分配的吞吐量因此我们可以将这种内存分配方式称为快速分配策略。 5.2、为什么要有 TLAB ? 因为堆区是线程共享的任何线程都可以访问到堆区中的共享数据。由于对象实例的创建在 JVM 中非常频繁因此在并发环境下从堆区中划分内存空间是线程不安全的为避免多个线程操作同一地址需要使用加锁等机制进而影响分配速度。 尽管不是所有的对象实例都能够在 TLAB 中成功分配内存但 JVM 确实是将 TLAB 作为内存分配的首选。在程序中可以通过 -XX:UseTLAB 设置是否开启 TLAB 空间。默认情况下TLAB 空间的内存非常小仅占有整个 Eden 空间的 1%我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。 一旦对象在 TLAB 空间分配内存失败时JVM 就会尝试着通过使用加锁机制确保数据操作的原子性从而直接在 Eden 空间中分配内存。 6、堆外对象分配 6.1、逃逸分析 逃逸分析(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不逃出方法可以调整为不直接返回 StringBuffer那么 StringBuffer 将不会逃逸出方法 public static String createStringBuffer(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); } 在 JDK 6u23 版本之后HotSpot 中默认就已经开启了逃逸分析如果使用较早版本可以通过-XXDoEscapeAnalysis显式开启。 使用逃逸分析编译器可以对代码完成如下优化 栈上分配将堆分配转化为栈分配。如果一个对象在子程序中被分配要使指向该对象的指针永远不会逃逸对象可能是栈分配的候选而不是堆分配同步省略如果一个对象被发现只能从一个线程被访问到那么对于这个对象的操作可以不考虑同步分离对象或标量替换有的对象可能不需要作为一个连续的内存结构存在也可以被访问到那么对象的部分或全部可以不存储在内存而存储在 CPU 寄存器 JIT 编译器在编译期间根据逃逸分析的结果发现如果一个对象并没有逃逸出方法的话就可能被优化成栈上分配。分配完成后继续在调用栈内执行最后线程结束栈空间被回收局部变量对象也被回收。这样就无需进行垃圾回收了。常见栈上分配的场景成员变量赋值、方法返回值、实例引用传递。 6.2、代码优化之同步省略消除 由于线程同步的代价相当高可能会降低并发性和性能因此在动态编译同步块的时候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); } 6.3、代码优化之标量替换 标量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); } 6.4、代码优化之栈上分配 我们通过 JVM 内存分配可以知道 JAVA 中的对象都是在堆上进行分配当对象没有被引用的时候需要依靠 GC 进行回收内存如果对象数量较多的时候会给 GC 带来较大压力也间接影响了应用的性能。为了减少临时对象在堆内分配的数量JVM 通过逃逸分析确定该对象不会被外部访问。那就通过标量替换将该对象分解在栈上分配内存这样该对象所占用的内存空间就可以随栈帧出栈而销毁就减轻了垃圾回收的压力。 七、方法区 1、概述 方法区Method Area也被称为永久代。方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 运行时常量池Runtime Constant Pool是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外还有一项信息是常量池Constant Pool Table用于存放编译期生成的各种字面量和符号引用这部分内容将类在加载后进入方法区的运行时常量池中存放。 2、方法去、永久代与元空间 方法区method area只是 JVM 规范中定义的一个概念用于存储类信息、常量池、静态变量、JIT编译后的代码等数据并没有规定如何去实现它不同的厂商有不同的实现。而永久代PermGen是 Hotspot 虚拟机特有的概念 Java8 的时候又被元空间取代了永久代和元空间都可以理解为方法区的落地实现。 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 整个永久代有一个 JVM 本身设置的固定大小上限无法进行调整而元空间使用的是本地内存受本机可用内存的限制虽然元空间仍旧可能溢出但是比原来出现的几率会更小。元空间里面存放的是类的元数据这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制这样能加载的类就更多了。 方法区内存设置与垃圾回收 Java7 中我们通过-XX:PermSize 和 -xx:MaxPermSize 来设置永久代参数Java8 之后随着永久代的取消这些参数也就随之失效了改为通过-XX:MetaspaceSize 定义元空间的初始大小如果未指定此标志则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小和 -XX:MaxMetaspaceSize 最大元空间大小默认值为 unlimited这意味着它只受系统内存的限制用来设置元空间参数。 对于一个 64 位的服务器端 JVM 来说其默认的 -XX:MetaspaceSize 的值为20.75MB这就是初始的高水位线一旦触及这个水位线Full GC 将会被触发并卸载没用的类即这些类对应的类加载器不再存活然后这个高水位线将会重置新的高水位线的值取决于 GC 后释放了多少元空间。 如果释放的空间不足那么在不超过 MaxMetaspaceSize时适当提高该值。如果释放空间过多则适当降低该值如果初始化的高水位线设置过低上述高水位线调整情况会发生很多次通过垃圾回收的日志可观察到 Full GC 多次调用。 为了避免频繁 GC建议将 -XX:MetaspaceSize 设置为一个相对较高的值。 3、运行时常量池 class 文件中除了有类的版本、字段、方法、接口等描述信息外还有用于存放编译期生成的各种字面量Literal和符号引用Symbolic Reference的 常量池表(Constant Pool Table) 。字面量是源代码中的固定值的表示法即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。 常量池表会在类加载后存放到方法区的运行时常量池中。运行时常量池的功能类似于传统编程语言的符号表尽管它包含了比典型符号表更广泛的数据。既然运行时常量池是方法区的一部分自然受到方法区内存的限制当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。   4、字符串常量池 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串String 类专门开辟的一块区域主要目的是为了避免字符串的重复创建。 // 在堆中创建字符串对象”ab“ // 将字符串对象”ab“的引用保存在字符串常量池中 String aa ab; // 直接返回字符串常量池中字符串对象”ab“的引用 String bb ab; System.out.println(aabb);// true HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 可以简单理解为一个固定大小的HashTable 容量为 StringTableSize可以通过 -XX:StringTableSize 参数来设置保存的是字符串key和 字符串对象的引用value的映射关系字符串对象的引用指向堆中的字符串对象。 JDK1.7 之前字符串常量池存放在永久代。 JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。 JDK 1.7 为什么要将字符串常量池移动到堆中 主要是因为永久代方法区实现的 GC 回收效率太低只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收将字符串常量池放到堆中能够更高效及时地回收字符串内存。 5、JDK演进过程 JDK版本是否有永久代字符串常量池放在哪里方法区逻辑上规范由哪些实际的部分实现的jdk1.6及之前有永久代运行时常量池包括字符串常量池静态变量存放在永久代上这个时期方法区在HotSpot中是由永久代来实现的以至于这个时期说方法区就是指永久代jdk1.7有永久代但已经逐步“去永久代”字符串常量池、静态变量移除保存在堆中这个时期方法区在HotSpot中由永久代类型信息、字段、方法、常量和堆字符串常量池、静态变量共同实现jdk1.8取消永久代类型信息、字段、方法、常量保存在本地内存的元空间但字符串常量池、静态变量仍在堆中这个时期方法区在HotSpot中由本地内存的元空间类型信息、字段、方法、常量和堆字符串常量池、静态变量共同实现 八、直接内存 直接内存是一种特殊的内存缓冲区并不在 Java 堆或方法区中分配的而是通过 JNI 的方式在本地内存上分配的。 直接内存并不是虚拟机运行时数据区的一部分也不是虚拟机规范中定义的内存区域但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。 JDK1.4 中新加入的 NIONon-Blocking I/O也被称为 New I/O引入了一种基于通道Channel与缓存区Buffer的 I/O 方式它可以直接使用 Native 函数库直接分配堆外内存然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能因为避免了在 Java 堆和 Native 堆之间来回复制数据。 直接内存的分配不会受到 Java 堆的限制但是既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 九、内存分配过程 下文节选自《Java内存区域详解》、《Java 内存管理》 1、流程 通过上面的介绍我们大概知道了虚拟机的内存情况下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程 类加载检查 虚拟机遇到一条 new 指令时首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有那必须先执行相应的类加载过程。类的加载可以参考我之前写过的《Java8之类的加载》、《Java8之类加载机制class源码分析》 分配内存 在类加载检查通过后接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。 分配方式有 “指针碰撞” 和 “空闲列表” 两种选择哪种分配方式由 Java 堆是否规整决定而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 指针碰撞 适用场合堆内存规整即没有内存碎片的情况下。原理用过的内存全部整合到一边没有用过的内存放在另一边中间有一个分界指针只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。使用该分配方式的 GC 收集器Serial, ParNew空闲列表  适用场合堆内存不规整的情况下。原理虚拟机会维护一个列表该列表中会记录哪些内存块是可用的在分配的时候找一块儿足够大的内存块儿来划分给对象实例最后更新列表记录。使用该分配方式的 GC 收集器CMS 选择以上两种方式中的哪一种取决于 Java 堆内存是否规整。而 Java 堆内存是否规整取决于 GC 收集器的算法是标记-清除还是标记-整理也称作标记-压缩值得注意的是复制算法内存也是规整的。 内存分配并发问题 在创建对象的时候有一个很重要的问题就是线程安全因为在实际开发过程中创建对象是很频繁的事情作为虚拟机来说必须要保证线程是安全的通常来讲虚拟机采用两种方式来保证线程安全 CAS失败重试 CAS 是乐观锁的一种实现方式。所谓乐观锁就是每次不加锁而是假设没有冲突而去完成某项操作如果因为冲突失败就重试直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。TLAB 为每一个线程预先在 Eden 区分配一块儿内存JVM 在给线程中的对象分配内存时首先在 TLAB 分配当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时再采用上述的 CAS 进行内存分配。 初始化零值 内存分配完成后虚拟机需要将分配到的内存空间都初始化为零值不包括对象头这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用程序能访问到这些字段的数据类型所对应的零值。 设置对象头 初始化零值完成之后虚拟机要对对象进行必要的设置例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外根据虚拟机当前运行状态的不同如是否启用偏向锁等对象头会有不同的设置方式。 执行 init 方法 在上面工作都完成之后从虚拟机的视角来看一个新的对象已经产生了但从 Java 程序的视角来看对象创建才刚开始init 方法还没有执行所有的字段都还为零。所以一般来说执行 new 指令之后会接着执行 init 方法把对象按照程序员的意愿进行初始化这样一个真正可用的对象才算完全产生出来。 2、样例演示 我们用如下样例来展示下具体的内存运行过程 public class JVMCase {// 常量public final static String MAN_SEX_TYPE man;// 静态变量public static String WOMAN_SEX_TYPE woman;public static void main(String[] args) {Student stu new Student();stu.setName(nick);stu.setSexType(MAN_SEX_TYPE);stu.setAge(20);JVMCase jvmcase new JVMCase();// 调用静态方法print(stu);// 调用非静态方法jvmcase.sayHello(stu);}// 常规静态方法public static void print(Student stu) {System.out.println(name: stu.getName() ; sex: stu.getSexType() ; age: stu.getAge());}// 非静态方法public void sayHello(Student stu) {System.out.println(stu.getName() say: hello);} }class Student{String name;String sexType;int age;public String getName() {return name;}public void setName(String name) {this.name name;}public String getSexType() {return sexType;}public void setSexType(String sexType) {this.sexType sexType;}public int getAge() {return age;}public void setAge(int age) {this.age age;} }运行以上代码时JVM 处理过程如下 1JVM 向操作系统申请内存JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间根据内存大小找到具体的内存分配表然后把内存段的起始地址和终止地址分配给 JVM接下来 JVM 就进行内部分配。 2JVM 获得内存空间后会根据配置参数分配堆、栈以及方法区的内存大小。 3class 文件加载、验证、准备以及解析其中准备阶段会为类的静态变量分配内存初始化为系统的初始值即上文的初始化零值步骤 4完成上一个步骤后将会进行最后一个初始化阶段。在这个阶段中JVM 首先会执行构造器 clinit 方法编译器会在 .java 文件被编译成 .class 文件时收集所有类的初始化代码包括静态变量赋值语句、静态代码块、静态方法收集在一起成为 clinit() 方法。 5执行方法。启动 main 线程执行 main 方法开始执行第一行代码。此时堆内存中会创建一个 student 对象对象引用 student 就存放在栈中。 6此时再次创建一个 JVMCase 对象调用 sayHello 非静态方法sayHello 方法属于对象 JVMCase此时 sayHello 方法入栈并通过栈中的 student 引用调用堆中的 Student 对象之后调用静态方法 printprint 静态方法属于 JVMCase 类是从静态方法中获取之后放入到栈中也是通过 student 引用调用堆中的 student 对象。 3、对象的内存布局 在 Hotspot 虚拟机中对象在内存中的布局可以分为 3 块区域对象头、实例数据和对齐填充。 Hotspot 虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据哈希码、GC 分代年龄、锁状态标志等等另一部分是类型指针即对象指向它的类元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。 实例数据部分是对象真正存储的有效信息也是在程序中所定义的各种类型的字段内容。 对齐填充部分不是必然存在的也没有什么特别的含义仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数1 倍或 2 倍因此当对象实例数据部分没有对齐时就需要通过对齐填充来补全。 4、对象的访问定位 建立对象就是为了使用对象我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定目前主流的访问方式有使用句柄、直接指针。 句柄 如果使用句柄的话那么 Java 堆中将会划分出一块内存来作为句柄池reference 中存储的就是对象的句柄地址而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。 直接指针 如果使用直接指针访问reference 中存储的直接就是对象的地址。 这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址在对象被移动时只会改变句柄中的实例数据指针而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快它节省了一次指针定位的时间开销。HotSpot 虚拟机主要使用的就是这种方式来进行对象访问。 十、OutOfMemoryError 什么是 OutOfMemoryError OutOfMemoryError 简称为 OOM。Java 中对 OOM 的解释是没有空闲内存并且垃圾收集器也无法提供更多内存。通俗的解释是JVM 内存不足了。 在JVM 规范中除了程序计数器区域外其他运行时区域都可能发生 OutOfMemoryError 异常简称 OOM。 堆空间溢出 java.lang.OutOfMemoryError: Java heap space 这个错误意味着堆空间溢出。 更细致的说法是Java 堆内存已经达到 -Xmx 设置的最大值。Java 堆用于存储对象实例只要不断地创建对象并且保证 GC Roots 到对象之间有可达路径来避免垃圾收集器回收这些对象那么当堆空间到达最大容量限制后就会产生 OOM。 堆空间溢出有可能是内存泄漏也可能是内存溢出。需要使用 jstack 和 jmap 生成 threaddump 和 heapdump然后用内存分析工具如MAT进行分析。 分析方法 使用 jmap 或 -XX:HeapDumpOnOutOfMemoryError 获取堆快照使用内存分析工具visualvm、mat、jProfile 等对堆快照文件进行分析。根据分析图重点是确认内存中的对象是否是必要的分清究竟是是内存泄漏Memory Leak还是内存溢出Memory Overflow 内存泄漏 内存泄漏是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。 内存泄漏并非指内存在物理上的消失而是应用程序分配某段内存后由于设计错误失去了对该段内存的控制因而造成了内存的浪费。内存泄漏随着被执行的次数不断增加最终会导致内存溢出。 内存泄漏常见场景 静态容器 声明为静态static的 HashMap、Vector 等集合通俗来讲 A 中有 B当前只把 B 设置为空A 没有设置为空回收时 B 无法回收。因为被 A 引用监听器 监听器被注册后释放对象时没有删除监听器物理连接 各种连接池建立了连接必须通过 close() 关闭链接 重点关注 FGC — 从应用程序启动到采样时发生 Full GC 的次数。FGCT — 从应用程序启动到采样时 Full GC 所用的时间单位秒。FGC 次数越多FGCT 所需时间越多越有可能存在内存泄漏。 如果是内存泄漏可以进一步查看泄漏对象到 GC Roots 的对象引用链。这样就能找到泄漏对象是怎样与 GC Roots 关联并导致 GC 无法回收它们的。掌握了这些原因就可以较准确的定位出引起内存泄漏的代码。 导致内存泄漏的常见原因是使用容器且不断向容器中添加元素但没有清理导致容器内存不断膨胀。 内存溢出 如果不存在内存泄漏即内存中的对象确实都必须存活着则应当检查虚拟机的堆参数-Xmx 和 -Xms与机器物理内存进行对比看看是否可以调大。并从代码上检查是否存在某些对象生命周期过长、持有时间过长的情况尝试减少程序运行期的内存消耗。 GC 开销超过限制 java.lang.OutOfMemoryError: GC overhead limit exceeded 这个错误官方给出的定义是超过 98% 的时间用来做 GC 并且回收了不到 2% 的堆内存时会抛出此异常。这意味着发生在 GC 占用大量时间为释放很小空间的时候发生的是一种保护机制。导致异常的原因一般是因为堆太小没有足够的内存。 与 Java heap space 错误处理方法类似先判断是否存在内存泄漏。如果有则修正代码如果没有则通过 -Xms 和 -Xmx 适当调整堆内存大小。 永久代、元空间不足 java.lang.OutOfMemoryError: PermGen space、java.lang.OutOfMemoryError: Metaspace (JDK8 及以后版本) 可能是永久代元空间中装入了太多的类或太大的类 Perm 永久代空间主要用于存放 Class 和 Meta 信息包括类的名称和字段带有方法字节码的方法常量池信息与类关联的对象数组和类型数组以及即时编译器优化。GC 在主程序运行期间不会对永久代空间进行清理默认是 64M 大小。 在 JDK8 之前的版本中可以通过 -XX:PermSize 和 -XX:MaxPermSize 设置永久代空间大小从而限制方法区大小并间接限制其中常量池的容量。 而对于元空间而面临 OutOfMemoryError 时如果应用程序耗尽了内存中的 Metaspace 区域则应增加 Metaspace 的大小。更改应用程序启动配置-XX:MaxMetaspaceSize512m 另一种解决方案甚至更简单。你可以通过删除此参数来完全解除对 Metaspace 大小的限制JVM 默认对 Metaspace 的大小没有限制。但是请注意以下事实这样做可能会导致大量交换或达到本机物理内存而分配失败。 直接内存溢出 由直接内存导致的内存溢出一个明显的特征是在 Head Dump 文件中不会看见明显的异常如果发现 OOM 之后 Dump 文件很小而程序中又直接或间接使用了 NIO就可以考虑检查一下是不是这方面的原因。 StackOverflowError 对于 HotSpot 虚拟机来说栈容量只由 -Xss 参数来决定如果线程请求的栈深度大于虚拟机所允许的最大深度将抛出 StackOverflowError 异常。 从实战来说栈溢出的常见原因一般都是递归函数调用层数太深或大量循环、死循环导致。 参考资料 《运行时数据区域》 《Java 内存管理》 《JVM 基础 - JVM 内存结构》 《Java内存区域详解》
http://www.hkea.cn/news/14555516/

相关文章:

  • 东莞网站设计方案网站正在建设中 htmll
  • 山东省交通运输厅网站开发单位安装wordpress要数据库吗
  • php p2p网站开发太原网站建设质量推荐
  • 网站推广优化网站制作的流程是什么
  • 那个网站有兼职做室内设计购物网站系统设计
  • 上海医疗网站备案网业车资格证怎么报名
  • asp网站建设教程四站合一网站建设
  • 阿里网站多个域名凡科平台盲审
  • dedecms本地调试好的网站怎么上传到服务器完整网站开发教程
  • 网站关键词被百度屏蔽怎么办搜索不到我的网站
  • 天津做网站报价国内十大搜索引擎
  • 梅州市住房和建设局网站深圳企业网站建设企业
  • 外贸网站google推广室内设计方案网站
  • 自己建设网站不会咋办呀住房和城乡建设部科技网站首页
  • 新网互联 网站上传常州网站建设企业网站制作
  • 郑州网站建设zzmshlwordpress 删除版权信息
  • 企业网站报价网站建设服务好公司排名
  • 新校区建设网站管理规定pc网站向手机站传递权重
  • 网页建设网站代码wordpress 图片加速
  • wordpress自动标签内联太原百度seo优化推广
  • 校园文化建设网站素材小荷特卖的网站谁做的
  • 企业建设网站的过程网站建设明细报价单
  • 网站首页全屏怎么做做移动网站
  • 肇庆企业做网站柳江企业网站建设公司
  • 做网站必须用域名吗做数学网站
  • 青州网站建设公司包装盒网站模板下载
  • wordpress 仿站 菜单wordpress移动支付免费
  • 上海全国网站建设wordpress主题怎么该轮播
  • 东莞技术好的网站建设推广个人网站建设的论文
  • 做venn图的网站wordpress商店会员管理