栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

三、JVM 内存分配机制

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

三、JVM 内存分配机制

  • 一、JVM 类加载机制
  • 二、JVM 内存模型
  • 三、JVM 内存分配机制
  • 四、JVM 垃圾收集算法 和 垃圾收集器

一、JVM 内存分配与回收
1. 对象优先在 Eden 区分配
  • 大多数情况下,对象在新生代中 Eden 区分配。
  • 当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 MinorGC。

  • MinorGC/YoungGC:指发生新生代的的垃圾收集动作,MinorGC 非常频繁,回收速度一般也比较快。
  • MajorGC/FullGC:一般会回收 年轻代、老年代、方法区 的垃圾,MajorGC 的速度一般会比 MinorGC 的慢 10 倍以上。

2. 大对象直接进入老年代
  • 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
  • JVM 参数(-XX:PretenureSizeThreshold=0)可以设置大对象的大小。
  1. 默认值是 0,意味着任何对象都会现在新生代分配内存。
  2. 如果对象超过设置的大小,会直接进入老年代,不会进入年轻代。
  3. 这个参数只在 Serial 和 ParNew 两个收集器下有效。

  • 避免为大对象分配内存时的复制操作而降低效率,配置 大对象
# 设置`JVM`参数,大对象(超过`1000000`B)直接进入老年代。
-XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC

3. 长期存活的对象将进入老年代
  • 既然虚拟机采用了 分代收集的思想 来管理内存。
    那么内存回收时就必须能识别,哪些对象应放在新生代,哪些对象应放在老年代中。
  • 为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
  1. 如果对象在 Eden 出生,并经过第一次 MinorGC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1 岁。
  2. 对象在 Survivor 区中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认:15岁),就会被晋升到老年代中。

  • 对象晋升到老年代的 年龄阈值,可以通过参数(-XX:MaxTenuringThreshold=15)来设置。
-XX:MaxTenuringThreshold=10

4. 对象动态年龄判断
  • 当前放对象的 Survivor 区域里(放对象的那块 Survivor 区),对象的总大小大于这块 Survivor 区域内存大小的 50%。
  • 那么此时 大于等于 这批对象年龄最大值的其他对象,就可以直接进入老年代了。
  1. 例如:Survivor 区域里现在有一批对象(年龄1、年龄2、年龄n)的多个年龄对象。
  2. 对象大小总和超过了 Survivor 区域的 50%。
  3. 此时就会把 年龄n 以上的对象都放入老年代。
  • 这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。
  • 对象动态年龄判断机制,一般是在 MinorGC 之后触发的。

5. MinorGC 后存活的对象 Survivor 区放不下
  • 这种情况会把存活的对象,部分挪到老年代,部分可能还会放在 Survivor 区。

6. 老年代空间分配担保机制
  • 年轻代每次 MinorGC 之前,JVM 都会计算下 老年代剩余可用空间。
  1. 如果这个可用空间 小于 年轻代里现有的所有对象大小之和(包括垃圾对象)。
  2. 就会查看(-XX:-HandlePromotionFailure)的参数是否设置了(JDK-8 默认就设置了)。
  3. 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次 MinorGC 后进入老年代的对象的 平均大小。
  4. 如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次 FullGC,对老年代和年轻代一起回收一次垃圾。
  5. 如果回收完还是没有足够空间存放新的对象,就会发生 "OOM"。
  • 当然,如果 MinorGC 之后,剩余存活的需要挪动到老年代的对象大小 还是大于 老年代可用空间。
  1. 那么也会触发 FullGC。
  2. FullGC 完之后,如果还是没用空间放 MinorGC 之后的存活对象,则也会发生 "OOM"。

7. Eden 与 Survivor 区默认 8:1:1
  • 大量的对象被分配在 Eden 区,Eden 区满了后会触发 MinorGC。
    可能会有 99% 以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块 Survivor 区。
  • 下一次 Eden 区满了后又会触发 MinorGC。
    把 Eden 区和 Survivor 区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的Survivor 区。
  • 因为新生代的对象都是朝生夕死的,存活时间很短。
    所以 JVM 默认的 8:1:1 的比例是很合适的,让 Eden 区尽量的大,Survivor 区够用即可。

  • JVM 默认有这个参数(-XX:+UseAdaptiveSizePolicy),会导致这个比例自动变化。
    如果不想这个比例有变化,可以设置参数(-XX:-UseAdaptiveSizePolicy)。

二、如何判断对象可以被回收
  • 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

