【JVM】JVM内存结构之——堆内存细节(堆内参数设置/ GC分类/ GC日志分析/ TLAB/ 内存逃逸/ 堆空间常见参数)

追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

导读:本篇文章讲解 【JVM】JVM内存结构之——堆内存细节(堆内参数设置/ GC分类/ GC日志分析/ TLAB/ 内存逃逸/ 堆空间常见参数),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文


1. 堆内存细节划分

Java7 及之前堆内存逻辑上分为三部分:新生区+老年代区+永久区
Java8 及之前堆内存逻辑上分为三部分:新生区+老年代区+元空间

新生代:(eden(伊甸园)+from(s0)+to(s1))
老年代
注意:JDK8开始永久代被改为元空间。
新生代中有会分配 s0(from) s1(to) 区 空间是相等的。

YoungGen(新生代)
oldGen(老年代)
s0(from)
s1(to)
PermGen(永久代)
Metaspace(元空间) GC 日志 Young—-新生代 old 老年代 JDK8 Metaspace元空间
在这里插入图片描述
在这里插入图片描述

2. 堆内存参数设置

1.Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项”-Xms”和”-Xmx”来进行设置。
2.-Xms用于表示堆区的起始内存 、-Xmx则用于表示堆区的最大内存
3.堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutofMemoryError异常。
4.通常会将-Xms和-Xmx两个参数配置相同的值
原因:频繁的扩容和释放造成不必要的压力,避免在GC之后调整堆内存给服务器带来压力。
如果两个设置一样的就少了频繁扩容和缩容的步骤。内存不够了就直接报OOM
默认情况下:
初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4

  1. 设置堆空间大小的参数 -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
    -X 是jvm的运行参数
    ms 是memory start -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小

  2. 默认堆空间的大小
    初始内存大小:物理电脑内存大小 / 64
    最大内存大小:物理电脑内存大小 / 4

  3. 手动设置:-Xms300m -Xmx300m
    开发中建议将初始堆内存和最大的堆内存设置成相同的值。

  4. 查看设置的参数:方式一: jps / jstat -gc 进程id
    方式二:-XX:+PrintGCDetails

//-Xms1m -Xmx300m -XX:+PrintGCDetails
while (true) {
    byte[] bytes = new byte[1024 * 1024 * 10];
    Thread.sleep(1000);
}

相关代码:

public class Test02 {
    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");

