写点什么

假期后来一波干货:一文理清 JVM 和 GC

用户头像
比伯
关注
发布于: 2021 年 04 月 07 日
假期后来一波干货:一文理清JVM和GC

本文主要介绍 JVM 和 GC 解析如有需要,可以参考如有帮助,不忘 点赞 ❥

创作不易,白嫖无义!

一、JVM 内存体系

其中方法区被 JVM 中多个线程共享,比如类的静态常量就被存放在方法区,供类对象之间共享。

虚拟机栈本地方法栈程序计数器是每个线程独立拥有的,不会与其他线程共享。

所以 Java 在通过 new 创建一个类对象实例的时候,一方面会在虚拟机栈中创建一个对该对象的引用,另一方面会在堆上创建类对象的实例,然后将对象引用指向该对象的实例。对象引用存放在每一个方法对应的栈帧中。


  • 虚拟机栈:虚拟机栈中执行每个方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

  • 本地方法栈:与虚拟机栈发挥的作用相似,相比于虚拟机栈为 Java 方法服务,本地方法栈为虚拟机使用的 Native 方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

  • 方法区:它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,方法区在 JDK1.7 版本及之前称为永久代,从 JDK1.8 之后永久代被移除。

  • 堆:堆是 Java 对象的存储区域,任何 new 字段分配的 Java 对象实例和数组,都被分配在了堆上,Java 堆可使用 - Xms 和-Xmx 进行内存控制,从 JDK1.7 版本之后,运行时常量池从方法区移到了堆上。

  • 程序计数器:指示 Java 虚拟机下一条需要执行的字节码指令。

二、JAVA8 之后的 JVM

从图中我们可以看出 JAVA8 的 JVM 用元空间取代了永久代




三、GC 作用域


四、常见垃圾回收算法

引用计数法:

JVM 的实现一般不采用这种方式


缺点

  • 每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗;

  • 较难处理循环引用;

复制算法:

Java 而从 GC 的角度可以细分为:新生代(Eden 区、From Survivor 区 和 To Survivor 区)和 老年代。特点:复制算法不会产生内存碎片,但会占用空间。用于新生代。


MinorGC 的过程(复制 --> 清空 --> 互换)

  1. 复制: (Eden、SurvivorFrom 复制到 SurvivorTo,年龄加 1)


    首先,当 Eden 区满的时候会触发第一次 GC,把还活着的对象拷贝到 SurvivorFrom 区,当 Eden 区再次触发 GC 的时候会扫描 Eden 区域和 From 区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到 To 区域(如果有对象的年龄已经到达了老年的标准,则复制到老年代区),同时把这些对象的年龄加 1。

  2. 清空:(清空 Eden、SurvivorFrom)


    清空 Eden 和 SurvivorFrom 中的对象,也即复制之后有交换,谁空谁是 to。

  3. 互换:(SurvivorTo 和 SurvivorFrom 互换)


    最后,SurvivorTo 和 SurvivorFrom 互换,原 SurvivorTo 成为下一次 GC 是的 SurvivorFrom 区。

标记清除法

算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些。特点:不会占用额外空间,但会扫描两次,耗时,容易产生碎片,用于老年代


标记压缩法

优点:没有内存碎片,可以利用 bump 缺点:需要移动对象的成本,用于老年代原理

标记:与标记清除一样


压缩:再次扫描,并往一段滑动存活对象


五、判断对象是否可回收

引用计数法

Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然的一个方法就是通过引用计数来判断一个对象是否可以回收。简单来说就是给对象添加一个引用计数器。每当有一个地方引用它,计数器的值加 1,每当有一个引用失效时,计数器的值减 1。任何时刻计数器值为 0 的对象就是不可能再被使用的,那么这个对象就是可回收对象。缺点

很难解决对象之间相互循环引用的问题

枚举根节点做可达性分析(根搜索路径)

所谓 GC roots 或者说 tracing GC 的 根集合 就是一组必须活跃的引用。基本思路就是通过一系列名为 GC Root 的对象作为起始点,从这个被称为 GC Roots 的对象开始向下搜索

如 GC Roots 没有任何引用链相连是,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系


哪些可以做 GCRoots 对象

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)

  • 方法区中的类静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中 N(Native 方法)引用的对象

六、JVM 的参数类型

1)标配参数

  • java -version

  • java -help


2)X 参数

  • java -Xint -version :解释执行

  • java -Xcomp -version :第一次使用就编译成本地代码

  • java -Xmixed :混合模式


3)XX 参数

  • Boolean 类型

-XX:+ 或者 - 某个属性值(+:表示开启,-:表示关闭)例子:-XX: +PrintGCDetails: 开启打印 GC 收集细节-XX: -PrintGCDetails: 关闭打印 GC 收集细节-XX: +UseSerialGC: 开启串行垃圾收集器-XX: -UseSerialGC:关闭串行垃圾收集器

  • KV 设置类型

-XX: 属性 key = 属性 value 例子:-XX: MetaspaceSize = 128m:设置元空间大小为 128m -XX:MaxTenuringThreshold = 15:控制新生代需要经历多少次 GC 晋升到老年代中的最大阈值

  • jinfo -查看当前运行程序的配置

公式:jinfo -flag 配置项 进程编号例子

  1. 查看初始堆大小:


2.查看其他参数


3.查看使用哪种垃圾回收器


两个经典参数

  • -Xms 等价于 -XX: InitialHeapSize

  • -Xmx 等价于 -XX: MaxHeapSize

