zoukankan      html  css  js  c++  java
  • JVM 基础、堆内存分析和垃圾回收算法

    文章首发我的博客,欢迎访问:https://blog.itzhouq.cn/jvm

    首先基本的面试题都是下面的夺命连环问,感受一下。

    • 请你谈谈你对 JVM 的理解。java8 虚拟机和之前有什么变化?
    • 什么是 OOM, 什么是栈溢出 StackOverFlowError? 怎么分析?
    • JVM 的常用调优参数有哪些?
    • 内存快照如何抓取,怎么分析 Dump 文件?你知道吗?
    • 谈谈 JVM 中,你对类加载器的认识?

    这篇文章先大体梳理一下相关的知识点,后面再整理一篇基本面试题相关的,先挖个坑。要说明的是,文章中很多地方关于概念是一带而过的,难免有部分内容没有说明白。对于不明白的点,建议自己动手查查相关资料,决不能指望一篇笔记就能把 JVM 搞明白,这显然也是不可能的。

    1、JVM 的位置

    可以看到 JVM 是 JRE 的一部分。主要工作是解释自己的字节码并映射到本地的 CPU 指令集和 OS 的系统调用。Java 语言是跨平台的,不同的操作系统会有不同的 JVM 映射规则,这就使得 Java 语言与操作系统无关。

    2、JVM 的体系结构

    3、类加载器

    作用:加载 Class 文件,比如我们 new Student() 的时候,Student 是类,是抽象的,使用 new 关键词创建对象实例,实例的引用是在栈中,而具体的人是放在堆中。

    3.1、类的实例化和双亲委派机制

    public class Car {
        public static void main(String[] args) {
            // 类是模板,对象是具体的
            Car car1 = new Car();
            Car car2 = new Car();
            Car car3 = new Car();
    
            System.out.println(car1.hashCode()); // 460141958
            System.out.println(car2.hashCode()); // 1163157884
            System.out.println(car3.hashCode()); // 1956725890
    
            Class<? extends Car> aClass1 = car1.getClass();
            Class<? extends Car> aClass2 = car2.getClass();
            Class<? extends Car> aClass3 = car3.getClass();
    
            System.out.println(aClass1.hashCode()); // 685325104
            System.out.println(aClass2.hashCode()); // 685325104
            System.out.println(aClass3.hashCode()); // 685325104
        }
    }
    

    3.2、类加载

    类加载器:

    1. 虚拟机自带的加载器
    2. 启动类(根)加载器
    3. 扩展类加载器
    4. 应用程序加载器

    试验:自己定义一个String类,看是否能执行

    package java.lang;
    
    public class String {
        // 双亲委派机制:安全
        // BOOT --> EXT --> APP (最终执行)
        // BOOT
        // EXT
        // APP
        public String toString () {
            return "Hello";
        }
    
        public static void main(String[] args) {
            String s = new String();
            System.out.println(s.toString());
        }
        
        // 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
        //   public static void main(String[] args)
        //否则 JavaFX 应用程序类必须扩展javafx.application.Application
        
        /**
         * 类加载的流程
         * 1. 类加载器收到类加载的请求
         * 2. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
         * 3. 启动节加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,
         	否则,抛出异常通知子加载器进行加载
         * 4. 重复步骤3
         */
    
    }
    

    百度:双亲委派机制

    4、沙盒安全机制

    Java安全的模型的核心就是 Java 沙箱 (sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限制在虚拟机特定的运行环境中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统操作破坏。沙箱主要限制系统资源访问

    5、Native

    开启一个多线程启动类:

    public static void main(String[] args) {
        new Thread(() -> {
    
        }, "my thread name").start();
    }
    

    点进去查看start()方法的源码:

    public synchronized void start() {
        /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
    
        /* Notify the group that this thread is about to be started
             * so that it can be added to the group's list of threads
             * and the group's unstarted count can be decremented. */
        group.add(this);
    
        boolean started = false;
        try {
            start0(); // 调用start0()方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                      it will be passed up the call stack */
            }
        }
    }
    
    private native void start0();
    

    可以看到源码中使用了特殊的方法 start0(),使用了 native关键词。

    native :凡是带了 native 关键词,说明 java 的作用范围达不到了,会调用底层 C 语言的库!
    native 的方法会进入本地方法栈,调用本地方法接口 JNI(Java Native Interface 本地方法接口),其他的就是 Java 栈。
    
    JNI的作用:扩展 java 的使用,融合不同的编程语言为 java 所用!最初的时候需要融合 C 和 C++。
    		Java 诞生的时候, C 和 C++ 横行,想要立足,必须要有调用 C 和 C++的程序。
    		它在内存区域中专门开辟了一块标记区域: Native Method Stack ,登记 native 方法,
    		在最终执行的时候,加载本地方法库中的方法通过 JNI。
    比如: Java程序驱动打印机,管理系统。这部分掌握即可,在企业级应用中较为少见。
    
    现在调用第三方语言接口的方式很多,比如:Socket、WebService、HTTP等。
    

    6、PC 寄存器

    程序计数器:Program Counter Register

    每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎下读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

    7、方法区

    Method Area 方法区

    方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间

    **静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关 ** 。

    static、final、Class、常量池。

    8、栈

    栈是一种数据结构,可以形象地理解为一个水桶或水杯,其特点是先进后出。 比如,依次将乒乓球放入杯子中,先放进去的球,最后才能拿出来。

    栈中存放 8 大基本数据类型 + 对象引用 + 实例的方法。

    栈内存主管程序的运行,生命周期和线程同步。Java 中执行方法的过程就是调用栈的过程。为什么 main() 方法,最先执行,最后结束呢?因为main() 方法是程序的入口,执行时 main() 会最先被压到栈底, 在 main() 方法中调用其他方法时,依次将其他方法压入栈中。

    线程结束,栈内存就释放了,对于栈来说,不存在垃圾回收问题。

    栈的运行原理:

    栈的运行原理

    栈 + 堆 + 方法区的交互关系

    栈堆方法区的交互关系

    画一个对象实例化的过程在内存中:百度、看视频。

    JVM的内存区域划分,对象实例化分析

    JVM系列分析- 内存模型

    9、三种 JVM

    • Sun 公司 HotSpotJava HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
    • BEA:JRockit
    • IBM:J9VM

    我们学习的都是HotSpot

    10、堆

    Heap,一个 JVM 只有一个堆内存,堆内存的大小是可以调节的。

    类加载器读取类文件后,一般会把什么东西放入堆中?类、方法、常量、变量,保存所有引用类型的真实对象。

    堆内存中还要细分为三个区域:

    • 新生区(伊甸园区 Eden):Young/New
    • 养老区:old
    • 永久区:Perm

    堆内存的细分

    新生区中没有被垃圾收集器干掉的对象会进入幸存区0区幸存区0区中没有被干掉的对象会进入幸存区1区幸存区0区幸存区1区会不停的交换位置。经过一定次数后的垃圾回收后还没有被干掉的对象会进入养老区,这个区域的对象一般不会被干掉,但不是绝对的。假设养老区满了,对象会进入永久存储区

    针对新生区的垃圾回收称为轻量级的垃圾收集,也称轻 GC。针对养老区的垃圾回收称为重量级的垃圾收集,也称重 GC。

    GC 垃圾回收,主要是在伊甸园区和养老区。

    假设内存满了,会报错 OOM,OutOfMemeroy,对内存不够!

    public static void main(String[] args) {
        String str = "hello world!";
    
        while (true) {
            str += str + new Random().nextInt(88888888) + new Random().nextInt(99999999);
        }
        //        Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        //        at java.util.Arrays.copyOf(Arrays.java:3332)
        //        at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
        //        at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
        //        at java.lang.StringBuilder.append(StringBuilder.java:208)
        //        at Hello.main(Hello.java:8)
    }
    

    在 JDK8 以后,永久存储区改名为元空间。

    11、新生区、老年区

    新生区:

    • 类:诞生和成长的地方,甚至死亡;
    • 伊甸园,所有对象都是在伊甸园区 new 出来的;
    • 幸存者区:0 和 1 区。
    • 经过研究,99%的对象都是临时对象,所以进入老年区的对象很少。

    12、永久区

    这个区域常驻内存的。用来存放 JDK 自身携带的 Class 对象。Interface 元数据,存储的是 Java 运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭 VM 虚拟机就会释放这个区域的内存。

    一个启动类,加载了大量的第三方 jar 包。Tomcat 部署了太多的应用,大量动态生成的反射类。不断的被加载,知道内存满,就会出现 OOM。

    • JDK1.6:永久代,常量池存放在方法区;
    • JDK1.7:永久代,但是慢慢的退化了,常量池在堆中;
    • JDK1.8:无永久代,常量池在元空间。

     堆空间内存模型

    13、堆内存调优

    public static void main(String[] args) {
        // 返回 JVM 试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory(); // 字节
    
        // 返回 JVM 的初始化总内存
        long total = Runtime.getRuntime().totalMemory();
    
        System.out.println("max=" + max + "字节	" + (max / (double)1024/1024) + "MB");
        // max=2831679488字节	2700.5MB
        System.out.println("total=" + max + "字节	" + (total / (double)1024/1024) + "MB");
        // total=2831679488字节	182.5MB
    
        // 默认情况下:分配的从内存是电脑内存的 1/4, 而初始化内存是电脑内存的 1/64。
    }
    

    元空间在逻辑上存在,物理上不存在。

    OOM 解决方案:

    1. 尝试扩大堆内存看结果;
    2. 分析内存,看一下哪个地方出现了问题(专业工具)。

    堆内存调优

    14、使用 JProfier 工具分析 OOM 原因

    在一个项目中,突然出现了 OOM 故障,那么该如何排除,研究为什么出错?

    • 能够看到代码第几行出错:内存快照分析工具,MAT(Eclipse)、JProfiler
    • Debugger,一行行分析代码。

    MAT 、JProfiler的作用:

    • 分析 Dump 内存文件,快读定位内存泄漏;
    • 获得堆中的数据;
    • 获得大的对象;
    • 。。。。。。

    JProfiler 插件和Windows客户端安装百度。

    配置JProfiler

    编写一个 OOM 的程序测试

    public class Demo03 {
        byte[] array = new byte[1 * 1024 * 1024]; // 1MB
    
        public static void main(String[] args) {
            ArrayList<Demo03> list = new ArrayList<>();
            int count = 0;
    
            try {
                while (true) {
                    list.add(new Demo03());
                    count ++;
                }
            } catch (Exception e) { // 错误写法
                e.printStackTrace();
            }
        }
    //    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    //    at Demo03.<init>(Demo03.java:4)
    //    at Demo03.main(Demo03.java:12)
    }
    

    这个程序出现了 OOM,但是从报错信息无法看出哪里的问题。

    此时需要添加一些配置,打印一些信息。

    配置Dump参数

    Dump文件

    通过 JProfiler 工具打开文件:

    线程Dump

    大对象

    总结:
    	// -Xms 设置初始化内存分配的大小  默认1/64
        // -Xmx 设置最大分配内存    默认 1/4
        // -XX:+PrintDCDetails  // 打印GC垃圾回收信息
        // -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError    OOM  Dump(转储文件)
    

    15、GC : 常用算法

    GC 的作用区域:

    GC的作用区域

    JVM 在进行 GC 时,并不是对这三个区域统一回收,大部分时候,回收都是新生区。

    • 新生区
    • 幸存区:from to
    • 老年区

    GC 的分类:

    轻 GC(普通的 GC):主要针对新生区,偶尔对幸存区进行 GC。

    重 GC(全局 GC):把上面所有的区域都进行 GC,也就是释放内存。

    堆内存中的幸存区是可以交换位置的。

    堆内存中幸存区

    GC 的题目:

    • JVM 的内存模型和分区,详细到每个区放什么;
    • 堆里面的分区有哪些? Eden、from、to、Old,说说他们的特点;
    • GC 的算法有哪些?标记清除法、标记压缩、复制算法、引用计数器,怎么用的?
    • 轻 GC 和重 GC 分别在什么时候发生?

    引用计数器:(用得少)

    引用计数法

    复制算法:幸存区的复制

    GC复制算法

    GC复制算法2

    GC 复制算法:

    好处:没有内存碎片;

    坏处:浪费了空间内存,多了一半空间(to)永远都是空的。极端情况下,比如对象 100% 存活,这个缺点就很明显。

    复制算法最佳使用场景:对象存活度较低,比如新生区。

    标记清除算法

    优点:不需要额外的空间!

    缺点:两次扫描,严重浪费时间,会产生内存碎片。

    标记清除算法

    标记压缩

    对标清除进行再优化。

    标记压缩算法

    标记清除压缩

    再次优化上述算法,可以多次进行标记清除,进行一次标记压缩。

    16、总结:

    • 内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
    • 内存整齐度:复制算法 == 标记压缩算法 > 标记清除算法
    • 内存利用率:标记压缩算法 == 标记清除算法 > 复制算法

    思考一个问题:难道没有一个最优的算法吗?

    答案:没有,没有最好的算法,只有最合适的算法---> GC:分代收集算法

    年轻代:

    • 存活率低
    • 复制算法

    老年代:

    • 区域大,存活率低
    • 标记清除(内存碎片不是太多)+ 标记压缩混合实现。

    参考书籍:《深入理解 JVM》

    17、JMM(高频) 和 快速学习方法

    1. 什么是 JMM?

    JMM:Java Memory Model 的缩写。

    1. 它是干嘛的?

    作用:缓存一致性协议,用于定义数据读写的规则。

    JMM 定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(Local Memory)。

    解决共享对象可见性这个问题: volilate

    1. 它该如何学习?

    官方、其他人的博客、对应的视频。。。

    Java内存模型(JMM)总结

    关于内存图:可以去思维导图 : processon 网站搜索 JVM 可以看到别人画的相关思维导图。


    参考文章

  • 相关阅读:
    JAVA学习日报 7.24
    JAVA学习日报 7.23
    JAVA学习日报 7.22
    【刷题-LeetCode】275. H-Index II
    【刷题-LeeetCode】260. Single Number III
    【刷题-LeetCode】240. Search a 2D Matrix II
    【刷题-LeetCode】239. Sliding Window Maximum
    【刷题-LeetCode】238. Product of Array Except Self
    【经验总结】VSCode中找不到numpy/matplotlib/pillow,navigator没了
    【刷题-LeetCode】236. Lowest Common Ancestor of a Binary Tree
  • 原文地址:https://www.cnblogs.com/itzhouq/p/jvm.html
Copyright © 2011-2022 走看看