概述
- 一个 JVM 实例中只存在一个堆内存,是 Java 内存管理的核心区域。在 JVM 启动时创建堆。
- 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但是在逻辑上它应该被视为连续的。
- 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)
- 几乎所有的对象实例以及数组都应当在运行时分配在堆上,所以在栈帧上只保存对象实例和数组的引用。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾回收时才有可能被移除。堆上的数据是线程共享的,本线程用完后,无法确定其他线程是否还是使用(要确定其他线程是否在使用代价较高)。因此需要等到离线 任务(垃圾回收)去执行这个耗时较长的确认过程。
- 堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。由于 Method Area 和 Heap Area 内存没有过期策略,所以才诞生类 GC。Heap Area 数据变化比 Method Area 更加剧烈,有用户线程创建很多数据,因此也可以 GC就是为 Heap Area 而生的。
在下图在与到 new、newarray、anewarray 指令时就会在堆中开辟空间创建对象。
内存细分
现代大部分垃圾收集器都是基于分代收集理论设计的。
Java 7 之前堆内存逻辑划分为:
- 新生区(Young Generation Space): Young/New
- 养老区(Tenure generation Space):Old/Tenure
- 永久区(Permanent Space):Perm
Java 8 之前堆内存逻辑划分为:
- 新生区(Young Generation Space): Young/New
- 又被划分为 Eden 区和 Survivor 区
- 养老区(Tenure generation Space):Old/Tenure
- 元空间(Meta Space):Mate
约定:
- 新生区 ⇔ \Leftrightarrow ⇔ 新生代 ⇔ \Leftrightarrow ⇔ 年轻代
- 养老区 ⇔ \Leftrightarrow ⇔ 老年区 ⇔ \Leftrightarrow ⇔ 老年代
- 永久区 ⇔ \Leftrightarrow ⇔ 永久代
堆空间内部结构(JDK 7)
如下图:我们限制 JVM 的栈空间为 10 M,通过下图可以看到:
- 伊甸园:2 M
- S0区:0.5 M
- S1区:0.5 M
- 老年代:7 M
一共 10 M
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("end...");
}
设置堆内存大小与 OOM
设置堆内存大小的参数
- “-Xms”:用于表示堆区的起始内存,等价于-XX:InitialHeapSize。-X:是 JVM 的运行参数。ms:memory start
- “-Xmx”:用于表示堆区的最大内存,等价于-XX:MaxHeapSize。
一旦堆区中内存大小超过 “-Xmx” 所指定的最大内存是,将会抛出 OutOfMemoryError 异常。
通常 会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小,从而提供性能。
默认情况下,
- 初始内存大小:电脑物理内存大小 / 64
- 最大内存大小:电脑物理内存大小 / 4
public static void main(String[] args) {
// 返回 Java 虚拟机中堆内存总量(当前堆大小:初始化内存)
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回 Java 虚拟机试图使用的最大内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:" + initialMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
}
/*
-Xms:245M
-Xmx:3641M
系统内存大小为:15.3125G
系统内存大小为:14.22265625G
*/
查看设置参数
方法一:jstat -gc xxxx
- S0C:S0 区的最大内存量
- S0U:S0 区的当前使用内存量
- EC:伊甸园的最大内存量
- OC:老年代的最大内存量
S0 与 S1 在使用时,只会使用其中一个,所以只需要计算其中一个。
方法二:使用 +PrintGCDetails 参数
年轻代与老年代
JVM 中的 Java 对象可以根据生命周期长短划分成两类:
- 生命周期较短的对象,这类对象的创建和消亡非常迅速,朝生夕死。这类对象的个数占所有所有对象个数的 80%,有必要单独划出来一个区,单独处理。
- 另一类对象生命周期非常长,在某些极端情况下还能够与 JVM 的生命周期保持一致。这类对象如果每次 GC,都去判断,非常浪费性能。
下边这参数开发中一般不会调
配置新生代与老年代在堆结构中的占比
- 默认 -XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
- 可以修改 -XX:NewRatio=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
在 HotSpot 中,Eden 空间和另外两个 Survivor 空间缺省占比的比例是:8:1:1
可以通过“ -XX:SurvivorRation ” 调整这个空间的比例。
几乎所有 Java 对象都是在 Eden 区被 new
出来的。
可以使用 “-Xmn” 设置新生代最大内存大小:(一般使用默认值),如果 -Xmn 与 -XX:NewRatio=2 出现矛盾,那么以 -Xmn 为准,因为 -Xmn 明确指定新生代的内存大小。
如下图:最大堆空间为 600M。按照之前的说法:新生代与老年代是1:2,那老年代是 600 ∗ 2 3 = 400 600*\frac{2}{3}=400 600∗32=400 M,如下图没有问题。
而:Eden 空间和另外两个 Survivor 空间缺省占比的比例是:8:1:1,那么Eden 应该是: 200 ∗ 0.8 = 160 M 200*0.8=160M 200∗0.8=160M ,s0 和 s1 各占 20 M。
但真实情况:
- 老年代是 400 M 没有问题
- Eden 区是150M
- s0 和 s1 各占 25 M
- 此时的比例:6:1:1 ,与《Java 虚拟机规范》规定不一致,是因为 JVM 有自适应机制,如果要严格执行 8:1:1 ,需要手动指定:-XX:SurvivorRation=8
对象分配过程
为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存垃圾回收算法密切相关,所以还需要考虑 GC 执行完内存垃圾回收后是否在内存空间中产生内存碎片。
对象分配的一般过程
- new 的对象先放到伊甸园区。此区有大小限制。
- 当伊甸园区填满时,程序有需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
- 然后将伊甸园中的剩余对象移动到幸存者 0 区。
- 如果再次触发垃圾回收,上次幸存下来的放到幸存者 0 区的对象,如果没有回收,就会放到幸存者 1区,并清空幸存者 0 区。
- 如果再次经历垃圾回收,会重新将幸存者放回幸存者 0 区,接着再去幸存者 1 区。
- 啥时候能去养老区呢?可以设置次数:默认值是 15 次。
- 设置参数:-XX:MaxTenuringThreshold=16 进行设置
- 在养老区,相对悠闲。当养老区内存不足时,再次出发 GC:Major GC,进行养老区的内存清理。
- 若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常。
new 对象先放到伊甸园区,当伊甸园区满了,触发YGC。伊甸园区内的垃圾对象直接清除,不是垃圾对象迁移到 S0(to 区:S0 和 S1 哪个区为空,哪个是 to 区) 区,记录这些对象的生命值为 1。YGC 后,伊甸园区为空,S0 和 S1 某一个区为空。
当再次出发 YGC 时,S1 为空时 to 区,那么将伊甸园区和 S0 区的有效对象迁移到 S1 区(to 区),同事这些对象的生命值+1。在 YGC 时如果from 区中有对象失效了(比如对象 A),也需要清除。
这次 YGC 有些特殊,因为在 S1 区中有些对象的生命值到达 15(默认值)时,需要这些对象升级到老年代(对象H 和 D)。通过 -XX:MaxTenuringThreshold=16 进行修改进入老年代的阈值。
注意:
- 只有伊甸园区满时才会触发 YGC。S0 和 S1 区满时不会触发 YGC。
总结:
- 针对幸存者 S0、S1 区的总结:复制之后有交换,谁空谁是 to
- 关于垃圾回收:频繁在新生代收集,很少在老年代收集,几乎不在永久区(元空间)收集。
对象分配的特殊情况
特殊情况
- 在伊甸园区 YGC 后,空间还是不够存放一个实例对象,那么直接将该对象晋升到老年代。如果此时老年代空间也不够,那么触发 FGC,FGC 后如果老年代空间足够,直接将对象存储在老年代;如果空间依然不够,直接报 OOM 异常。
- 在YGC 时,由于伊甸园区的空间大于 to 区(S0 或者 S1) ,如果有一个对象要往 to 区迁移时, to 区没有足够的空间,那么将这个对象晋升老年代。
如下图:
-
在黄色框里,可以看到有 3 次 YGC。
-
红色框里时伊甸园区的使用情况,在 GC 前数据一直在增长,YGC 后伊甸园区被清空。
-
蓝色框里时幸运者区的使用情况,S1 和 S0 交替使用,交替被清空,在没有 YGC 数据是不变的。
-
蓝色框里时老年代的使用情况,在每次 YGC 后老年代的数据都在阶梯式增长。
根据跑代码实际的情况来看,是符合上边理论的分析的。
/**
* -Xms600m -Xmx600m
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) throws InterruptedException {
List<HeapInstanceTest> list = new ArrayList<>();
while (true) {
list.add(new HeapInstanceTest());
Thread.sleep(10);
}
}
}
GC
GC 按照回收区域分为两大类
- 不分收集
- 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集。
- 老年代收集(Manor GC / Old GC):只是老年代的垃圾收集。
- 目前,只有 CMS GC 会有单独收集老年代的行为
- 注意:很多时候 Major GC 会和 Full GC 混淆 ,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有 G1 GC 会有这种行为
- 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。
新生代GC(Minor GC)触发机制
- 当 Eden 代空间不足时,就会触发 Minor GC。Survivor 满不会引发 GC。
- 因为 Java 对象大多都具备朝生夕死的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
- Minor GC 会引发 STW(Stop The World),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
老生代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(Full GC)
- 如果 Major GC 后,内存还不足,就报 OOM
Full GC 触发机制
- 调用 System.gc() 时,系统建议执行 Full GC,但是不必然执行。
- 老年代空间不足
- 方法区空间不足
- 通过 Minor GC 后,进入老年代的平均大小大于老年代的可用内存。
- 有 Eden 区、from 区向 to 区复制时,对象大小大于 to 区可用内存,则把该对象转存到老年代,且老年代的可用内存大小小于该对象大小。
full GC 是开发或调优中尽量避免的
GC 日志分析:
- 发生了三次 YGC 后,才发生 Full GC。
- [PSYoungGen: 1875K->496K(2560K)] 表示:YGC 前的新生代占空空间 --> YGC 后的新生代占空空间 ( 新生代总空间 )
- Full GC,会对新生代、老年代、元空间进行垃圾收集。
- 最后一次 Full GC,从绿框看:Full GC 前与 Full GC 之后老年代内存空间几乎没有变化,所以新对象需要占用的空间大于老年代剩余空间,所以Full GC 之后报 OOM
/**
* -Xms9m -Xmx9m -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String str = "dyf";
while (true) {
list.add(str);
str += str;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.printf("遍历次数:" + i);
}
}
}
内存分配策略
针对不同年龄段的对象分配原则
- 优先分配到 Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。因为每次从 from 区向 to 区拷贝数据时,这些相同年龄的对象如果非常多,那么拷贝时非常耗性能。
- 空间分配担保
- -XX:HandlePromotionFailure
测试大对象之间进入老年代
下边代码:
- 堆空间为 60 M
- -XX:NewRatio=2:新生代占堆空间的 1/3,那么新生代空间为:20M;老年代空间为 40M
- -XX:SurvivorRatio-8:Eden : S0 : S1 = 8 : 1 : 1,那么Eden 区空间为:16M,S0 和 S1 区空间为: 2 M
- byte[] buffer 空间为 20M,Eden 和 S0 都放不下,符合直接晋升老年代的条件。
/**
* -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio-8 -XX:+PrintGCDetails
*/
public class YoungOldAreaTest {
public static void main(String[] args) {
// 20 m
byte[] buffer = new byte[1024 * 1024 * 20];
}
}
为对象分配内存:TLAB
TLAB 是(Thread Local Allocation Buffer)为每个线程分配一个缓存区。
TLAB 诞生的背景
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享的数据。
- 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
- 为了避免多个线程操作统一地址,需要使用加锁机制,进而影响分配速度。
什么是 TLAB
- 从内存模型而不是垃圾收集的角度,对 Eden 区继续划分,JVM 为每个线程分配一个私有缓存区域,它包含在 Eden 空间内。
- 多线程同事分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方案称之为快速分配策略
- 所有 OpenJDK 衍生出来的 JVM 都提供类 TLAB 的设计。
- 尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但JVM 确实是将 TLAB 作为内存分配的首选。
- 在程序中,开发人员可以通过选项 ”-XX:UseTLAB“ 设置是否开启 TLAB 空间,默认开启。
- 默认情况下,TLAB 空间的内存非常小,仅占整个 Eden 空间的 1%,当然我们可以通过选项:”-XX:TLABWasteTargetPercent“ 设置 TLAB 空间所占用 Eden 空间的百分比大小。
- 一旦对象在 TLAB 空间分配内存失败,JVM 就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
对象分配过程:TLAB
下图 ”Eden 分配“ 如果分配失败,会触发 YGC后,在进行分配,如果还是失败,会将对象晋升到老年代。
注意:由于 TLAB 的存在,堆空间不一定都是线程共享的。
堆空间的参数设置
官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html
- -XX:+PrintFlagsInitial:查看所有的参数的默认值
- -XX:+PrintFlagsFinal:查看所有参数的最终值(实际值:存在修改,不再是初始值)
- -Xms:初始堆空间内存(默认值为物理内存的 1 64 \frac{1}{64} 641)
- -Xmx:最大堆空间内存(默认值为物理内存的 1 4 \frac{1}{4} 41)
- -XX:NewRatio:配置新生代和老年代在堆结构的占比
- -XX:SurvivorRatio:配置新生代中 Eden 和 S0/S1 空间比例
- -XX:MaxTenuringThreshold:设置新生代的最大年龄
- -XX:+PrintGCDetails:输出详细的 GC 处理日志
- 打印 GC 简要信息
- -XX:+PrintGC
- -verbose:gc
- -XX:handlePromotionFailure:是否设置空间分配担保
NewRatio
NewRatio 设置如果比较的大,那么 Eden 区比较大。此时当出现 YGC 时,那些幸存下来的对象 S0/S1 放不下,会将大量的对象晋升到老年代,导致老年代空间占用较多,引起频繁 Full GC。
NewRatio 设置如果比较的小,那么 Eden 区太小。Eden 很容易满,引起频繁 YGC。
handlePromotionFailure
在发生 Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
- 如果大于,则此次 Minor GC 时安全的
- 如果小于,则虚拟机会查看 -XX:HandlePromotionFailure 设置只是否允许担保失败
- 如果 HandlePromotionFailure = true,那么会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小。
- 如果大于,则尝试进行一次 Minor GC,但这次 Minor GC 依然有失败的风险。
- 如果小于,则改为进行一次 Full GC
- 如果 HandlePromotionFailure = false,则改为进行一次 Full GC
- 如果 HandlePromotionFailure = true,那么会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小。
JDK 7 之后 HandlePromotionFailure 参数不会影响到虚拟机的空间分配担保策略(JDK7 HandlePromotionFailure 永远为 true)。JDK 7 之后:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。
堆是分配对象的唯一选择吗?
随着 JIT 编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么 ”绝对“ 了。
如果经过逃逸分析(Escape Analysis)后发现,一个对象bing没有逃逸出方法的话,那么就可能被优化成栈上分配。
基于 OpenJDK 深度定制的 TaoBaoVM,其中创新的 GCIH(GC invisible heap)技术实现 off-heap,将生命周期较长的 Java 对象从 heap 中移至 heap 外,并且 GC 不能管理 GCIH 内部的 Java 对象(法外之地),以此达到降低 GC 的回收频率和提升 GC 的回收效率的目的。
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域
- 当一个对象在方法中定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中定义后,它被外部方法引用,则认为发生了逃逸。例如:作为调用参数传递到其他方法中。
没有放生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。栈有自动过期(出栈)机制。栈帧线程私有,如果对象没有逃逸,那么它的作用域就在栈帧内部,不可能共享,可以随着栈帧而生,随着栈帧而亡。
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
// stringBuffer 对象发生逃逸,返回出去它作用域也就出方法区了
return stringBuffer;
}
public static String createStringBuffer2(String s1, String s2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
// stringBuffer 对象没有发生逃逸,方法执行完毕,stringBuffer 也就烟消云散了
return stringBuffer.toString();
}
// 判断是否发生逃逸,就看对象是否有可能在方法外调用(是不是方法私有)
public class EscapeAnalysis {
// 这个实体不属于某个方法私有,它不可能放到栈帧里
public EscapeAnalysis obj;
// 方法返回 EscapeAnalysis 对象,发生逃逸
public EscapeAnalysis getInstance() {
return obj == null ? new EscapeAnalysis() : obj;
}
// 为成员变量赋值,发生逃逸
public void setObj() {
this.obj = new EscapeAnalysis();
}
// 对象的作用域仅在当前方法中有效,没有发生逃逸
public void useEscapeAnalysis() {
EscapeAnalysis e = new EscapeAnalysis();
}
// 引用成员变量的值,发生逃逸
public void useEscapeAnalysis1() {
EscapeAnalysis e = getInstance();
}
}
JDK 7 以后 HotSpot 中默认就已经开启了逃逸分析。
结论:开发中能使用局部变量的,就不要定义在方法外。
对于没有逃逸对象进行代码优化
- 栈上分配。将对分配转化为栈分配
- 同步省略。如果发现一个对象只能被一个线程访问到,那么对于这个对象的操作可以不考虑同步了。
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在,也可以被访问到,那么对象的部分(或者全部)可以不存储在内存,而是存储 Java 栈中。
栈上分配测试
当不使用栈上分配时,使用了4G堆空间,调用 100000000 次方发生了 YGC,YGC 之后堆中有 User 对象:34306167,程序耗时:701ms。
当使用栈上分配时,使用了1G堆空间,调用 100000000 次方没发生了 YGC,内存中有 User 对象:84729,程序耗时:6ms。
标量替换
标量(Scalar)是指一个无法在分解成更小的数据。Java 中的原始数据类型就是标量。
相对标量,那些可以分解的数据叫聚合量。Java 对象就是聚合量。
在 JIT 阶段,经过逃逸分析,发现一个不逃逸对象,经过 JIT 优化,就会把这个对象拆解成若干个标量来替换成员变量,这个过程就是标量替换
public static void alloc1() {
// point 对象没有发生逃逸
Point point = new Point(1, 2);
System.out.println("point.x=" + point.x + ";point.y=" + point.y);
}
static class Point {
public int x;
public int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
上边代码经过标量替换后。
这样可以大大减少堆内存的占用,标量替换为栈上分配提供了很好的基础。
// x 和 y 存在栈帧的局部变量表中
public static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x=" + x + ";point.y=" + y);
}
参数:-XX:+EliminateAllocations 开启标量替换(默认打开),允许将对象打散分配在栈上。
标量替换代码测试
未打开标量替换
- 大量 YGC
- 耗时长:438ms
打开标量替换
- 没有 GC
- 耗时短:5ms
逃逸分析小结:逃逸分析并不成熟
无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,Oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。