目录
1、运行时数据区的结构
运行时数据区包括,方法去、堆、pc计数器、本地方法区和虚拟机栈。其中方法区在JDK8中更名为元空间。程序计数器和虚拟机栈都不会发生GC,程序计数器也不会发生溢出异常,虚拟机栈有溢出错误。堆空间和方法区有GC和OOM异常,但是方法区GC条件苛刻,GC效率低,GC主要发生在堆区。
由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。栈是运行时的单位,而堆是存储的单位。
2、虚拟机栈栈帧的组成
虚拟机栈由一个个的栈帧组成,栈帧的大小决定了栈的最大深度,而栈帧的大小主要取决于局部变量表和操作数栈。
虚拟机栈由局部变量表 、操作数栈、动态链接和方法返回地址四部分组成,有时还会包含一些附加信息。
2.1局部变量表
局部变量表也被称之为局部变量数组或本地变量表,定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
solt(槽)是局部变量表最基本的存储单元,jvm虚拟机会为局部变量表上的每一个槽分配一个访问索引,通过该索引可以访问到该局部变量表中所指定的局部变量值。32位的变量占据一个槽位,64位的变量占据2个槽位(long,double),byte,short,char,boolean都会转化位int进行存储。
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。静态方法是没有this的。
局部变量表中的变量也是垃圾回收中的根节点,被其直接或间接引用的对象不能被垃圾回收器回收。
栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
2.2操作数栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
栈中可以存储任意的数据类型,32bit占用一个栈深,64bit占用两个栈深。Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
2.3动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
常量池的作用,就是为了提供一些符号和常量,便于指令的识别
动态链接与静态链接的区别
当一个 字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。如果被调用的目标方法在编译期不确定,只有在运行期才能将调用的符号引用 转化为直接引用,这种情况称之为动态链接。
2.4方法返回地址
存放调用该方法的PC寄存器的值,方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
在字节码指令中,返回指令包含ireturn(当返回值是boolena、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
3、程序计数器(PC寄存器)
PC寄存器是用来存储指向下一条指令的地址 ,它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器会存储当前线程正在执行的java方法的JVM指令地址。
面试问题
1.使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2.PC寄存器为什么会设定为线程私有
只有在线程私有pc寄存器的情况下,才能准确地记录各个线程正在执行的当前字节码指令地址,保证程序安全有效的运行。
4、本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法是使用C语言实现的,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
本地方法可以通过本地方法接口来 访问虚拟机内部的运行时数据区,它甚至可以直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存。
简单来讲,一个Native Method就是一个java程序调用非java代码的接口,一个Native Method 的实现由非Java语言实现。在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
为什么要使用Native Method
(1)与java环境之外的环境交互
有时java应用需要与java外面的环境交互,这是本地方法存在的主要原因。
(2)与操作系统交互
JVM支持着java语言本身和运行库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至jvm的一些部分就是用C写的。还有,如果我们要使用一些java语言本身没有提供封装的操作系统特性时,我们也需要使用本地方法。
(3)Sun’s Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。
5、堆空间
一个进程对应一个jvm实例,一个运行时数据区,又包含多个线程,这些线程共享了方法区和堆,每个线程包含了程序计数器、本地方法栈和虚拟机栈。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
5.1堆的细分内存结构
JDK 7以前: 新生区+养老区+永久区
JDK 8以后: 新生区+养老区+元空间
Young Generation Space:又被分为Eden区和Survior区
默认情况下,初始内存大小:物理内存大小 1/64;最大内存大小:物理内存大小 1/4。默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3。在hotSpot中,Eden空间和另外两个Survivor空间所占的比例是8:1:1(测试的时候是6:1:1),开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例,如-XX:SurvivorRatio=8。可以使用选项-Xmn设置新生代最大内存大小(这个参数一般使用默认值就好了)
5.2对象分配过程
为新对象分配内存是件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配的问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
- new的对象先放伊甸园区。此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存者0区。
- 再加载新的对象放到伊甸园区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数:-XX:MaxTenuringThreshold=进行设置。
- 在养老区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
- 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。
5.3Minor GC、Major GC、Full GC
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 目前,只有CMS GC会有单独收集老年代的行为
- 注意,==很多时候Major GC 会和 Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收==
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
- 目前,之后G1 GC会有这种行为
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC.(每次Minor GC会清理年轻代的内存,Survivor是被动GC,不会主动GC)。出现了Major GC,经常会伴随至少一次的Minor GC(不是绝对的,在Parallel Scavenge 收集器的收集策略里就有直接进行Major GC的策略选择过程)也就是老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC。Major GC速度一般会比Minor GC慢10倍以上,STW时间更长。
5.4Full GC触发机制
①调用System.gc()时,系统建议执行Full GC,但是不必然执行
②老年代空间不足
③方法区空间不足
④通过Minor GC后进入老年代的平均大小小于老年代的可用内存
⑤由Eden区,Survivor S0(from)区向S1(to)区复制时,所有幸存对象的内存大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
5.5为什么要把Java堆分代?
其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描,而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
5.6动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入到老年代。无需等到MaxTenuringThreshold中要求的年龄
5.7 TLAB
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。多线程同时分配内存时,使用TLAB可以避免一系列的线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM明确是是将TLAB作为内存分配的首选。在程序中,开发人员可以通过选项“-XX:UseTLAB“ 设置开启TLAB空间,默认情况下,TLAB空间的内存非常小,仅占有整个EDen空间的1%,当然我们可以通过选项 ”-XX:TLABWasteTargetPercent“ 设置LAB空间所占用Eden空间的百分比大小。一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
5.8堆空间的参数设置
- -XX:PrintFlagsInitial: 查看所有参数的默认初始值
- -XX:PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
- 具体查看某个参数的指令:
- jps:查看当前运行中的进程
- jinfo -flag SurvivorRatio 进程id: 查看新生代中Eden和S0/S1空间的比例
- 具体查看某个参数的指令:
- -Xms: 初始堆空间内存(默认为物理内存的1/64)
- -Xmx: 最大堆空间内存(默认为物理内存的1/4)
- -Xmn: 设置新生代大小(初始值及最大值)
- -XX:NewRatio: 配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认15)
- -XX:+PrintGCDetails:输出详细的GC处理日志
- 打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
- -XX:HandlePromotionFailure:是否设置空间分配担保
在发生Minor Gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
- 如果大于,则此次Minor GC是安全的
- 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。(==JDK 7以后的规则HandlePromotionFailure可以认为就是true==)
- 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
- √如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
- √如果小于,则改为进行一次Fu11 GC。
- √如果HandlePromotionFailure=false,则改为进行一次Fu11 GC。
- 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
在JDK6 Update24之后(JDK7),HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC
5.9堆不是分配对象的唯一选择
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GCinvisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
- 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
- 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
- 逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,==对象只在方法内部使用,则认为没有发生逃逸==。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
- 如何快速的判断是否发生了逃逸分析,就看new的对象实体是否有可能在方法外被调用
- 在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析
- 如果使用了较早的版本,开发人员可以通过
- -XX:DoEscapeAnalysis 显式开启逃逸分析
- -XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
5.10 使用逃逸分析,编译器可以对代码做如下优化
- 栈上分配:将堆分配转化为栈分配。
- 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
- 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
栈上分配:
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成之后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须机型垃圾回收了
同步省略:
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除
分离对象或标量替换:
- 标量是指一个无法在分解成更小的数据的数据。Java中的原始数据类型就是标量
- 相对的,那些还可以分解的数据叫做==聚合量(Aggregate)==,Java中对象就是聚合量,因为它可以分解成其他聚合量和标量
- 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含成员变量来替代。这个过程就是标量替换。
6、方法区(元空间)
6.1栈、堆、方法区的交互关系
《Java虚拟机规范》中明确说明:‘尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。’但对于HotSpotJVM而言,方法区还有一个别名叫做Non-heap(非堆),目的就是要和堆分开。所以,方法区可以看作是一块独立于Java堆的内存空间。
方法区的大小,跟堆空间一样,可以选择固定大小或者可拓展方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space 或者 java.lang,OutOfMemoryError:Metaspace,比如:
- 加载大量的第三方jar包;
- Tomcat部署的工程过多;
- 大量动态生成反射类;
在jdk7及以前,习惯上把方法区称为永久代。jdk8开始,使用元空间取代了永久代。在jdk8中,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。
6.2设置方法区大小
jdk7及以前:
- 通过一XX:PermSize来设置永久代初始分配空间。默认值是20.75M
- -XX : MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
- 当JVM加载的类信息容量超过了这个值,会报异常OutOfMemoryError : PermGen space
jdk8及以后:
- 元数据区大小可以使用参数一XX:MetaspaceSize和一XX :MaxMetaspaceSize指定,替代上述原有的两个参数。
- 默认值依赖于平台。windows下,一XX:MetaspaceSize是21M,一 XX MaxMetaspaceSize的值是-1, 即没有限制。
- 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。 如果元数据区发生溢出,虚拟机一样会拋出异常OutOfMemoryError: Metaspace
- -XX:MetaspaceSize: 设置初始的元空间大小。对于一个64位的服务器端JVM来说, 其默认的一XX :MetaspaceSize值为21MB.这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
- 如果初始化的高水位线设置过低,高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将- XX :MetaspaceSize设置为一个相对较高的值。
6.3方法区的内部结构
《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
对每个加载的类型( 类class、接口interface、枚举enum、注解annotation),JVM必 .须在方法区中存储以下类型信息:
- ①这个类型的完整有效名称(全名=包名.类名)
- ②这个类型直接父类的完整有效名(对于interface或是java. lang.Object,都没有父类)
- ③这个类型的修饰符(public, abstract, final的某个子集)
- ④这个类型直接接口的一个有序列表
域信息(成员变量)
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、 域类型、域修饰符(public, private, protected, static, final, volatile, transient的某个子集)
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public, private, protected, static, final, synchronized, native , abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native 方法除外)
- 异常表( abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例所共享,即使没有类实例你也可以访问它。
全局常量 static final 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就被分配了。
运行时常量池
- 一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池.
几种在常量池内存储的数据类型包括:
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
运行时常量池
- 运行时常量池( Runtime Constant Pool)是方法区的一部分。
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用==,这部分内容将在类加载后存放到方法区的运行时常量池中。
- JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
6.4方法区的演进细节
- jdk1.6及之前:有永久代(permanent generation) ,静态变量存放在 永久代上
- jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
- jdk1.8及之后: 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
6.5永久代为什么要被元空间替换
- 为永久代设置空间大小是很难确定的。 在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
- 对永久代进行调优是很困难的。
6.6StringTable(字符串常量池) 为什么要调整到堆空间
jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full GC 是老年代的空间不足、永久代不足时才会触发。这就导致了StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存.
6.7明静态变量存在哪
静态变量的引用与静态变量的实例对象都存放在堆空间中。前文说的将静态变量转移到堆空间指的是将静态变量的引用改存入堆空间。也就是我们自己给方法的命名值。
6.8方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废奔的常量和不再使用的类型 。方法区内常量池之中主要存放的两大类常量:字面量和符号引用。 字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 1、类和接口的全限定名
- 2、字段的名称和描述符
- 3、方法的名称和描述符
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。|】
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java虛拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了一Xnoclassgc 参数进行控制,