JVM~第四天~堆

1.本地方法接口

​ 简单来说,一个Native Method就是一个Java调用非Java代码的接口。

​ 为什么要使用Native Method?

  • 与Java环境外交互

    有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。

  • 与操作系统交互

    通过使用本地方法,我们得以使用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的。

  • Sun’s Java

    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。

2.本地方法栈

​ Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。

  • 本地方法栈,也是线程私有的。
  • 允许被实现成固定或者是可动态扩展。(在内存溢出方面是相同的)
  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限
    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等

3.堆

1)堆的核心概述

在这里插入图片描述
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。

​ 堆,是GC(Garbage Collection,垃圾回收器)执行垃圾回收得重点区域。

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

2)设置堆内存大小与OOM

  • 可以通过“-Xmx”和“-Xms”来进行设置。

    • “-Xms”用于表示堆区的起始内存,等价于-XX:InitialSize。用来设置堆空间(年轻代+老年代)的初始化内存大小。
    • “-Xmx”则用于表示堆区的最大内存,等价于-XX:MaxHeapSize。用来设置堆空间(年轻代+老年代)的最大内存大小。
  • 一旦堆区中的内存大小超过超过"-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。

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

    初始内存大小:电脑物理内存大小 / 64

    最大内存大小:电脑物理内存大小 / 4

    初始大小:
    在这里插入图片描述

    修改配置后:在这里插入图片描述在这里插入图片描述

  • 一般开发中建议将初始堆内存和最大的堆内存设置成相同的值。

  • sleep1000s,通过cmd查看内存使用。在这里插入图片描述

    25600+25600+153600+409600=614400 -> 614400/1024 = 600

    25600+153600+409600=588,800‬ -> 588,800/1024 = 575

    因为S0C和S1C只会使用一个。

3)年轻代与老年代

​ Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)。年轻代又可以划分为Eden空间、Survivor0和Survivor1空间(有时也叫做from区、to区)。
在这里插入图片描述

  • 配置新生代和老年代在堆结构的占比

    默认-XX:NewRation=2,表示新生代占1,老年代占2,即新生代占整个堆的1/3。
    在这里插入图片描述
    在HotSpot中,Eden和Survivor0,Survivor1所占比为8:1:1。可以使用-XX:SurvivorRatio=8进行调整。

4)图解对象分配过程

  1. new的对象先放在Eden区。此区有大小限制。

  2. 当Eden区的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(GC),将Eden区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到Eden区。

  3. 然后将Eden区中的剩余对象移动到Survivor0区。

  4. 如果再次触发垃圾回收,此时上次放入Survivor0区的,如果没有回收,就会放到Survivor1区。

  5. 如果再经历垃圾回收,此时会重新放入Survivor0区,接着再去Survvivor1区。

  6. 什么时候去Tenured区?可以设置次数,默认是15次。

    -XX:MaxTenuringThreshold=N进行设置。

  7. 在Tenured区,相对悠闲,当Tenured区内存不足时,再次触发GC:Major GC,进行Tenured区的内存清理。

  8. 若Tenured区执行了Major GC之后发现仍然无法进行对象的保存,就会生成OOM异常(java.lang.OutOfMemoryError:Java heap space)
    在这里插入图片描述
    在这里插入图片描述

5)Minor GC、Major GC、Full GC

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

  • 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
    • 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集。
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有CMS GC会有单独收集老年代的行为。很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为。
  • 整体收集:收集整个Java堆和方法区的垃圾收集。
  • 年轻代GC(Minor GC)触发机制:
    • 当年轻代空间不足时,就会触发Minor GC,这里年轻代满指的是Eden区满,Survivor满不会触发GC。(每次Minor GC 会清理年轻代的内存。)
    • 因为Java对象大多都具备朝生夕灭的特性,所有Minor GC非常频繁,一般回收速度也比较快。
    • Minor GC会引发STW(Stop-The-World),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
  • 老年代GC(Major GC /Full GC)触发机制:
    • 指发生在老年代的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后,内存还不足,就会报OOM。
  • Full GC触发机制:
    • 调用System.gc()时,系统建议执行Full GC,但是不必然执行。
    • 老年代空间不足。
    • 方法区空间不足。
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
    • 由Eden区、Survior space0(From Space)区向Survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
    • ps:Full GC是开发或调优中尽量要避免的。

6)堆空间分代思想

​ 分代就是为了优化GC性能。

7)内存分配策略

​ 针对不同年龄段的对象分配原则如下:

  • 优先分配到Eden

  • 大对象直接分配到老年代

  • 长期存活的对象分配到老年代

  • 动态对象年龄判断

    如果Survivor区中相同年龄的所有对象的总和大小大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的值。

  • 空间分配担保

    -XX:HandlePromotionFailure

8)为对象分配内存:TLAB(Thread Local Allocation Buffer)

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

  • 可以通过“-XX:UserTLAB”来设置是否开启,默认开启。
  • 默认情况下,TLAB空间的内存非常小,仅占整个Eden空间的1%,可以通过“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
  • 一但对象在TLAB空间分配内存失败时,JVM会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
  • 分配过程在这里插入图片描述

9)小结堆空间的参数设置

​ -XX:+PrintFlagsInitial:查看所有参数的默认初始值

​ -XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)

​ -Xms:初始堆空间内存。(默认为物理内存的1/64)

​ -Xmx:最大堆空间内存。(默认物理内存的1/4)

​ -Xmn:设置新生代的大小。(初始值及最大值)

​ -XX:NewRatio:配置新生代与老年代在堆结构的占比。

​ -XX:SurvivorRatio:配置新生代中Eden和S0/S1空间的比例

​ -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄

​ -XX:+PrintGCDetails:输出详细的GC处理日志

10)堆是分配对象的唯一选择吗?

​ 逃逸分析的基本行为就是分析对象动态作用域:

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

  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如,作为调用参数传递到其他地方中。

    没有发生逃逸的对象,则可以分配到栈中,随着方法执行的结束,栈空间就被移除。

    // 如何快速判断是否发生了逃逸分析,就看new的对象是否可能在方法外被调用。
    public class EscapeAnalysis{
    	public EscapeAnalysis obj;
        
        // 方法返回EscapeAnalysis对象,发生逃逸
        public EscpaeAnalysis 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();
        }
    }
    

使用逃逸分析,编译器可以对代码做如下优化:

  • 栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
  • 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
    • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除
  • 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
    • 标量是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
    • 相对的,可以分解的数据叫做聚合量,Java中的对象就是聚合量,因为它可以分解成其他聚合量和标量。
    • 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值