尚硅谷2020最新版宋红康JVM教程学习笔记 三 堆

点击查看合集

1.堆位于运行时数据区中是进程共享的。一个进程对应一个jvm实例。一个jvm实例对应一个运行时数据区。一个运行时数据区有一个堆空间。
2.java堆区在jvm启动的时候就被创建了,其空间大小也就被确定了(堆是jvm管理的最大的内存空间)java堆的大小是可以调节的。

public class Test1 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        System.out.println("开始");
        Thread.sleep(1000*100*100);
        System.out.println("结束");
    }
}
public class Test2 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        System.out.println("开始");
        Thread.sleep(1000*100*100);
        System.out.println("结束");
    }
}

用-Xms 设置堆空间的最小空间 用-Xmxs设置堆的最大空间(年轻代和老年代 不包括永久代/元空间)-X是jvm的运行参数 ms是memory start(初始大小)
默认情况下初始化大小是电脑内存空间的64分之一。最大空间为电脑内存空间的四分之一 在这里插入图片描述在这里插入图片描述

在这里插入图片描述

3.堆可以处于物理上不连续的内存空间。但是在逻辑上是连续的。
4.所有线程共享java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer 即TLAB)。
5.几户所有的对象实例以及数组都应该在运行时分配在堆上。栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
在这里插入图片描述

6.方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
7.堆是GC(Garbage Collection 垃圾收集器)执行垃圾回收的重点区域。

堆中的内存细分结构

java7及以前的堆内存逻辑上分为三个部分:新生区 养老区 永久区
java 8及之后堆内存逻辑上分为三个部分:新生区 养老区 元空间
用-Xms和-Xmx设置堆空间大小不包括永久区/元空间区

public class Test3 {
    
    
    public static void main(String[] args) {
    
    
        //堆初始大小
        long init = Runtime.getRuntime().totalMemory()/1024/1024;
        System.out.println(init+"M");
        Thread.sleep(1000*1000);
    }
}

在这里插入图片描述
在这里插入图片描述
设置的是600M为什么显示的是575M
在这里插入图片描述
年轻区(S0区 S1区 伊甸园区 )和老年区总和为600M但是同一时间只有一个S区(S0或者S1区)被使用。所以堆的总空间为600M但是同一时间被使用的是575M
另一种查询方式
设置jvm参数
-XX:+PrintGCDetails
在这里插入图片描述
在这里插入图片描述

年轻代与老年代

1.存储在jvm中的java对象可以被划分为两类一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。另外一类对象的声明周期却非常长,在某些极端的情况下还能够与jvm的声明周期保持一致。
2.java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)其中年轻代又可以划分为Eden空间(伊甸园区)Survivor0空间和Survivor1空间(幸存者0/1区有时候也叫做from区、to区)
3.几乎所有java对象都是在eden区被new出来的。(对象比eden区还要大,就要在老年区new出来)绝大部分java对象的销毁都在新生代。(IBM的研究表明新生代中有90%以上的对象都是朝生夕死的)
在这里插入图片描述

-XX:NewRatio

默认-XX:NewRatio=2 表示新生代占1老年代占2即新生代占整个堆的1/3
-XX:NewRatio=4表示新生代:老年代=1:4

-XX:SurvivorRatio

默认是伊甸园区:幸存者0区:幸存者1区=8:1:1(因为自适应的原因,实际上为6:1:1)
如果要修改的话
-XX:SurvivorRatio 8 伊甸园区:幸存者0区:幸存者1区=8:1:1
在这里插入图片描述

对象分配过程

1.new的对象现方伊甸园区(大对象直接放老年代)
2.伊甸园区被填满时,程序再次创建对象时。jvm垃圾回收器将对伊甸园区进行垃圾回收(叫做YGC或者Minor GC)将伊甸园区中的不再被引用的对象销毁。
3.将伊甸园区剩余对象移动到幸存者0区
4.再次触发垃圾回收时,将伊甸园区和幸存者0区中不被引用的对象销毁。将剩余对象移放到幸存者1区
5.再次触发垃圾回收时操作相同,但是要将剩余对象放入幸存者0区
6.每次移动进入幸存者区都要增加年龄(默认15次时放入老年区即第16次)可以通过
-XX:MaxTenuringThreshold=10 此时为10
注意:如果survivor区中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大雨或等于该年龄的对象可以直接进入老年代,无需等到Max
7.在养老区内存不足时,再次出发GC(Major GC)
8.若养老区执行了Major Gc之后发现依然无法进行对象的保存,就会发生OOM(OutOfMemoryError)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Minor GC Major GC Full GC

