赞
踩
在JVM中,堆是各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。一个JVM实例只存在一个堆内存,我们创建类HeapDemo和HeapDemo1说明一个JVM实例只有一个堆内存,代码如下所示:
上面的代码很简单,启动程序,使程序等待1000秒。两段代码的内存大小设置是不一样的:HeapDemo使用参数Xms和Xmx设置内存大小为10MB;HeapDemo1使用参数Xms和Xmx设置内存大小为20MB。这里大家知道参数Xms和Xmx是用来设置内存大小即可,后面我们会详细介绍。
启动完程序之后,此时两个Java程序对应两个JVM实例,对应的进程id(下图中pid)分别是111348和101564,通过JDK自带工具jvisualvm来查看程序的堆空间,,如下图所示:
从上图中可以看到两个应用程序对应不同的堆空间分配,各自对应的堆内存分别是10MB和20MB,也对应上了参数Xms和Xmx的设置。上述试验结果说明,每个应用程序对应唯一的堆空间,即每个JVM实例对应唯一的堆空间。
如图下图所示:
堆也是Java内存管理的核心区域。堆在JVM启动的时候被创建,其空间大小也随之被确定。堆是JVM管理的最大一块内存空间,其大小是可以根据参数调节的,它可以处于物理上不连续的内存空间中,但在逻辑上应该被视为连续的。
堆中存放的是对象,栈帧中保存的是对象引用,这个引用指向对象在堆中的位置。下面的案例用于说明栈和堆之间的关系,如代码清单如下所示:
如下图所示:
展示了Java栈和堆之间的关系。Java栈中的s1和s2分别是堆中s1实例和s2实例的引用。
代码中的main()方法与字节码对应的关系如下图所示:
比如new指令用来开辟堆空间,创建对象。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆也是GC(Garbage Collector,垃圾收集器)执行垃圾回收的重点区域。现代垃圾收集器大部分都基于分代收集理论设计,这是因为堆内存也是分代划分区域的,堆内存分为新生代(又叫年轻代)和老年代。
堆内存区域规划如下图所示:
Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了,可以通过JVM参数“-Xms”和“-Xmx”来进行设置。
Intellij IDEA中参数设置步骤如下图所示:
一旦堆区中的内存大小超过“-Xmx”所指定的最大内存,将会抛出内存溢出异常(OutOfMemoryError,OOM)。
通常会将“-Xms”和“-Xmx”两个参数配置相同的值。否则,服务器在运行过程中,堆空间会不断地扩容与回缩,势必形成不必要的系统压力。所以在线上生产环境中,JVM的Xms和Xmx设置成同样大小,避免在GC后调整堆大小时带来的额外压力。
初始内存大小占据物理内存大小的1/64。最大内存大小占据物理内存大小的1/4。下面我们通过代码演示查看堆区的默认配置大小,如代码清单如下所示:
/**
* @title HeapSpaceInitial
* @description 1.设置堆空间大小的参数
* -Xms 用来设置堆空间(新生代+老年代)的初始内存大小
* -X 是JVM的运行参数
* ms 是memory start
* -Xms 用来设置堆空间(新生代+老年代)的最大内存大小
* 2.默认堆空间大小
* 初始内存大小占据物理内存大小的1/64
* 最大内存大小占据物理内存大小的1/4
* 3.手动设置:-Xms600m -Xmx600m
* 开发中建议将初始堆内存和最大的堆内存设置成相同的值
* 4.查看设置的参数:
* 方式一:jps/jstat -gc 进程 id
* 方式二:-XX:+PrintGCDetails
* @author: yangyongbing
* @date: 2024/3/4 12:04
*/
public class HeapSpaceInitial {
public static void main(String[] args) {
// 返回JVM中的堆内存总量
long initMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回JVM试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:"+initMemory+"M");
System.out.println("-Xmx:"+maxMemory+"M");
System.out.println("系统内存大小为:"+initMemory*64.0/1024+"G");
System.out.println("系统内存大小为:"+initMemory*4.0/1024+"G");
// try {
// Thread.sleep(1000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
代码输出结果如下图所示:
由上图可知,最终计算系统内存大小不到16GB,这是因为内存的制造厂商会有不同的进制计算方式,比如现实是以1000进制计算GB,而计算机中是以1024进制计算,这样会出现一定的误差。
上面案例讲解了默认的内存参数配置原则,那么通过-Xms和-Xmx手动配置了参数之后,程序运行起来后如何查看设置的参数呢?继续以上面的代码为例,解开sleep相关代码的注释,运行代码,然后通过以下两种方式查看设置的参数明细。
(1)按Windows+R键打开命令行,输入“jps”查看进程id,再输入“jstat -gc进程id”命令查看参数配置,如下图所示:
通过上图我们可以看到jstat –gc命令下的结果有很多参数选项,下面我们解释其中几个参数的含义:
通过计算可以得到堆内存中的大小,S0C加上S1C、EC、OC的大小,正好就是600MB。
(2)在IDEA中进行设置VM options为“-XX:+PrintGCDetails”,参数配置如下图所示:
输出-Xms和-Xmx结果为575M,这是因为只计算了一个From区,另外一个To区没有参与内存的计算。从结果可以得到各个区域的大小,Eden区大小为153600K,From区大小为25600K,To区大小为25600K,Old区大小为409600K。两种方式查看的结果是一样的。
在堆内存区域最容易出现的问题就是OOM,下面我们通过如下代码演示OOM:
import java.util.ArrayList;
import java.util.Random;
/**
* @title OOMTest
* @description 堆内存溢出
* -Xms600m -Xmx600m
* @author: yangyongbing
* @date: 2024/3/4 12:37
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while (true) {
// try {
// Thread.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture {
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
}
代码输出结果如下图所示,可以看到程序已经发生了OOM异常。
使用JDK自带工具jvisualvm查看堆内存中占用较大内存的对象为byte[]数组,可以判断是由于byte数组过大导致的内存溢出,如下图所示:
关于内存溢出的情况还有很多种,我们会在后面的章节中一一介绍,并且会给出对应的解决方案,当前大家对堆内存溢出有一个大致的认识即可。
存储在JVM中的Java对象可以被划分为两类,分别是生命周期较短的对象和生命周期较长的对象:
Java堆区分为新生代和老年代,生命周期较短的对象一般放在新生代,生命周期较长的对象会进入老年代。在堆内存中新生代和老年代的所占用的比例分别是多少呢?新生代与老年代在堆结构的占比可以通过参数“-XX:NewRatio”配置。默认设置是“-XX:NewRatio=2”,表示新生代占比为1,老年代占比为2,即新生代占整个堆的1/3,如下图所示:
可以修改“-XX:NewRatio=4”,表示新生代占比为1,老年代占比为4,新生代占整个堆的1/5。
下面我们通过代码演示堆区新生代和老年代的比例,如代码清单如下所示:
/**
* @title EdenSurivorTest
* @description -Xms600m -Xmx600m
* -XX:NewRation: 设置新生代与老年代的比例。默认值是2.
* @author: yangyongbing
* @date: 2024/3/4 13:04
*/
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println(" 我只是来打个酱油~");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
堆内存大小设置为600M,设置新生代与老年代的比例为1:4,如下图所示:
运行时堆空间新生代和老年代所占比例如下图所示:
可以看到新生代内存大小为120M,老年代内存大小为480M,比例为1:4。这里需要注意的是,在Eden Space后面的数据包括119M和90M两个,其中119M指的是Eden区的最大容量,90M指的是初始化容量,一般计算的时候以90M为准,下面的Survivor区同理。
在HotSpot虚拟机中,新生代又分为一个Eden区和两个Survivor区,这三块区域在新生代中的占比也是可以通过参数设置的。Eden区和两个Survivor区默认所占的比例是8:1:1。但是大家查看上图的时候发现Eden区和两个Survivor区默认所占的比例为6:1:1,
这是因为JDK8的自适应大小策略导致的,JDK8默认使用UseParallelGC垃圾回收器,该垃圾回收器默认启动参数AdaptiveSizePolicy,该参数会根据垃圾收集的情况自动计算Eden区和两个Survivor区的大小。使用UseParallelGC垃圾回收器的情况下,如果想看到Eden区和两个Survivor区的比例为8:1:1的话,只能通过参数“-XX:SurvivorRatio”手动设置为8:1:1,或者直接使用CMS垃圾收集器。
参数“-XX:SurvivorRatio”可以设置Eden区和两个Survivor区比例。比如“-XX:SurvivorRatio=3”表示Eden区和两个Survivor区所占的比例是3:1:1。下面通过代码演示设置Eden区和两个Survivor区的比例,如代码清单如下所示:
设置Eden区和两个Survivor区的比例为3:1:1,如下图所示:
运行时Eden区和另外两个Survivor区的大小分别是120M和40M,所占比例为3:1:1,如下图所示:
IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的,表明大部分对象的产生和销毁都在新生代完成。所以某些情况下可以使用参数“-Xmn”设置新生代的最大内存来提高程序执行效率,一般来说这个参数使用默认值就可以了。
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后,是否会在内存空间中产生内存碎片。内存具体分配过程有如下步骤:
S0区、S1区之所以也被称为From区和To区,是因为对象总是从某个Survivor(From)区转移至另一个Survivor(To)区。正常来说,垃圾回收频率应该是频繁在新生代收集,很少在老年代收集,几乎不在永久代/元空间(方法区的具体体现)收集。对象在堆空间分配的流程,如下图所示,注意图中我们用YGC表示Young GC,FGC表示Full GC:
下面我们通过代码演示Eden区和两个Survivor区的变化规律,如代码清单如下所示:
当程序运行起来后可以用jvisualvm工具进行查看,如下图所示:
可以看出新生代的Eden区达到上限的时候进行了一次Minor GC,将没有被回收的数据存放在S1区,当再次进行垃圾回收的时候,将Eden区和S1区没有被回收的数据存放在S0区。老年代则是在每次垃圾回收的时候,将S0区或S1区储存不完的数据存放在老年代,如上图中Old Gen区域所示,当每次垃圾进行回收后老年代的数据就会增加,增加到老年代的数据存不下的时候将会进行Major GC,进行垃圾回收之后发现老年代还是存不下的时候就会抛出OOM异常,如下图所示:
JVM在进行GC时,并非每次都对上面三个内存区域(新生代、老年代和方法区)一起回收,大部分时候回收的都是新生代。
在HotSpot VM中,GC按照回收区域分为两种类型,分别是部分GC(Partial GC)和整堆GC(Full GC)。部分GC是指不完整收集整个Java堆,又细分为新生代GC、老年代GC和混合GC:
知道GC的分类后,什么时候触发GC呢?
新生代GC(Minor GC)触发机制如下:
老年代GC(Major GC/Old GC)触发机制如下:
Full GC触发机制有如下5种情况:
Full GC是开发或调优中尽量要避免的,这样暂停时间会短一些。
在数据的执行过程中,先把数据存放到Eden区,当Eden区空间不足时,进行新生代GC把数据存放到Survivor区。当新生代空间不足时,再把数据存放到老年代,当老年代空间不足时就会触发OOM。下面我们通过代码演示GC的情况,如代码清单如下所示:
运行结果如下图所示:
上图展示的是GC日志信息,后面的会专门详细介绍如何看懂GC日志,各位稍安勿躁。目前大家知道第一个标记框中的GC表示新生代GC,第二个和第三个表示整堆GC,最后一个标记框表示出现了OOM现象即可。
上图中可以看出程序先经历了新生代GC,后经历了整堆GC再抛出OOM。OOM之前肯定要经历一次整堆GC,当老年代空间不足时首先进行一次垃圾回收,当垃圾回收之后仍然空间不足才会报OOM。
为什么需要把Java堆分代?不分代就不能正常工作了吗?经研究,不同的对象生命周期不同。70%~99%的对象是临时对象。其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就需要对堆的所有区域进行扫描。而很多对象都是“朝生夕死”的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大空间。
如果对象在Eden区出生,并经过第一次MinorGC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor区中,并将对象年龄设为1。对象在Survivor区中每经过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度时(默认为15岁,其实每个JVM、每个GC都有所不同),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过“-XX:Max TenuringThreshold”来设置,也会有其他情况直接分配对象到老年代。对象分配策略如下所示:
下面我们通过代码演示大对象直接进入老年代的情景,如代码清单如下所示:
/**
* @title YoungOldAreaTest
* @description 测试:大对象直接进入老年代
* -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* @author: yangyongbing
* @date: 2024/3/4 17:00
*/
public class YoungOldAreaTest {
public static void main(String[] args) {
byte[] buffer=new byte[1024*1024*20]; //20M
}
}
结果如下图所示。20M的数据出现在ParOldGen区也就是老年代,说明大对象在Eden区存不下,直接分配到老年代。
程序中所有的线程共享Java中的堆区域,但是堆中还有一部分区域是线程私有,这部分区域称为线程本地分配缓存区(Thread Local Allocation Buffer,TLAB)。
TLAB表示JVM为每个线程分配了一个私有缓存区域,这块缓存区域包含在Eden区内。简单说TLAB就是在堆内存中的Eden区分配了一块线程私有的内存区域。什么是TLAB呢?
为什么有TLAB呢?原因如下:
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。在程序中,开发人员可以通过选项“-XX:+/-UseTLAB”设置是否开启TLAB空间。下面我们通过代码演示“-XX:UseTLAB”参数的设置,如代码清单如下所示:
/**
* @title TLABArgsTest
* @description 测试 -XX:+/-UseTLAB 参数是否开启的情况:默认情况是开启的
* @author: yangyongbing
* @date: 2024/3/4 17:37
*/
public class TLABArgsTest {
public static void main(String[] args) {
System.out.println(" 我只是来打个酱油~ ");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果如下图所示,通过jinfo命令查看参数是否设置,UseTLAB前面如果有“+”号,证明TLAB是开启状态。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden区的1%,我们可以通过选项“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden区的百分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden区中分配内存。加上了TLAB之后的对象分配过程如下图所示:
前面讲到了堆空间中几个参数对内存的影响,比如Xms和Xmx用来设置堆内存的大小,此外还有很多其他的参数。下面解说几个常用的参数设置:
参数HandlePromotionFailure设置策略如下:
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的。
在JDK6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察Open JDK中的源码变化,虽然源码中还定义了HandlePromotion Failure参数,但是在代码中已经不会再使用它。JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:“随着Java语言的发展,现在已经能看到有些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日益强大,栈上分配、标量替换等优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不那么绝对了。”
在JVM中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法,那么就可能被优化成栈上分配。这样就无须在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
此外,前面提到的基于Open JDK深度定制的TaoBaoVM,其中创新的GCIH(GC Invisible Heap)技术实现off-heap,将生命周期较长的Java对象从Heap中移至Heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC回收频率和提升GC回收效率的目的。
前面我们提到了对象经过逃逸分析,有可能把对象分配到栈上。也就是说如果将对象分配到栈,需要使用逃逸分析手段。
逃逸分析是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java HotSpot编译器能够分析出一个新对象引用的使用范围,从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象的动态作用域。
当一个对象在方法中被定义后,若对象只在方法内部使用,则认为没有发生逃逸。
当一个对象在方法中被定义后,若它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
下面我们通过伪代码演示没有发生逃逸的对象,如代码清如下所示:
代码示例中的对象V的作用域只在method()方法区内,若没有发生逃逸,则可以分配到栈上,随着方法执行的结束,栈空间就被移除了。
下面我们通过代码演示发生逃逸的对象,如代码清单如下所示:
如果想让上述代码中的StringBuffer sb不发生逃逸,可以参考代码清单如下所示的方法:
下面的代码清单展示了不同情景的逃逸分析:
/**
* @title EscapeAnalysis
* @description 逃逸分析
* 如何快速地判断是否发生了逃逸分析,就看new的对象实体是否有可能在方法外被调用
* @author: yangyongbing
* @date: 2024/3/5 7:32
*/
public class EscapeAnalysis {
public EscapeAnalysis obj;
// 方法返回EscapeAnalysis对象,发生逃逸
public EscapeAnalysis getInstance(){
return obj==null?new EscapeAnalysis():obj;
}
// 为成员属性赋值,发生逃逸
public void setObj(){
this.obj=new EscapeAnalysis();
}
// 如果当前的obj引用声明为static类型,是否会发生逃逸?答案是会发生逃逸
// 对象的作用域仅在当前方法中有效,没有发生逃逸
public void useEscapeAnalysis(){
EscapeAnalysis e=new EscapeAnalysis();
}
public void useEscapeAnalysis1(){
EscapeAnalysis e=getInstance();
// getInstance().xxx() 同样会发生逃逸
}
}
在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。如果使用的是较早的版本,开发人员则可以通过以下参数来设置逃逸分析的相关信息:
一般在开发中能使用局部变量的,就不要使用在方法外定义。
使用逃逸分析,编译器可以对程序做如下优化:
JIT(Just In Time)编译器在编译期间根据逃逸分析的结果,发现如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。常见的栈上分配场景如代码清单如下所示,展示了栈上分配节省存储空间的效果:
未开启逃逸分析时创建1000万个User对象花费的时间为200ms,如图下图所示:
未开启逃逸分析时User对象的实例数为1000万个,如下图所示:
把“-XX:-DoEscapeAnalysis”的“-”号改成“+”号,就意味着开启了逃逸分析。开启逃逸分析后花费的时间为18ms,时间比之前少了很多,如下图所示:
开启逃逸分析后当前内存中有51090个对象,内存中不再维护1000万个对象,如下图所示:
线程同步的代价是相当高的,同步的后果是降低了并发性和性能。在动态编译同步块的时候,JIT编译器可以借助逃逸分析,来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步,这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。如下代码清单展示了同步省略效果:
代码中对hollis对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程访问,所以在JIT编译阶段就会被优化掉。优化后的代码如下代码清单所示:
当代码中对hollis这个对象进行加锁时的字节码文件如下图所示:
同步省略是将字节码文件加载到内存之后才进行的,所以当我们查看字节码文件的时候仍然能看到synchronized的身影,在字节码文件中体现为monitorenter和monitorexit,如上图中标记框所示。
标量(Scalar)是指一个无法再分解成更小数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫作聚合量(Aggregate),Java中的对象就是聚合量,因为它可以分解成其他聚合量和标量。
在JIT编译器的编译阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个成员变量。这个过程就是标量替换。
如下代码清单展示了标量替换效果:
以上代码经过标量替换后,就会变成如下效果:
可以看到,point这个聚合量经过逃逸分析后,并没有逃逸就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。标量替换为栈上分配提供了很好的基础。
通过参数-XX:+EliminateAllocations可以开启标量替换(默认打开),允许将对象打散分配在栈上。如下代码清单展示了标量替换之后对性能的优化效果:
使用如下参数运行上述代码:
-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis
-XX:+PrintGC -XX:+EliminateAllocations
使用参数说明如下:
当未开启标量替换时,“-XX:-EliminateAllocations”设置为“-”号,程序运行花费的时间如下图所示:
当开启标量替换时,“-XX:+EliminateAllocations”设置为“+”号,程序运行花费的时间如下图所示:
开启标量替换时明显可以看出代码运行的时间减少了很多,同时也没有发生GC操作。
上述代码在主函数中调用了1亿次alloc()方法。调用alloc()方法的时候创建对象,每个User对象实例需要占据约16字节的空间,因此调用1亿次alloc()方法总共需要大约1.5GB内存空间。如果堆空间小于这个值,就必然会发生GC,所以当堆空间设置为100MB并且关闭标量替换的时候,发生了GC。
关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟。
其根本原因就是无法保证逃逸分析的性能收益一定能高于它的消耗。虽然经过逃逸分析可以做标量替换、栈上分配和锁消除,但是逃逸分析自身也需要进行一系列复杂分析,这其实也是一个相对耗时的过程。一个极端的例子就是经过逃逸分析之后,发现所有对象都是逃逸的,那这个逃逸分析的过程就白白浪费掉了。
虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。有一些观点认为,通过逃逸分析JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。Oracle Hotspot JVM中并未实现栈上分配,上面案例测试的效果都是基于标量替换实现的,这一点在逃逸分析相关的文档里已经说明,对象被标量替换以后便不再是对象了,所以可以明确所有的对象实例都创建在堆上。
目前很多书籍还是基于JDK 7以前的版本。JDK 8和之后的版本中内存分配已经发生了很大变化。比如intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元空间取代。但是JDK 8和之后的版本中intern字符串缓存和静态变量并不是被转移到元空间,而是直接在堆上分配。所以这一点同样符合前面的结论:对象实例都是分配在堆上。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。