七、查看 JVM 默认值

  • -XX:+PrintFlagsInitial: 查看默认初始值

java -XX: +PrintFlagsInitial -version

java -XX: +PrintFlagsInitial


  • -XX:+PrintFlagsFinal 查看修改更新

  • java -XX:+PrintFlagsFinal

  • java -XX:+PrintFlagsFinal -version

  • java -XX:+PrintCommandedLineFlags


八、常用的配置参数

经典案例设置:-Xms128m -Xmx4096m -Xss1024k -XX:Metaspacesize=512m -XX:+PrintCommandLineFlags -XX:PrintGCDetails -XX:UseSerialGC

  • -Xms

初始化大小内存,默认为物理内存 1/64 等价于 -XX:InitialHeapSize

  • -Xmx

最大分配内存,默认为物理内存 1/4 等价于 -XX:MaxHeapSize

  • -Xss

设置单个线程的大小,一般默认为 5112K~1024K 等价于 -XX:ThreadStackSize

  • -Xmn

设置年轻代大小

  • -XX:MetaspaceSize

设置元空间大小

元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制

  • -XX:+PrintGCdetails

输出详细的 GC 收集日志信息

  • -XX:SurvivorRatio

设置新生代中 eden 和 S0/S1 空间的比例默认:-XX:SurvivorRatio=8 --> Eden:S0:S1=8:1:1 修改:-XX:SurvivorRatio=4 --> Eden:S0:S1=4:1:1SurvivorRatio 值就是设置 eden 区的比例占多少,S0/S1 相同

  • -XX:NewRatio

设置年轻代与老年代在堆结构的占比默认:-XX:NewRatio=2: 新生代占 1,老年代占 2,年轻代占整个堆的 1/3 修改:-XX:NewRatio=4: 新生代占 1,老年代占 4,年轻代占整个堆的 1/5NewRatio 值就是设置老年代的占比,剩下的 1 给新生代

  • -XX:MaxTenuringThreshold

设置垃圾最大年龄-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。

如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入老年代。对于老年代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象在年轻代的存活时间,增加年轻代被回收的概论。

九、强软弱虚



1)强引用

  • 当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算出现了 OOM 也不会对该对象进行回收,`死都不收`

  • 强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的。即使该对象以后永远都不会被用到,JVM 也不会回收。 因此强引用是造成 Java 内存泄漏的主要原因之一。

  • 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般就是认为可以被垃圾收集(具体看垃圾收集策略)

public static void main(String[] args) {        Object o1 = new Object();   //默认为强引用        Object o2 = o1;     //引用赋值        o1 = null;          //置空 让垃圾收集        System.gc();        System.out.println(o1);     // null        System.out.println(o2);     // java.lang.Object@1540e19d    }复制代码
复制代码

2)软引用

  • 软引用就是一种相对强引用弱化了一些的引用。需要用 java.lang.ref.SoftReference 类来实现,可以让对象豁免一些垃圾收集。

  • 系统内存充足 -> 不会回收

  • 系统内存不足 -> 会回收

  • 软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收

    public static void main(String[] args) {        Object o1 = new Object();        SoftReference softReference = new SoftReference(o1);        o1 = null;        System.gc();        System.out.println(o1);        System.out.println(softReference.get());    }复制代码
复制代码

3)弱引用

  • 弱引用需要用 java.lang.ref.WeakReference 类来实现,它比软引用的生存期更短

  • 对于弱引用的对象,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存。

    public static void main(String[] args) {        Object o1 = new Object();        WeakReference weakReference = new WeakReference(o1);        o1 = null;        System.gc();        System.out.println(o1);                     //null        System.out.println(weakReference.get());    //null    }复制代码
复制代码

5)虚引用

  • 虚引用需要 java.lang.ref.PhantomReference 类来实现。

  • 形如虚设,它不会决定对象的生命周期。

  • 如果一个对象持有虚引用,那么它就和没有任何一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它来访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。

  • 虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。PhantomReference 的 get()方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 gc 回收,用来实现比 finalization 机制更灵活的回收操作。

public static void main(String[] args) {        Object o1 = new Object();        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();        PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);        System.out.println(o1);                         //java.lang.Object@1540e19d        System.out.println(phantomReference.get());     //null        System.out.println(referenceQueue.poll());      //null    }复制代码
复制代码

扩展:软弱引用适用场景

假如有一个引用需要读取大量的本地图片存在问题

  1. 如果每次读取图片都从硬盘读取则会严重影响性能。

  2. 如果一次性全部加载到内存中有可能造成内存溢出。

解决思路:用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题。Map<String,SoftReference> imgMap = new HashMap<String,SoftReference>()

WeakHashMap:

public static void main(String[] args) {        WeakHashMap<Integer,String> weakHashMap = new WeakHashMap<>();        Integer key = new Integer(1);        weakHashMap.put(key,"测试1");        System.out.println(weakHashMap);    //{1=测试1}        key=null;        System.out.println(weakHashMap);    //{1=测试1}        System.gc();        System.out.println(weakHashMap+"\t"+weakHashMap.size());    //{} 0    }复制代码
复制代码



看完不赞,都是坏蛋

原文链接:https://juejin.cn/post/6844904116272136200

发布于: 2021 年 04 月 07 日阅读数: 25
用户头像

比伯

关注

还未添加个人签名 2020.11.09 加入

还未添加个人简介

评论

发布
暂无评论
假期后来一波干货:一文理清JVM和GC