部分收集

部分收集不是完整收集整个java堆的垃圾收集。其中又分为
1.新生代收集(Minor GC/Young GC):只是新生代(Eden/S0/S1)的垃圾收集
2.老年代收集(Major GC/Old GC)只是老年代的垃圾收集。
目前只有CMS GC会有单独手机老年代的行为
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨时老年代回收还是整堆回收
3.混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前只有G1 GC会有这种行为

整堆收集(Full GC)

收集整个java堆和方法区的垃圾收集,

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

当年轻代(Eden区)空间不足时,就会触发Minor GC。会清理这个年轻代的内存空间。因为大部分对象生命周期都比较短,因此Minor GC发生的非常频繁,一般回收速度也比较快。Minor GC会触发STW(Stop Work)暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行,

老年代GC(Major GC/Full GC)触发机制

1.出现Major GC经常伴随至少一次的Minor GC(但并非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
也就是在老年代空间不足时会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
2.Major GC速度一般比Minor GC慢10倍以上 STW的时间更长
3.如果Magor GC后,内存还不足就OOM
###Full GC触发机制
触发Full GC的情况
1.调用System.gc()时,系统建议执行Ful GC但是不必然执行
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5.由Eden区survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转到老年代,且老年代的可用内存小于该对象大小。
说明:Full GC是开发或调优中尽量要避免的。这样暂停时间会短一些
可以通过-XX:+PrintGCDetails参数查看GC信息

对象分配过程:TLAB(Thread Local Allocation Buffer)

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

什么是TLAB

TLAB从内存模型的角度,对Eden区继续进行划分,JVM为每个线程在Eden分配了一个私有缓存区域,
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种分配方式为快速分配策略。

在这里插入图片描述
1.尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实时将TLAB作为内存分配的首选
2.在程序中,可以用过-XX:+/-UserTLAB设置是否开启(默认开启)
3.默认情况下,TLAB的内存非常小,仅占整个Eden空间的1%,当然我们可以通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
4.一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
在这里插入图片描述

JVM参数总结

在这里插入图片描述
在这里插入图片描述

对象分配在堆外的情况

随着JIT编译器的发展于逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术使对象不一定绝对分配在堆中。
在java虚拟机中,对象使java堆分配内存的,这是一个普遍常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸处方法的话,那么就可能被优化成栈上分配。这样就无需进行GC.这是最常见的堆外存储技术。
(了解)另外TaoBaoVM其中创新的GCIH(GC invisible heap)技术实现off-heap将生命周期较长的java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的

逃逸分析

当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。

//没有逃逸可以放到栈中
    public List getList(){
    
    
        List list = new ArrayList();
        list.add(new Object());
        return null;
    }

当一个对象在方法中被定义后,他被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中
没有发生逃逸的对象,则可以分配到栈上,随着方法执行结束,栈空间被移除

    //发生逃逸 不能放到栈中
    public List getList(){
    
    
        List list = new ArrayList();
        list.add(new Object());
        return list;
    }

通过判断对象有没有可能在方法外被调用。
从JDK7开始默认开启逃逸分析。
如果使用JDK7之前的版本可以通过-XX:+DoEscapeAnalysis开启逃逸分析
通过-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果(了解)

逃逸分析的目的

1.栈上分配:可以把没发生逃逸的对象分配在栈上

public class Test6 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //开始时间
        long star =  System.currentTimeMillis();
        for (int i = 0;i<10000000;i++){
    
    
            newObject();
        }
        long end = System.currentTimeMillis();
        System.out.println("执行了"+(end-star)+"ms");
        Thread.sleep(1000*1000*10);
    }
    public static void newObject(){
    
    
        //未逃逸
        Test6 t6 = new Test6();
    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

如果开启逃逸分析后
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.同步省略:没发生逃逸说明只有一个线程能访问到该对象,因此对该对象的操作可以不加锁
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步代码块的时候就会取消这部分代码的同步。这个过程就叫做同步省略或者叫锁消除。

public void method1(){
    
    
        Object lock = new Object();
        synchronized(lock){
    
    
            System.out.println(1);
        }
    }
public void method1(){
    
    
        Object lock = new Object();
        Object lock = new Object();
		System.out.println(1);
    }

3.分离对象或标量替换
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
通过-XX:+EliminateAllocations开启标量替换(默认开启)
首先这个对象需要是没有发生逃逸

猜你喜欢

转载自blog.csdn.net/qq_30033509/article/details/110549888
今日推荐