        try {
            Thread.sleep(3000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

3. 查看堆内存情况

  1. Jps 查看当前系统中有哪些Java进程
    在配合Jmap工具 查看堆内存占用情况 jmap -heap 进程id
  2. 图形化界面 Jvisualvm 或者是 jconsole.exe

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 314572800 (300.0MB) ##(最大堆内存)
NewSize = 104857600 (100.0MB)##新生代
MaxNewSize = 104857600 (100.0MB)##最大新生代内存
OldSize = 209715200 (200.0MB)##老年代内存
NewRatio = 2 ### NewRatio值就是设置老年代的占比,剩下的1给新生代
SurvivorRatio = 8 ##设置甸园区(Eden区)s0 s1 占比
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB##元空间
G1HeapRegionSize = 0 (0.0MB)

备注:

Heap Usage:
PS Young Generation
Eden Space:(伊甸园)
capacity = 78643200 (75.0MB)
used = 6301024 (6.009124755859375MB)
free = 72342176 (68.99087524414062MB)
8.012166341145834% used
From Space:(s0)
capacity = 13107200 (12.5MB)
used = 0 (0.0MB)
free = 13107200 (12.5MB)
0.0% used
To Space:(s1)
capacity = 13107200 (12.5MB)
used = 0 (0.0MB)
free = 13107200 (12.5MB)
0.0% used
PS Old Generation(老年代)
capacity = 209715200 (200.0MB)
used = 0 (0.0MB)
free = 209715200 (200.0MB)
0.0% used

1718 interned Strings occupying 155888 bytes.

4. -XX:+PrintGCDetails

输出gc回收的日志信息
-Xms300m -Xmx300m -XX:+PrintGCDetails

5. 新生代/老年代比例参数

1.在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8 : 1 : 1,
2.当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。比如-XX:SurvivorRatio=8
几乎所有的Java对象都是在Eden区被new出来的。
3.绝大部分的Java对象的销毁都在新生代进行了(有些大的对象在Eden区无法存储时候,将直接进入老年代),IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
4.可以使用选项”-Xmn”设置新生代最大内存大小,但这个参数一般使用默认值就可以了。

5.1 -XX:NewRatio

设置新生代比例参数:

配置年轻代与老年代在堆结构的占比
默认
-XX:NewRatio=2新生代占1,老年代占2,年轻代占整个堆的1/3
例如:
-XX:NewRatio=4新生代占1,老年代占4,年轻代占整个堆的1/5
NewRatio值就是设置老年代的占比,剩下的1给新生代
在这里插入图片描述
在默认的情况下 新生代与老年代比例 1:2

5.2 -XX:SurvivorRatio

新生代中可以分为伊甸园区(Eden区),From Survivor 区 (S0区)和 To Survivor 区 (S1区)。 占用的空间分别默认为 8:1:1

设置新生代中eden和S0/S1空间的比例
默认
-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1
假如
-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1
SurvivorRatio值就是设置Eden区的比例占多少,S0/S1相同

6. Stop the World机制

所谓的Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行

7. GC的分类

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)

部分收集(Partial GC)

  1. 新生代收集(Minor GC/Young GC):只是新生代(Eden,s0,s1)的垃圾收集
  2. 老年代收集(Major GC/Old GC):只是老年代的圾收集。
    目前,只有CMS GC会有单独收集老年代的行为。

混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为

整堆收集(Full GC):收集整个java堆和方法区(元空间)的垃圾收集。

7.1 年轻代 (Young)GC(Minor GC)触发机制

1.当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满。Survivor满不会主动引发GC,在Eden区满的时候,会顺带触发s0区的GC,也就是被动触发GC(每次Minor GC会清理年轻代的内存)
2.因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
3.Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

演示:-Xms300m -Xmx300m -XX:+PrintGCDetails

相关代码:

public static void main(String[] args) throws InterruptedException {
    byte[] bytes1 = new byte[1024 * 1024 * 25];
    Thread.sleep(5000);
    byte[] bytes2 = new byte[1024 * 1024 * 25];
    Thread.sleep(5000);
    byte[] bytes3 = new byte[1024 * 1024 * 25];
    Thread.sleep(3000000);
}

[GC (Allocation Failure) [PSYoungGen: 57353K->927K(89600K)] 57353K->52135K(294400K), 0.0199434 secs] [Times: user=0.20 sys=0.00, real=0.02 secs]

7.2 Full GC/MajorGC

7.2.1 MajorGC

1.指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了
出现了MajorGc,经常会伴随至少一次的Minor GC。(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
2.老年代空间不足时,会先尝试触发Minor GC(新生代),如果之后空间还不足,则触发Major GC
Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
如果Major GC后,内存还不足,就报OOM了

7.2.2 Full GC 触发机制

Full GC 触发机制
1.调用System.gc()时,系统建议执行FullGC,但是不必然执行
2.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:Full GC 是开发或调优中尽量要避免的。这样STW时间会短一些

7.3.3 大对象直接晋升老年代

演示:-Xms300m -Xmx300m -XX:+PrintGCDetails
相关代码:

//-Xms300m -Xmx300m -XX:+PrintGCDetails
byte[] bytes1 = new byte[1024 * 1024 * 80];
Thread.sleep(3000000);

在这里插入图片描述
老年代堆内存满了,触发fullGC

//-Xms300m -Xmx300m -XX:+PrintGCDetails
byte[] bytes1 = new byte[1024 * 1024 * 80];
Thread.sleep(5000);
byte[] bytes2 = new byte[1024 * 1024 * 130];
Thread.sleep(3000000);

在这里插入图片描述
触发了 fullGC 新生代 老年代 元空间
在这里插入图片描述

新生代GC回收
在这里插入图片描述
当老年代满时 触发 full GC
堆溢出:
当老年代满时 触发 full GC 如果清理完毕之后 还是没有足够的空间存放
则报错OOM异常

8. GC日志的分析

-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2021-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XX:+PrintGCApplicationStoppedTime // 输出GC造成应用暂停的时间
-Xloggc:…/logs/gc.log 日志文件的输出路径

相关代码:

public static void main(String[] args) throws InterruptedException {
    //-Xms300m -Xmx300m -XX:+PrintGCDetails
    byte[] bytes1 = new byte[1024 * 1024 * 25];
    //年轻代总空间为 89600K 当前占用62592K 回收后 2629K
    Thread.sleep(3000000);
}

在这里插入图片描述

6.1 新生代GC日志分析(PSYoungGen)

[GC (System.gc()) [PSYoungGen: 62597K->2685K(89600K)] 62597K->28309K(294400K), 0.0260174 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[Full GC (System.gc()) [PSYoungGen: 2685K->0K(89600K)] [ParOldGen: 25624K->27986K(204800K)] 28309K->27986K(294400K), [Metaspace: 9732K->9732K(1058816K)], 0.0114928 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

触发系统GC PSYoungGen(新生代GC)

新生代占用堆内存回收前:62597K/1024=61.1m
新生代占用堆内存回收后:2685K=2.6m
新生代占用堆内存大小:89600K/1024=87.5 m

62597K->28309K(294400K)
触发GC回收前:
堆内存整个使用62597K/1024=61.1m
触发GC回收后:
堆内存整个使用28309K/1024=27.5
堆内存使用:294400K/1024=287.5 (新生代eden+to/form)+老年代

0.0260174 secs] [Times: user=0.00 sys=0.00, real=0.03 secs

GC回收的时间:整个GC花费的时间 0.0260174 secs(单位是/s)

user:指的是CPU工作在用户态所花费的时间;
real:指的是在此次GC事件中所花费的总时间;
sys:指的是CPU工作在内核态所花费的时间。

6.2 Full GC 日志分析

[Full GC (System.gc()) [PSYoungGen: 2565K->0K(89600K)] [ParOldGen: 25616K->27930K(204800K)] 28181K->27930K(294400K), [Metaspace: 9379K->9379K(1058816K)], 0.0140295 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

PSYoungGen: 2565K->0K(89600K)]
新生代GC回收前占用2565K/1024=2.5mb
新生代堆内存大小89600K/1024=87.5
ParOldGen
老年代GC回收前占用堆内存25616K/1024=25mb
老年代GC回收后占用堆内存27930K/1024=28mb
老年代占用堆内存大小204800K/1024=200mb

Full GC回收前 整个堆内存使用28181K/1024=27.5
Full GC回收后 整个堆内存使用27930K/1024=27.27
整个堆内存大小:294400K

元空间大小
垃圾回收前 元空间占用9379K
垃圾回收后 元空间占用9379K
垃圾回收后 元空间内存1058816K
Full GC执行时间:1058816K

6.3 GC日志的分析工具

C Easy是一款在线的可视化工具,易用、功能强大
网站:https://gceasy.io/

配置GC日志参数:
-Xms300m -Xmx300m -XX:+PrintGCDetails -Xloggc:D:\logs\log.log

测试代码:

//-Xms300m -Xmx300m -XX:+PrintGCDetails  -Xloggc:D:\logs\log.log
byte[] bytes1 = new byte[1024 * 1024 * 25];
//年轻代总空间为 89600K 当前占用62592K 回收后 2629K
Thread.sleep(3000000);

在这里插入图片描述

9. 为什么堆会分代?

为什么要把Java堆分代?不分代就不能正常工作了吗?经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
新生代:有Eden、两块大小相同的survivor(又称为from/to或s0/s1)构成,to总为空。
老年代:存放新生代中经历多次GC仍然存活的对象。

10. TLAB(Thread Local Allocation Buffer)

TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独享的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。

TLAB属于Eden区域中的内存,不同线程的TLAB都位于Eden区,Eden区对所有的线程都是可见的。每个线程的TLAB有内存区间,在分配的时候只在这个区间分配。

1.堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
2.由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
3.为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

特点:
TLAB解决了:直接在线程共享堆上安全分配带来的线程同步性能消耗问题(解决了指针碰撞)。
TLAB内存空间位于Eden区。
默认TLAB大小为占用Eden Space的1%。
在这里插入图片描述
1.也就是说,虽然每个线程在初始化时都会去堆内存中申请一块TLAB,并不是说这个TLAB区域的内存其他线程就完全无法访问了,其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。

2.并且,在TLAB分配之后,并不影响对象的移动和回收,也就是说,虽然对象刚开始可能通过TLAB分配内存,存放在Eden区,但是还是会被垃圾回收或者被移到Survivor Space、Old Gen等。

3.还有一点需要注意的是,我们说TLAB是在eden区分配的,因为eden区域本身就不太大,而且TLAB空间的内存也非常小,默认情况下仅占有整个Eden空间的1%。所以,必然存在一些大对象是无法在TLAB直接分配。

4.遇到TLAB中无法分配的大对象,对象还是可能在eden区或者老年代等进行分配的,但是这种分配就需要进行同步控制,这也是为什么我们经常说:小的对象比大的对象分配起来更加高效。

10.1 TLAB 带来的问题

1.虽然在一定程度上,TLAB大大的提升了对象的分配速度,但是TLAB并不是就没有任何问题的。
2.比如一个线程的TLAB空间有100KB,其中已经使用了80KB,当需要再分配一个30KB的对象时,就无法直接在TLAB中分配,遇到这种情况时,有两种处理方案:
2.1如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则直接在堆内存中对该对象进行内存分配。
2.2如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则废弃当前TLAB,重新申请TLAB空间再次进行内存分配。
以上两个方案各有利弊,如果采用方案1,那么就可能存在着一种极端情况,就是TLAB只剩下1KB,就会导致后续需要分配的大多数对象都需要在堆内存直接分配。
如果采用方案2,也有可能存在频繁废弃TLAB,频繁申请TLAB的情况,而我们知道,虽然在TLAB上分配内存是线程独享的,但是TLAB内存自己从堆中划分出来的过程确实可能存在冲突的,所以,TLAB的分配过程其实也是需要并发控制的。而频繁的TLAB分配就失去了使用TLAB的意义。
3.为了解决这两个方案存在的问题,虚拟机定义了一个refill_waste的值,这个值可以翻译为“最大浪费空间”。
3.1当请求分配的内存大于refill_waste的时候,会选择在堆内存中分配。若小于refill_waste值,则会废弃当前TLAB,重新创建TLAB进行对象内存分配。
3.2前面的例子中,TLAB总空间100KB,使用了80KB,剩余20KB,如果设置的refill_waste的值为25KB,那么如果新对象的内存大于25KB,则直接堆内存分配,如果小于25KB,则会废弃掉之前的那个TLAB,重新分配一个TLAB空间,给新对象分配内存。

10.2 TLAB使用的相关参数

1.TLAB功能是可以选择开启或者关闭的,可以通过设置-XX:+/-UseTLAB参数来指定是否开启TLAB分配。
2.TLAB默认是eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
默认情况下,TLAB的空间会在运行时不断调整,使系统达到最佳的运行状态。如果需要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB来禁用,并且使用-XX:TLABSize来手工指定TLAB的大小。
3.TLAB的refill_waste也是可以调整的,默认值为64,即表示使用约为1/64空间大小作为refill_waste,使用参数:-XX:TLABRefillWasteFraction来调整。
4.如果想要观察TLAB的使用情况,可以使用参数-XX:+PringTLAB 进行跟踪。

10.3 TLAB相关案例代码演示

Eden区指针碰撞,需要模拟多线程并发申请内存空间。且需要关闭逃逸分析 -XX:-DoEscapeAnalysis -XX:+UseTLAB

/**
 * 测试 关闭逃逸分析 开启UseTLAB 效果演示
 * -Xmx100m -Xms100m -XX:-DoEscapeAnalysis -XX:+UseTLAB
 * -XX:TLABWasteTargetPercent=1 -XX:+PrintCommandLineFlags  -XX:+PrintGCDetails
 */
public class Test11 {

    private static final int threadNum = 100;
    private static CountDownLatch latch = new CountDownLatch(threadNum);
    private static final int n = 50000000 / threadNum;

    private static void alloc() {
        byte[] b = new byte[100];
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < n; j++) {
                    alloc();
                }
                latch.countDown();
            }).start();
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            System.out.println("hello world");
        }
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
    }

}


开启了tlab效果 代码运行时间:
在这里插入图片描述
关闭了tlab效果 代码运行时间:
-Xmx100m -Xms100m -XX:-DoEscapeAnalysis -XX:+UseTLAB -XX:TLABWasteTargetPercent=1 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails
在这里插入图片描述
经过对比,相差10倍左右

11. 内存逃逸分析

1.随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

2.在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
3.此外,前面提到的基于OpenJDK深度定制的TaoBao VM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

12. 逃逸分析

如何将堆上的对象分配到栈,需要使用逃逸分析手段。
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:
1.当一个对象在方法中被定义后,对象只在方法内部使用(栈帧中使用),则认为没有发生逃逸。—-new存放在栈空间上。
2.当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸 —-new存放在堆上

当一个对象, 能被其他方法访问到时, 这种逃逸叫做方法逃逸;

当一个对象, 能被其他线程访问到时, 这种逃逸叫做线程逃逸。

13. 逃逸分析案例

13.1 逃逸分析案例1

没有发生逃逸的对象,则可以分配到空间上((没有线程安全问题),随着方法执行的结束,栈帧空间就被移除,也无需GC回收。

// 逃逸案例分析
public static void main(String[] args) {

}

public void demo() {
    User user = new User();
}

class User {

}

13.2 逃逸分析案例2

StringBuffer sb 发生了逃逸,不能在栈上分配 因为 StringBuffer对象会被外部其他方法使用

public static void main(String[] args) {
    StringBuffer stringBuffer = createStringBuffer();
}
public static StringBuffer createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}


StringBuffer sb不发生逃逸,可以这样写

public static String createStringBuffer2(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

13.3 逃逸分析案例3

public class Test03 {
    private static User user;

    public static User getInstance() {
        return user == null ? new User() : user;
    }

    public static void getInstance2() {
        user = new User();
    }

    public void setUser(User user) {
        this.user = user;
    }

    public void getInstance3() {
        User user = getInstance();
    }

    static class User {

    }
}


开发者使用局部变量,没有发生逃逸,对象会存放在栈空间中 当栈帧方法结束之后 该对象会自动消失,不需要GC回收垃圾。

14. 逃逸分析参数设置

在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析
如果使用的是较早的版本,开发人员则可以通过:
选项“-XX:+DoEscapeAnalysis”显式开启逃逸分析
通过选项“-XX:+PrintEscapeAnalysis”查看逃逸分析的筛选结果

15. 逃逸优化分析

15.1 栈上分配

将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配,而不是堆上分配。

JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的栈上分配的场景:在逃逸分析中,已经说明了,分别是给成员变量赋值、方法返回值、实例引用传递。

注意:不要 使用debug 运行 测试逃逸分析 效果, 否则无效!!!

/**
 * -Xmx256m -Xms256m  -XX:+DoEscapeAnalysis -XX:+PrintGCDetails  开启逃逸分析
 * -Xmx256m -Xms256m  -XX:+DoEscapeAnalysis -XX:+PrintGCDetails  关闭逃逸分析
 * <p>
 * 注意:不要 使用debug 运行 测试逃逸分析 效果, 否则无效!!!!!
 *
 * @param args
 * @throws InterruptedException
 */
public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    for (int i = 0; i <= 10000000; i++) {
        demo();
    }
    long end = System.currentTimeMillis();
    System.out.println("程序执行的时间:" + (end - start));
    Thread.sleep(1000000);
}

public static void demo() {
    // 未发生逃逸
    User user = new User();
}

static class User {

}

开启逃逸分析:
-Xmx256m -Xms256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails 开启逃逸分析
在这里插入图片描述
3s 毫秒 没有触发 任何GC操作
关闭逃逸分析:
在这里插入图片描述
触发了新生代GC回收。

15.2 同步省略(锁的消除)

1.线程同步的代价是相当高的,同步的后果是降低并发性和性能。
2.在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被其他线程访问。
3.如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

private Object objectLock = new Object();

public static void main(String[] args) {
    Test05 test05 = new Test05();
    test05.demo();
}

public void demo() {

    synchronized (objectLock) {
        System.out.println(objectLock);
    }
}


15.3 标量替换

1.标量(scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
2.相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
3.在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

开启标量替换:

public static void main(String[] args) {
    demo();
}

public static void demo() {
    Demo demo= new Demo();
    demo.x = 10;
    demo.y = 20;
    System.out.println(demo.x + "," + demo.y);
}

/**
 * 以上代码,经过标量替换后,就会变成
 */

public static void demo() {
    int x = 10;
    int y = 20;
    System.out.println(x + "," + y);
}

static class demo {
    private int x;
    private int y;
}


可以看到,Demo 这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。
那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
标量替换为栈上分配提供了很好的基础。
标量替换参数设置
参数 -XX:+ElimilnateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。

15.3.1 标量替换案例演示

默认的情况下 JDK 已经开启了 标量替换

相关参数配置:-Xmx256m -Xms256m -XX:+PrintGCDetails -XX:-EliminateAllocations

相关代码

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            demo();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为: " + (end - start) + " ms");
    }

