JAVA基础-内存与垃圾回收

1,运行时数据区

1,程序计数器

  • 线程私有。
  • 生命周期:生命周期与线程同步。
  • 作用:它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 特点:它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域,唯一没有 OutofMemoryError 情况的区域。

2,虚拟机栈

  • 定义:Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用,是线程私有的。
  • 线程私有
  • 生命周期:与线程一致
  • 作用:主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
  • 大小设置:-Xss,栈的大小直接决定了函数调用的最大深度
  • 栈的存储单位
    • 栈帧,这个线程上每个执行的方法对应一个栈帧。
    • 运行原理:栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。当前活动的栈帧就是对应的当前方法。
    • 栈帧的内部结构:局部变量表,操作数栈,动态链接,方法返回地址,一些附件信息
  • 特点:
    • 会发生内存溢出:Exception in thread "main" java.lang.StackOverflowError,
    • 垃圾回收不会涉及虚拟机栈。
    • 方法局部变量是否线程安全:内部产生,内部消亡,没有返回到外部就是线程安全,反之线程不安全。

3,本地方法栈

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

4,堆

  • 定义:所有的对象实例以及数组都应当在运行时分配在堆上。堆针对一个 JVM 进程来说是唯一的,也就是一个进程只有一个 JVM,但是进程包含多个线程,他们是共享同一堆空间的。

  • 线程公有

  • 堆空间大小设置:
    -Xms:用于表示堆区起始内存,等价于 -XX:InitialHeapSize,默认电脑内存大小 1/64
    -Xmx:用于表示堆区的最大内存,等价于 -XX:MaxHeapSize,默认电脑内存大小 1/4

  • 特点:

    1. 会发生内存溢出:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    2. 垃圾回收的主要作用区域
  • 堆内存细分:
    Java7 及以前:新生区 + 养老区 + 永久区
    Java8 及以后:新生区 + 养老区 + 元空间

  • 新生代和老年代

    Java 堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen),其中年轻代又可以划分为 Eden 空间、Survivor0 空间和 Survivor1 空间(有时也叫做 from 区、to 区)。

    • 参数设置:
      -XX:NewRatio=2:表示新生代占 1,老年代占 2,新生代占整个堆的 1/3,默认就是 2
      -xx:SurvivorRatio=8:Eden 空间和另外两个 survivor 空间比例,默认 8:1:1
      -Xmn:设置新生代最大内存大小

几乎所有的 Java 对象都是在 Eden 区被 new 出来的。绝大部分的 Java 对象的销毁都在新生代进行了。

  • 对象分配过程

    1)new 的对象先放伊甸园区。此区有大小限制。

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

    3)然后将伊甸园中的剩余对象移动到幸存者 0 区。

    4)如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区的,如果没有回收,就会放到幸存者 1 区。

    5)如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区。

    6)啥时候能去养老区呢?可以设置次数。默认是 15 次。

    可以设置参数:-Xx:MaxTenuringThreshold= N 进行设置
    在养老区,相对悠闲。当养老区内存不足时,再次触发 GC:Major GC,进行养老区的内存清理
    

    7)若养老区执行了 Major GC 之后,发现依然无法进行对象的保存,就会产生 OOM 异常。
    java.lang.OutofMemoryError: Java heap space

关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集,分代的作用就是优化 GC 性能。

  • GC 方式

    • 1)部分 GC
      新生代 GC:(Minor GC / Young GC):只是新生代的垃圾收集
      老年代 GC:(Major GC / Old GC):只是老年代的圾收集

    • 2)整堆 GC(Full GC):收集整个 java 堆和方法区的垃圾收集。

  • 内存分配策略

    • 优先分配到 Eden
    • 大对象直接分配到老年代(尽量避免程序中出现过多的大对象)
    • 长期存活的对象分配到老年代
    • 动态对象年龄判断:如果 survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
    • 空间分配担保: -XX:HandlePromotionFailure

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

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

老年代 GC(Major GC)触发机制:

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

5,方法区

  • 定义
    尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。方法区看作是一块独立于 Java 堆的内存空间。
  • 线程共有
  • 特点
    • 方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域。
    • 方法区在 JVM 启动的时候被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的。
    • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
    • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace

加载大量的第三方的 jar 包;Tomcat 部署的工程过多(30~50 个);大量动态的生成反射类
关闭 JVM 就会释放这个区域的内存。

方法区存储的是什么?

  它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

永久代为什么换成元空间?

  永久代的大小很难确定。元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。

6,对空间参数总结