1. 引用计数法
  • 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1。
  • 当引用失效,计数器就减 1。
  • 任何时候计数器为 0 的对象,就是不可能再被使用的。

  • 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
  • 所谓对象之间的相互引用问题,如下面代码所示:除了对象 testA 和 testN 相互引用着对方之外,这两个对象之间再无任何引用。
  • 但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
public class ReferenceCountingGCTest {

    private ReferenceCountingGCTest instance = null;

    public static void main(String[] args) {
        ReferenceCountingGCTest testA = new ReferenceCountingGCTest();
        ReferenceCountingGCTest testB = new ReferenceCountingGCTest();
        testA.instance = testB;
        testB.instance = testA;
        testA = null;
        testB = null;
        
    }
}

2. 可达性分析算法
  • 这个算法的基本思想:就是通过一系列的称为 "GCRoots" 的对象作为起点。
    从这些节点开始向下搜索引用的对象,找到的对象都标记为 非垃圾对象,其余未标记的对象都是 垃圾对象。
  • "GCRoots" 根节点:线程栈的本地变量、静态变量、本地方法栈的变量 等等。

3. 常见引用类型
  • Java 的引用类型一般分为四种:强引用、软引用、弱引用、虚引用。

3.1 强引用
  • 强引用:普通的变量引用。
public static Person person = new Person();

3.2 软引用
  • 软引用:将对象用 SoftReference 软引用类型的对象包裹。
  • 正常情况不会被回收,但是 GC 做完后,发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。
  • 软引用可用来实现内存敏感的高速缓存。

  • 软引用在实际中有重要的应用,例如:浏览器的后退按钮。
  • 按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
  1. 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建。
  2. 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
public static SoftReference personSoftReference = new SoftReference<>(new Person());

3.3 弱引用
  • 弱引用:将对象用 WeakReference 弱引用类型的对象包裹。
    弱引用跟没引用差不多,GC 会直接回收掉,很少用。
public static WeakReference personWeakReference = new WeakReference<>(new Person());

3.4 虚引用
  • 虚引用:虚引用也称为 幽灵引用 或者 幻影引用,它是最弱的一种引用关系,几乎不用。

4. finalize() 方法最终判定对象是否存活
  • 即使在可达性分析算法中不可达的对象,也并非是 “非死不可” 的,这时候它们暂时处于 “缓刑阶段”,要真正宣告一个对象死亡,至少要经历再次标记过程。
  • 标记的前提是对象在进行可达性分析后,发现没有与 GCRoots 相连接的引用链。

  • 第一次标记并进行一次筛选。
  1. 筛选的条件是此对象是否有必要执行 finalize() 方法。
  2. 当对象没有覆盖 finalize() 方法,对象将直接被回收。
  • 第二次标记。
  1. 如果这个对象覆盖了 finalize() 方法,finalize() 方法是对象脱逃死亡命运的 最后一次机会。
  2. 如果对象要在 finalize() 中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可。
  3. 譬如:把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出 “即将回收” 的集合。
  4. 如果对象这时候还没逃脱,那基本上它就真的被回收了。
@Test
public void test2() throws InterruptedException {
    ArrayList list = new ArrayList<>();

    long i = 0;
    long j = 0;
    while (true) {
        list.add(new Person(String.valueOf(i++), 18));
        new Person(String.valueOf(j--), 20);
        Thread.sleep(10);
    }
    
}
 

5. 如何判断一个类是无用的类
  • 类需要同时满足下面三个条件,才能算是 “无用的类”
  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

三、JVM 调优概念
  1. 就是尽可能让对象都在 新生代 里分配和回收。
  2. 尽量别让太多对象频 繁进入老年代,避免 频繁对老年代进行垃圾回收。
  3. 同时给系统充足的内存大小,避免 新生代频繁的进行垃圾回收。

java -jar test.jar
# 堆`3G`
# 年轻代:`Eden=800M`、`S0=100M`、`S1:100M`
# 老年代:`Old=2G`
-Xms3G 
-Xmx3G 
-Xss1M 
-XX:MetaspaceSize=512M 
-XX:MaxMetaspaseSize=512M
java -jar test.jar
# 堆`3G`
# 年轻代:`Eden=1.6G`、`S0=200M`、`S1=200M`
# 老年代:`Old=1G`
-Xms3G 
-Xmx3G 
-Xmn2G 
-Xss1M 
-XX:MetaspaceSize=512M 
-XX:MaxMetaspaseSize=512M 
转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号