    public static void demo() {
        Demo demo = new Demo();
        demo.userName = "demo";
        demo.age = 21;
    }

    /**
     * 以上代码,经过标量替换后,就会变成
     */

//    public static void demo() {
//        int x = 10;
//        int y = 20;
//        System.out.println(x + "," + y);
//    }

    static class Demo{
        private String userName;
        private Integer age;
    }


没有开启标量替换:
相关参数配置:-Xmx256m -Xms256m -XX:+PrintGCDetails -XX:-EliminateAllocations

执行该代码需要花费50毫秒左右
在这里插入图片描述
开启标量替换:
相关参数配置:-Xmx256m -Xms256m -XX:+PrintGCDetails -XX:+EliminateAllocations
只需要5毫秒
在这里插入图片描述

15.4 逃逸分析的优缺点

1.关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟的。
其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。2.但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
3.注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,Oracle Hotspot JVM中并未这么做(刚刚演示的效果,是因为HotSpot实现了标量替换),这一点在逃逸4.分析相关的文档里已经说明,所以可以明确在HotSpot虚拟机上,所有的对象实例都是创建在堆上。
目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

逃逸分析优点:

  1. 减少GC回收的次数 提高程序的效率
  2. 将new对象存放在栈空间中,当方法执行结束 自动释放内存
  3. 但是我们在写代码的过程基本上都是逃逸了。

16. 堆空间常见设置参数

1.测试堆空间常用的jvm参数:
2.-XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
3.-XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数的指令: jps:查看当前运行中的进程
4. jinfo -flag SurvivorRatio 进程id
5.-Xms:初始堆空间内存 (默认为物理内存的1/64)
6.-Xmx:最大堆空间内存(默认为物理内存的1/4)
7.-Xmn:设置新生代的大小。(初始值及最大值)
8.-XX:NewRatio:配置新生代与老年代在堆结构的占比
9.-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
10.-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
11.-XX:+PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/131188.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!