其他分享
首页 > 其他分享> > 07 堆

07 堆

作者:互联网

概述

堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。

image-20220727175923026

/**
 * VM: -Xms10m -Xmx10m
 */
public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            TimeUnit.SECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}

/**
 * VM: -Xms20m -Xmx20m
 */
public class HeapDemo1 {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            TimeUnit.SECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}

image-20220727180128602

image-20220727180315260

堆的内存细分

image-20220727180345386

堆空间内部结构,JDK1.8之前从永久代 替换成 元空间

image-20220727180406428

堆空间大小的设置

public class HeapSizeDemo {
    public static void main(String[] args) {
        long initialMemory = Runtime.getRuntime().totalMemory();
        long maxMemory = Runtime.getRuntime().maxMemory();

        System.out.println("-Xms:" + (initialMemory / 1024 / 1024) + " M");
        System.out.println("-Xms:" + (maxMemory / 1024 / 1024) + " M");
        System.out.println("系统内存大小:" + (initialMemory * 64 / 1024 / 1024 / 1024) + " G");
        System.out.println("系统内存大小:" + (maxMemory * 4 / 1024 / 1024 / 1024) + " G");

    }
}

/**执行结果
 * -Xms:245 M
 * -Xms:3618 M
 * 系统内存大小:15 G
 * 系统内存大小:14 G
 */

如何查看堆内存的内存分配情况

jps
jstat -gc 进程id

image-20220727180543913

image-20220727180610883

OutOfMemoryError 举例

/**
 * VM args: -Xms20m -Xmx20m
 * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
 */
public class HeapOomCase {
    static class OomObject {

    }

    public static void main(String[] args) {
        ArrayList<OomObject> list = new ArrayList<>();
        while (true) {
            list.add(new OomObject());
        }
    }
}

年轻代和老年代

image-20220727180646245

下面这参数开发中一般不会调:

image-20220727210644583

image-20220727180759212

图解对象的分配过程

图解过程

我们创建的对象,一般都是存放在Eden区的,当我们Eden区满了后,就会触发GC操作,一般被称为 YoungGC / Minor GC操作

image-20220727180903117

当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From)区。同时我们给每个对象设置了一个年龄计数器,一次回收后就是1。

同时Eden区继续存放对象,当Eden区再次存满的时候,又会触发一个MinorGC操作,此时GC将会把 Eden和Survivor0 From中的对象进行一次收集,把eden中存活的对象放到 Survivor1 To区,并且把s1区的对象判断,存活的话就放到s2区,同时让年龄 + 1

image-20220727181027436

我们继续不断的进行对象生成 和 垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion晋升的操作,也就是将年轻代中的对象 晋升到 老年代中

image-20220727181039291

思考:幸存区区满了后?

特别注意,在Eden区满了的时候,才会触发YoungGC/MinorGC,而幸存者区满了后,不会触发MinorGC操作

如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代

举例:以当兵为例,正常人的晋升可能是 : 新兵 -> 班长 -> 排长 -> 连长

但是也有可能有些人因为做了非常大的贡献,直接从 新兵 -> 排长

针对幸存者s0,s1区的总结:

复制之后有交换,谁空谁是to

关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/
元空间收集。

对象分配的特殊情况

image-20220727181103464

/**
 * VM args: -Xms20m -Xmx20m
 */
public class HeapOomCase {
    static class OomObject {

    }

    public static void main(String[] args) {
        ArrayList<OomObject> list = new ArrayList<>();
        while (true) {
            list.add(new OomObject());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

image-20220727181224019

最终,在老年代和新生代都满了,就出现OOM

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.atguigu.java.chapter08.HeapInstanceTest.<init>(HeapInstanceTest.java:13)
	at com.atguigu.java.chapter08.HeapInstanceTest.main(HeapInstanceTest.java:17)

常用的调优工具

总结

Minor GC、Major GC和Full GC

年轻代GC(Minor GC/YGC)的触发机制

image-20220727181340759

老年代GC(Major GC / Full GC)的触发机制
Full GC 触发机制(后面细讲)

堆空间分代思想

image-20220727181429057

其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

image-20220727181442378

堆内存分配策略

为对象分配内存:TLAB

image-20220727181511846

对象的分配过程

image-20220727181533158

小结堆空间的参数设置


堆是分配对象存储的唯一选择吗?

逃逸分析概述

逃逸分析举例

没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除,每个栈里面包含了很多栈帧,也就是发生逃逸分析

public void my_method() {
    V v = new V();
    // use v
    // ....
    v = null;
}

针对下面的代码

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 createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

完整的逃逸分析代码举例

/**
 * 逃逸分析
 * 如何快速的判断是否发生了逃逸分析,大家就看new的对象是否在方法外被调用。
 */
public class EscapeAnalysis {

    public EscapeAnalysis obj;

    /**
     * 方法返回EscapeAnalysis对象,发生逃逸
     * @return
     */
    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 useEscapeAnalysis2() {
        EscapeAnalysis e = getInstance();
        // getInstance().XXX  发生逃逸
    }
}

参数设置

在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析

如果使用的是较早的版本,开发人员则可以通过:

逃逸分析:代码优化

栈上分配
同步策略 - 锁消除

例如下面的代码

public void f() {
    Object hellis = new Object();
    synchronized(hellis) {
        System.out.println(hellis);
    }
}

代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:

public void f() {
    Object hellis = new Object();
	System.out.println(hellis);
}

我们将其转换成字节码

image-20220729171109138

分离对象和标量替换

标量(scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。

相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

public static void main(String args[]) {
    alloc();
}
class Point {
    private int x;
    private int y;
}
private static void alloc() {
    Point point = new Point(1,2);
    System.out.println("point.x" + point.x + ";point.y" + point.y);
}

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

private static void alloc() {
    int x = 1;
    int y = 2;
    System.out.println("point.x = " + x + "; point.y=" + y);
}

可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
标量替换为栈上分配提供了很好的基础。

代码优化之标量替换

上述代码在主函数中进行了1亿次alloc。调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB。如果堆空间小于这个值,就必然会发生GC。使用如下参数运行上述代码:

-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

这里设置参数如下:

栈上分配其实就是分配到局部变表中,而局部变量表中只能存放基本类型的变量和对家引用所以必须通过标量替换将对像打散成基本类型变量,这前提又要依赖逃逸分析,即对象未发生逃逸

逃逸分析的不足

关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟。

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JvM设计者的选择。据我所知,oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。

目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。

总结

标签:Java,07,对象,逃逸,GC,内存,分配
来源: https://www.cnblogs.com/flypigggg/p/16533011.html