程序计数器(Program Counter Register)
是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。
虚拟机栈 (Stack Area)
栈是线程私有,栈帧是栈的元素。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
本地方法栈 (Native Method Area)
JVM 中的栈包括 Java 虚拟机栈和本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 方法服务,本地方法栈则为 JVM 使用到的 Native 方法服务。
堆 (Heap Area)
堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间。
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。
方法区(Method Area)、 元空间区(MetaSpace)
方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。
JDK 8 使用元空间 MetaSpace 代替方法区,元空间并不在JVM中,而是在本地内存中
在运行时数据区中包括那几个区域?
1、线程私有区域:1. 程序计数器 2. 虚拟机栈 3. 本地方方法栈
2、线程共享区域:4. 方法区(元空间) 5. 堆
线程是一个程序中的运行单元,JVM允许一个应用有多个线程并行的执行任务。
在Hotspot JVM中,每个线程都与操作系统的本地线程之间映射,当一个Java线程准备好执行后,此时一个操作系统的本地线程也会同时创建,Java线程执行终止后,本地线程也会回收。
操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程的run()方法。
JVM线程的主要几类:
JVM中的程序计数器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能运行。
这里,并非广义上所指的物理寄存器,或许将其翻译为PC寄存器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
PC寄存器用来存储指向下一条指令的地址,也就是即将要执行的指令代码,由执行引擎读取下一条指令。
线程私有
的,生命周期与线程的生命周期保持一致。当前方法
。PC寄存器会存储当前线程正在执行的Java方法的JVM指令地址,如果执行的native方法,则是undefined。public class PCRegister { public static void main(String[] args) { int i = 20; int j = 30; int k = i + j; String str = "hello"; System.out.println(str); } }
我们使用jclasslib看一下编译后:
左侧是数字其实就是偏移地址,PC寄存器就是存储的这个,而右侧就是指令。
前面操作比较简单,其实就是将常量值20压入栈然后存入索引1的位置,然后将常量值30压入栈然后存入索引2,然后取出1,2,相加之后存入索引3。
我们重点说一下后面的操作,偏移地址10的位置。
ldc
:将int, float或String型常量值从常量池中推送至栈顶。
而后面的#2
的位置从下图常量池中,我们可以看到对应的是String,它又关联了#27
,#27
对应的UTF-8 字符串为:hello。存入索引4的位置。但是我们发现偏移地址从10跳到了12,就是因为我们在ldc中执行了两步操作。
getstatic
:获取静态变量引用,并将其引用推到操作数栈中。
我们可以看到它对应的常量池#3
对应的属性 #28
.#29
两个,#28
对应的是Class 找到#34
我们可以看到是java.lang.System,#29
对应了#35
,#36
,也就是out和printStream。
然后读取aload 4 也就是str的值进行输入,最后return结束。
局部变量表,操作数栈都是由执行引擎来操作的,再翻译成机器指令来操作cpu。
因为CPU需要不停的切换各个线程,这个时候切换回来后,需要知道从哪里接着继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
多线程在一个特定的时间段内指挥执行其中某一个线程的方法,CPU会不停地做任务切换,这必然会导致经常中断或者恢复。
简单来说就是方便各个线程之间可以独立计算,不会出现相互干扰的问题。
每个线程都会有一个虚拟机栈,多线程就会有多个虚拟机栈。是线程私有
,虚拟机栈里面是一个一个的栈帧(Stack Frame),每一个栈帧都是在方法执行的同时创建的,描述的是Java方法执行的内存模型。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。栈是先进后出的。
作用是主管Java程序的运行,它保存方法的局部变量、部分结果、并参与方法的调用与返回。
在活动线程中,只有一个栈帧是处于活跃状态的,也就是说只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
优点:跨平台,指令集小,编译器容易实现。
缺点:性能下降,实现同样的功能需要更多的指令。
StackOverflowError
错误。OutofMemroyError
错误。解决方案:
使用参数 -Xss 选项来设置线程最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
在启动参数加入 -Xss256k 或者随意大小。
栈运行原理
压栈
和出栈
,遵循后进先出
原则。当前栈帧(Current Frame)
,定义这个方法的类就是当前类(Current Class)
。分为五大类:
局部变量表(Local Variables)
操作数栈(Operand Stack)
局部变量表(Local Variables)
不存在数据的安全问题
方法嵌套调用的次数由栈的大小来决定。
一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增加的需求。进而函数调用就会占用更多的栈空间,导致其嵌套的次数就会减少。局部变量表中的变量只在当前方法调用中有效。
在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。关于Slot的理解
顺序
被复制到局部变量表中的每一个Slot上。代码小例子:
public String test(Date dateP,String name2){ dateP = null; name2 = "Jack"; double weight = 1.1; char gender = '男'; return dateP + name2; }
我们使用jclasslib来看的话可以看到Index也就是Slot,我们发现3也就是double是占了两个Slot的。
操作数栈
后进先出
的操作数栈,也称之为表达式栈
。主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
这个时候方法的操作数栈是空的。
栈中任意一个元素都可以是任意的Java数据类型。
操作数栈的字节码指令分析
首先我们创建如下简单的代码:
public class OperandStackTest { public void testAddOperand(){ byte i = 15; int j = 8; int k = i + j; } }
使用jclasslib反编译以后我们看到如下指令:
Code: stack=2, locals=4, args_size=1 0 bipush 15 2 istore_1 3 bipush 8 5 istore_2 6 iload_1 7 iload_2 8 iadd 9 istore_3 10 return
在标注灰色的地方,我们看一看到指令地址是0,所以右侧的PC寄存器就是0,bipush操作就是将常量值15存入我们的操作数栈的栈顶,现在局部变量表中还是初始化的状态。
当指令执行到了2的位置,PC寄存器里存放的就是2,执行的istore指令,将操作数栈中数据取出存入对应的局部变量表中。
当指令执行到了3的位置,PC寄存器里存放的就是3,bipush操作就是将常量值8存入我们的操作数栈的栈顶,现在局部变量表中只有对应下标为i的值为15。
当指令执行到了5的位置,PC寄存器里存放的就是5,执行的istore指令,将操作数栈中数据取出存入对应的局部变量表中。
当指令执行到了6的位置,PC寄存器里存放的就是6,执行的iload指令,将局部变量表中的数据取出存入操作数栈的栈顶。(指令地址7也同理)
当指令执行到了8的位置,PC寄存器里存放的就是8,执行的iadd指令,将栈顶的两个数据取出进行相加,将结果存入操作数栈栈顶。(相加操作由执行引擎将字节码指令来翻译成机器指令来操作cpu。)
当指令执行到了9的位置,PC寄存器里存放的就是9,执行的istore指令,将栈顶的元素取出存入对应的局部变量表中。
stack=2, locals=4, args_size=1
stack对应的就是我们的操作数栈的深度。
locals对应的就是我们的局部变量表的长度。
args_size对应的就是参数的长度,静态代码块为0。
动态链接(或指向运行时常量池的方法引用)
运行时常量池
中栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
。比如:invokedynamic指令动态链接的作用就是为了将这些符号引用转换为调用方法的直接饮用
。public class DynamicLinkingTest { int num = 10; public void methodA() { System.out.println("methodA..."); } public void method() { System.out.println("methodB..."); methodA(); num++; } }
使用jclasslib反编译以后我们看到如下指令:
Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokevirtual #2 // Method a:()V 4: return
invokevirtual 后面的#2
符号引用对应的就是Constant pool中的直接引用。#2
对应了方法引用,#3
,#17
……最终对应到方法A()
Constant pool: #1 = Methodref #4.#16 // java/lang/Object."<init>":()V #2 = Methodref #3.#17 // com/suanfa/jvm/OperandStackTest.a:()V #3 = Class #18 // com/suanfa/jvm/OperandStackTest #4 = Class #19 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 LocalVariableTable #10 = Utf8 this #11 = Utf8 Lcom/suanfa/jvm/OperandStackTest; #12 = Utf8 a #13 = Utf8 b #14 = Utf8 SourceFile #15 = Utf8 OperandStackTest.java #16 = NameAndType #5:#6 // "<init>":()V #17 = NameAndType #12:#6 // a:()V #18 = Utf8 com/suanfa/jvm/OperandStackTest #19 = Utf8 java/lang/Object
方法调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关。
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知
,且运行期间保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接
如果被调用方法在编译期间无法被确定下来
,只能在程序运行时将调用方法的符号引用转换为直接引用,由于这种引用转换的过程具备动态性,因此也就被称为动态链接。
对应的绑定机制为:早期绑定(Early Binding)、晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用,这个过程仅发生一次。
早期绑定
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变
时,即可将这个方法所属的类型进行绑定,这样一来,由于明确了被调用方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用替换为直接引用。
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关方法
这种绑定就叫做晚期绑定。
方法的调用:虚方法与非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称之为非虚方法。
静态变量、私有方法、final方法、实例构造器、父类方法都是非虚方法。
其它方法称之为虚方法、
普通调用指令:
<init>
方法、私有方法以及父类方法,解析阶段确定唯一方法版本动态调用指令:
前四条指令固化在虚拟机的内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定版本。其中invokevirtual和invokestatic指令调用的方法称为非虚方法,其余的(final修饰除外)称为虚方法。
/** * 解析调用中非虚方法、虚方法的测试 */ class Father { public Father(){ System.out.println("Father默认构造器"); } public static void showStatic(String s){ System.out.println("Father show static"+s); } public final void showFinal(){ System.out.println("Father show final"); } public void showCommon(){ System.out.println("Father show common"); } } public class Son extends Father{ public Son(){ super(); } public Son(int age){ this(); } public static void main(String[] args) { Son son = new Son(); son.show(); } //不是重写的父类方法,因为静态方法不能被重写 public static void showStatic(String s){ System.out.println("Son show static"+s); } private void showPrivate(String s){ System.out.println("Son show private"+s); } public void show(){ //invokestatic showStatic(" 大头儿子"); //invokestatic super.showStatic(" 大头儿子"); //invokespecial showPrivate(" hello!"); //invokespecial super.showCommon(); //invokevirtual 因为此方法声明有final 不能被子类重写,所以也认为该方法是非虚方法 showFinal(); //虚方法如下 //invokevirtual showCommon();//没有显式加super,被认为是虚方法,因为子类可能重写showCommon info(); MethodInterface in = null; //invokeinterface 不确定接口实现类是哪一个 需要重写 in.methodA(); } public void info(){ } } interface MethodInterface { void methodA(); }
invokedynamic指令
直到JDK8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接生成的方式。
方法返回地址(Return Address)
区别在于,通过异常完成的出口不会给它上层调用者产生任何的返回值
栈帧存放空间不足导致出现StackOverflowError异常。通过 -Xss设置栈的大小。
不能保证。
不是。在同一台机器上,如果jvm设置的内存过大,就会导致其它程序所占用的内存小。比如elasticsearch、kafka,虽然它们都是基于jvm运行的程序(java和scala都是依赖于jvm),但是它们的数据不是放到jvm内存中,而是放到os cache中(操作系统管理的内存区域),避免了jvm垃圾回收的影响。
垃圾回收是否会涉及到虚拟机栈?
不会
运行时数据区 | Error | GC |
---|---|---|
程序计数器 | × | × |
虚拟机栈 | √ | × |
本地方法栈 | √ | × |
堆 | √ | √ |
方法区 | √ | √ |
不一定,可能会发生方法逃逸。
public StringBuilder escapeDemo1() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("a"); stringBuilder.append("b"); return stringBuilder; }
方法逃逸:在一个方法体内,定义一个局部变量,而它可能被外部方法引用,比如作为调用参数传递给方法,或作为对象直接返回。或者,可以理解成对象跳出了方法。
什么是本地方法?
一个Native Method是这样的Java方法:该方法的实现由非Java语言实现,比如C。
本地方法的作用就是为了融合不同编程语言为Java所用。
使用native
关键字修饰的方法就是本地方法。
本地方法栈简介
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。2. 它甚至可以直接使用本地处理器中的寄存器。3. 直接从本地内存的堆中分配任意数量的内存。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
堆的核心概述
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项"-Xmx"和"-Xms"来进行设置。
存储在JVM中的Java对象可以分为两类:
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)
其中年轻代又划分为Eden空间、Survivor0和Survivor1空间(也叫做from区、to区)。
在Hotspot中,Eden空间和Survivor0和Survivor1空间缺省比例是8:1:1。
也可以使用"-XX:SurvivorRatio" 调整这个空间比例。比如-XX:SruvivorRatio=8。
几乎所有的Java对象都在Eden区被new出来的。
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
1、new的对象先放在Eden区,此区域有大小限制。
2、当Eden的空间填满时,程序又需要创建新的对象,JVM的垃圾回收器将对Eden区不再被其它对象所引用的对象进行销毁。再加载新的对象放到Eden。
3、 然后将Eden区剩余的对象移动到Survivor0区。
4、如果再次触发垃圾回收,此时上次幸存下来的放在Survivor0区,如果没有回收,就会放到Survivor1区。
5、 如果再次经历垃圾回收,此时会重新放回Survivor0区,接着再去Survivor1区。
6、 当"年龄"到达15之后就会被放到old区。可以设置参数:-XX:MaxTenuringThreshold=<N>
。
7、 当old区内存不足时,再次触发 GC:Major GC,进行old区内存清理。
8、 如果old区在进行了GC后依然无法进行对象的保存,就会产生OOM异常。
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
分配内存的特殊情况
如果对象一开始就过大,如果Eden区放不下的话会直接放入old区。
如果old区也放不下,则会发生Full GC 。如果GC后还是放不下则会报错OOM。
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区/元空间)区域一起回收的,大部分时候回收的手是指新生代。
针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大类,一种是部分收集(Partial GC),一种是完整收集(Full GC)。
部分收集
目前只有CMS GC会有单独收集老年代的行为。
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
目前只有G1 GC会有这种行为。
年轻代GC(Minor GC)触发机制
当年轻代空间不足时候,就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor满不会触发GC。(每次Minor GC就会清理Eden区内存)
因为Java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。
Minor GC会引发STW,暂停其它用户线程,等垃圾回收结束,用户线程才会恢复运行。
老年代GC(Major GC/Full GC)触发机制
指发生在老年代的GC,对象从老年代消失时,我们说”Major GC“ 或者 ”Full GC”发生了。
出现了Major GC,经常会伴随至少一次Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
也就是在老年代空间不足时,先尝试触发Minor GC,如果之后空间还是不足,则触发Major GC。
Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
如果Major GC后内存还不足就会报OOM了。
Full GC触发机制
说明:在开发中尽量避免 Full GC,这样STW时间会更短
为什么要有TLAB(Thread Local Allocation Buffer)
堆区是线程共享区域
,任何线程都可以访问到堆区中的共享数据。
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是不安全的。
为避免多个线程操作统一地址,需要使用加锁等机制,进而影响分配速度。
什么是TLAB
从内存模型而不是垃圾收集的角度,对Eden区继续进行划分,JVM为每个线程分配了一个私有缓存区域
,它包含在Eden区内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略
。
TLAB说明
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM的确是将TLAB作为内存分配的首选
。
在程序中,开发人员可以通过选项"-XX:UseTLAB"设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%
,可以通过"-XX:TLABWasteTargetPercent"设置TLAB空间所占用Eden空间的百分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试通过使用加锁机制
确保数据操作的原子性,从而直接在Eden空间中分配内存。
如果经过逃逸分析( Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。逃逸分析手段
逃逸分析的基本行为就是分析对象动态作用域:
结论:开发中能用局部变量的,就不要使用在方法外定义。
运行时数据区图解
栈、堆、方法区、的交互关系
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。” 但对于HotspotJVM而言,方法区还有一个别名叫做Non-Heap,目的就是要和堆分开。
所以方法区看作是一块独立于Java堆的内存空间。
JDK7及以前(永久代):
JDK8(元空间):
jdk7及以前: 查询 jps -> jinfo -flag PermSize [进程id] -XX:PermSize=100m -XX:MaxPermSize=100m jdk8及以后: 查询 jps -> jinfo -flag MetaspaceSize [进程id] -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
解决报错OOM:(内存泄漏、内存溢出)
代码案例:
/** * jdk6/7中: * -XX:PermSize=10m -XX:MaxPermSize=10m * * jdk8中: * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m * */ public class OOMTest extends ClassLoader { public static void main(String[] args) { int j = 0; try { OOMTest test = new OOMTest(); for (int i = 0; i < 10000; i++) { //创建ClassWriter对象,用于生成类的二进制字节码 ClassWriter classWriter = new ClassWriter(0); //指明版本号,修饰符,类名,包名,父类,接口 classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); //返回byte[] byte[] code = classWriter.toByteArray(); //类的加载 test.defineClass("Class" + i, code, 0, code.length);//Class对象 j++; } } finally { System.out.println(j); } } }
《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的 类型信息、常量、静态变量、即时编译器编译后的代码缓存
等。
类型信息
对每个加载的类型( 类class、接口interface、枚举enum、注解annotation),JVM必 .须在方法区中存储以下类型信息:
域(Field)信息
方法信息(method)
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
non-final的类变量(非声明为final的static静态变量)
全局常量(static final)
被声明为final的类变量的处理方法则不同,每个全局常量在编译的
时候就被分配了。
public static int count = 1; public static final int number = 2;
反编译后就可以看到如下代码:
public static int count; descriptor: I flags: ACC_PUBLIC, ACC_STATIC public static final int number; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 2
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poo1 Table),包括各种字面量和对类型域和方法的符号引用。
一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池;而这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。
小结:字节码当中的常量池结构(constant pool),可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
首先明确:只有HotSpot才有永久代。 BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虛拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
Hotspot中 方法区的变化:
但字符串常量池、静态变量仍留在堆空间。
永久代为什么要被元空间替换
这项改动是很有必要的,原因有:
StringTable为什么要调整
有些人认为方法区(如Hotspot,虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的ZGC 收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 Hotspot 虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废奔的常量
和不再使用的类型。
常量池中废奔的常量
常量池中包括下面三类常量:
只要常量池中的常量没有被任何地方引用,就可以被回收。
回收废弃常量与回收Java堆中的对象非常类似。常量池中不再使用的类型
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
Java虛拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了一Xnoclassgc 参数进行控制,还可以使用一verbose:class以及一XX: +TraceClass一Loading、一XX:+TraceClassUnLoading查 看类加载和卸载信息。
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及oSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。