-XX:+PrintFlagsInitial  //查看所有的参数的默认初始值
-XX:+PrintFlagsFinal  //查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms  //初始堆空间内存(默认为物理内存的1/64)
-Xmx  //最大堆空间内存(默认为物理内存的1/4)
-Xmn  //设置新生代的大小。(初始值及最大值)
-XX:NewRatio  //配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio  //设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold  //设置新生代垃圾的最大年龄
-XX:+PrintGCDetails //输出详细的GC处理日志
-XX:HandlePromotionFalilure://是否设置空间分配担保

查看当前内存情况:

 static public void printMemoryInfoJVM() {

    double allocatedMemory = (double) Runtime.getRuntime().totalMemory() / 1024. / 1024. / 1024.;
    double maxMemory = (double) Runtime.getRuntime().maxMemory() / 1024. / 1024. / 1024.;
    double freeMemory = (double) Runtime.getRuntime().freeMemory() / 1024. / 1024. / 1024.;
    freeMemory += (maxMemory - allocatedMemory);
    double memoryUsage = (maxMemory - freeMemory) / maxMemory;
    log.info("JVM 当前申请的内存:" + String.format("%.2f", allocatedMemory) + "G");
    log.info("JVM 最多能获取到的内存:" + String.format("%.2f", maxMemory) + "G");
    log.info("JVM 空闲的内存:" + String.format("%.2f", freeMemory) + "G");
    log.info("JVM 内存利用率:" + String.format("%.2f", memoryUsage * 100) + "%");
};

//获取宿主机内存状态
@PostConstruct
static public void printMemoryInfoHost(){
    OperatingSystemMXBean mem = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
    double totalMemorySystem = (double)mem.getTotalPhysicalMemorySize() / 1024. / 1024. /1024.;
    double freeMemorySystem = (double)mem.getFreePhysicalMemorySize() / 1024. / 1024. /1024.;
    double memoryUsageSystem = (totalMemorySystem - freeMemorySystem)/totalMemorySystem;
    log.info("主机总内存:"+String.format("%.2f",totalMemorySystem)+"G");
    log.info("主机空闲内存:"+String.format("%.2f",freeMemorySystem)+"G");
    log.info("主机内存使用率:"+String.format("%.2f",memoryUsageSystem*100)+"%");
}

设置内存参数:

7,逃逸分析

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

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

堆是分配对象的唯一选择么?
在 Java 虚拟机中,对象是在 Java 堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配.。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术

8,字符串常量池

字符串常量池,即String Constant Pool,又叫做String Pool,String Table。顾名思义,即用于存放字符串常量的运行时内存结构,其底层实现为一种Hashtable。其中所指的字符串常量,可以是编译期在源码中显式的字符串字面量,也可以是之后在程序运行时创建的字符串String对象。

  • 编译器优化放在常量池中。
 1)String s = "hello" + " world";
 2)String s = "hello world";
 3)final String s = "hello";
    String s1 = s + " world";
  • 含有变量的放堆中。
 1)String s = "hello";
    String s1 = s + " world";
 2)String s = "hello" + new String("world");
  • intern() 方法:
    将变量放入字符串常量池中并返回地址。

2,垃圾回收机制:GC

垃圾回收机制的作用:释放没用的对象,回收内存空间,也可以清除内存里的记录碎片。

垃圾回收主要是方法区和堆:

  • 频繁收集 Young 区
  • 较少收集 Old 区
  • 基本不收集 Perm 区(元空间)

1,内存泄漏和内存溢出

内存泄露:未使用的内存没用被正确回收。
内存溢出:程序申请的内存大小超过可用的内存。

2,垃圾标记算法

  • 引用计数算法

    • 对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
    • 一个对象引用了 A,则 A的引用计数器就加 1。当引用失效就减 1,当 A 的引用计数器为 0,表示对象 A 不再被使用,可进行回收。
  • 可达性分析算法

    • 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
    • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
    • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
    • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

3,垃圾回收算法

  • 标记-清除算法

    • 标记:Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象。
    • 清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收
  • 复制算法

      将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收

  • 标记-压缩(标记-清除-压缩)

    • 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
    • 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
    • 之后,清理边界外所有的空间。
  • 分代收集算法:

    • 年轻代:复制算法
    • 老年代:标记清除或标记压缩算法。

何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。

4,显式触发 GC

system.gc();
Runtime.getRuntime().gc();    

只是提醒该 GC 了,并不是立即进行垃圾回收。

posted @ 2024-02-19 10:36  primaryC  阅读(10)  评论(0编辑  收藏  举报