JAVA面试八股文宝典(黑马学习随笔)--虚拟机篇

学习随笔简介

跟随着黑马满老师的《Java八股文面试题视频教程,Java面试八股文宝典》学习,视频教程地址: https://www.bilibili.com/video/BV15b4y117RJ?p=104&share_source=copy_web&vd_source=12ab4e2292b9162c982607a3ca0075ba

共分为四个部分,分别是 基础篇、并发篇、虚拟机、框架篇 

本篇更新虚拟机篇的内容

目录

学习随笔简介

一、JVM内存结构

1.内存结构划分

2.内存溢出的区域

3.方法区、永久代、元空间

二、JVM内存参数

1.堆内存设置

2.元空间内存设置

3.代码缓存内存设置

三、JVM垃圾回收

1.三种垃圾回收算法 

2.GC与分代回收算法 

3.并发漏标问题

4.垃圾回收器

四、内存溢出

1.误用线程池导致的内存溢出

2.查询数据量太大导致的内存溢出

3.动态生成类导致的内存溢出


一、JVM内存结构

1.内存结构划分

1.类加载子系统:在运行程序时首次运行类时进行  加载——连接——初始化

2.可运行数据区:

【注意】主要可以分为两个部分:共享区和独占区。共享区主要包括方法区和堆区,独占区包括程序计数器、虚拟机栈和本地方法栈

  • 方法区: 存放已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
  • 堆:存放对象、数组、非静态变量
  • 程序计数器:可以正确控制Java程序中的流程控制,正确轮换多线程
  • 虚拟机栈:每个方法对应一个栈帧,栈帧包含局部变量表、操作数栈、动态链接、方法返回值等
  • 本地方法栈:不是使用Java实现的函数,用来支持本地方法的调用逻辑的

3.执行引擎:

  • 解释器:作用就是读取字节码,并且逐一执行
  • 即时编译器:由于一段代码被多次调用时,都需要解释执行,即时编译器可以将这样的字节码编译成本地代码,用于多次的重复调用来提高效率
  • 垃圾回收:收集并删除未引用的对象

4.本地方法接口:

  • 与本地方法库进行交互,提供执行引擎所需要的本地库

5.本地方法库:

  • 执行引擎所需要的本地库的集合

2.内存溢出的区域

  • 不会出现内存溢出的区域——程序计数器
  • 出现 OutOfMemoryError 的情况:

    • 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收

    • 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类

    • 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时

  • 出现 StackOverflowError 的区域:

    • JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用

3.方法区、永久代、元空间

  • 方法区:JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  • 永久代:HotSpot虚拟机对JVM规范的实现(JDK1.8之前)
  • 元空间:HotSpot虚拟机对JVM规范的另一种实现(JDK1.8之后),使用本地内存作为这些信息的存储空间

  • 当第一次用到某个类的时候,由类加载器将class文件的类元信息读入,并存储于元空间
  • 类元信息是存储于元空间中,无法直接访问
  • 可以用 .class文件间接访问类元信息,它们两属于Java对象,我们在代码中可以使用

从这个图中看出来:

  • 堆内存中:当一个类加载器对象,这个类加载器对象加载的所有类对象,这些类对象对应的所有实例对象都没人引用时,GC就会对它们占用的内存进行释放
  • 元空间中:内存释放以类加载器为单位, 当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放


二、JVM内存参数

1.堆内存设置

按大小设置: 

 具体参数:

  • -Xms:最小堆内存( 包括新生代和老年代)
  • -Xmx:最大堆内存(包括新生代和老年代)
  • 通常建议将最小堆内存和最大堆内存设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
  • -XX:NewSize-XX:MaxNewSize设置新生代的最小与最大值,但一般不建议设置,由JVM自己控制
  • -Xmn:设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
  • 图中的保留是指一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存

按比例设置:

 具体参数:

  • -XX:NewRatio=2:1  表示老年代占两份,新生代占一份
  • -XX:SurvivorRatio=4:1 表示新生代分为6份,伊甸园占4份,from和to区各占一份

2.元空间内存设置

 具体参数:

  • class space:存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
  • non-class space:存储除类的基本信息以外的其他信息(如方法字节码、注解等)
  • class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制

3.代码缓存内存设置

 

具体参数:

  • 如果 -XX:ReservedCodeCacheSize < 240m ,所有优化机器代码不加区分存在一起
  • 否则,即 -XX:ReservedCodeCacheSize >= 240m,则分成三个区域(图中笔误 mthod 拼写错误,少一个 e)

    • non-nmethods - JVM 自己用的代码

    • profiled nmethods - 部分优化的机器码

    • non-profiled nmethods - 完全优化的机器码

三、JVM垃圾回收

1.三种垃圾回收算法 

 1.标记清除算法:

 算法解释:

  • 找到 GC Root 对象(那些一定不会被回收的对象,如正在执行方法内局部变量引用的对象、静态变量引用的对象)
  • 标记阶段:沿着 GC Root对象的引用链找,直接或间接引用到的对象加上标记
  • 清除阶段:释放未加标记的对象占用的内存 

 要点:

  • 标记速度与存活对象线性关系
  • 清除速度与内存大小线性关系
  • 缺点:会产生内存碎片,无法找到足够的连续内存

2.标记整理算法:

算法解释:

  • 前面的标记阶段、清理阶段与标记清除法类似
  • 相较之前,多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片的产生

特点:

  • 标记速度与存活对象成线性关系
  • 清除、整理速度与内存大小成线性关系
  • 缺点:移动对象极为负重,必须全程暂停用户应用程序才能进行;性能上较慢

3.标记复制算法:

算法解释:

  • 将整个内存分为两个大小相等的区域:from区 和 to区,其中 to区总是处于空闲,from存储新创建的对象
  • 标记阶段与前面的算法类似
  • 在找出存活对象后,会将它们从 from区复制到 to区,复制的过程中自然完成了碎片整理
  • 复制完成后,交换 from区 和 to区 的位置即可

特点:

  • 标记、复制速度与存活对象成线性关系
  • 缺点就是会占用成倍的空间,造成空间的浪费

2.GC与分代回收算法 

GC的目的:实现无用的对象内存自动释放,减少内存碎片、加快分配速度

GC的要点:

  • 回收区域是堆内存,不包括虚拟机栈
  • 判断无用的对象的方法有可达性分析算法、三色标记法,标记存活的对象,回收未标记的对象
  • GC的具体实现称为垃圾回收器
  • GC 大都采用了分代回收思想(建立在弱分代假说、强分代假说和跨代引用假说之上)

    • 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收

    • 根据这两类对象的特性将回收区域分为新生代和老年代,新生代采用标记复制法、老年代一般采用标记整理法

  • 根据 GC的规模可以分成 Minor GC、Mixed GC、Full GC

对于上述每个点在下面都有对应的解释:

1.判断无用的对象的方法

  • 可达性分析算法:从GC Roots为起点开始遍历整个对象图,和GC Roots直接或间接相连的对象才是存活对象,反之就是死亡对象。从GC Roots搜索过的路径叫做引用链。
  • 三色标记法:使用三种颜色表示对象的标记状态,分别是 黑色——已标记灰色——标记中,白色——未被标记

2.垃圾回收器的介绍放在下面具体说

3.分代回收

  • 伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代

  • 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象

  • 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放

  • 将 from 和 to 交换位置

  • 经过一段时间后伊甸园的内存又出现不足

  • 标记伊甸园与 from(现阶段没有)的存活对象

  • 将存活对象采用复制算法复制到 to 中

  • 复制完毕后,伊甸园和 from 内存都得到释放

  • 将 from 和 to 交换位置

  • 老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

4.GC 规模

  • Minor GC 发生在新生代的垃圾回收,暂停时间短
  • Mixed GC 对新生代和老年代的部分区域进行垃圾回收,G1垃圾收集器特有
  • Full GC 新生代和老年代完整垃圾回收,暂停时间长,应全力避免

3.并发漏标问题

目前比较先进的垃圾回收器都支持并发标记 ,即在标记过程中,用户线程仍然可以工作。如果用户线程修改了对象的引用,那么就会出现漏标问题。

解决办法:

  1. Incremental Update 增量更新法CMS 垃圾回收器采用

    • 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍

  2. Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用

    • 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理

    • 新加对象会被记录                        

    • 被删除引用关系的对象也被记录

4.垃圾回收器

1.Paraller GC(并行垃圾回收器)

  • eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
  • old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
  • 注重吞吐量的时候使用这种垃圾回收器

2.ConncurrentMarkSweep GC(CMS垃圾回收器)

 工作在 old 老年代,支持并发标记的一款回收器,采用标记清除算法

  • 并发标记时不需暂停用户线程
  • 重新标记仍需暂停用户线程

如果并发失败(回收速度赶不上创建新对象的速度),会触发 Full GC

注重响应时间的时候使用这种垃圾回收器

3.G1 GC

将整个堆内存划分为多个大小相等区域,每个区域都可以充当 eden,survivor,old,humongous(专为大对象准备)

分为三个阶段:新生代回收、并发标记、混合收集

如果并发失败,会触发 Full GC

响应时间和吞吐量兼顾

四、内存溢出

这里举几种典型的导致内存溢出的情况:

  • 误用线程池导致的内存溢出
  • 查询数据量太大导致的内存溢出
  • 动态生成类导致的内存溢出

1.误用线程池导致的内存溢出

 示例1:通过Executors自动创建 FixedThreadPool 线程池,代码如下:

 private static void case1() {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        LoggerUtils.get().debug("begin...");
        while (true){
            executor.submit(() -> {
                try {
                    LoggerUtils.get().debug("send sms...");
                    TimeUnit.SECONDS.sleep(30);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }

运行结果: 

17:31:44.144 [main] DEBUG G - begin...
17:31:44.148 [pool-1-thread-1] DEBUG A - send sms...
17:31:44.148 [pool-1-thread-2] DEBUG B - send sms...

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

 造成原因:

我们查看源码,可以发现其中使用的工作队列为 LinkedBlockingQueue,它是一个无界的工作队列,任务数量将队列塞满:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

解决办法:

我们在使用Executors自动创建线程的时候尽量不要使用  newFixedThreadPool 这种方式

示例2:通过 Executors 自动创建带缓存的线程池 (CachedThreadPool),代码如下:

static AtomicInteger c = new AtomicInteger();

    private static void case2() {
        ExecutorService executor = Executors.newCachedThreadPool();
        while (true){
            System.out.println(c.incrementAndGet());
            executor.submit(() -> {
                try {
                    TimeUnit.SECONDS.sleep(30);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }

这里测试可以学习视频中,在linux系统中修改进程和线程的最大数来进行测试,这里建议最好不要在Windows下直接测试

造成原因:

我们查看源码,可以发现其中使用的工作队列为 SynchronousQueue ,它创建的线程是没有数量限制的,创建的线程数量过多,耗尽系统的线程资源 :

  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

解决办法:

 我们在使用Executors自动创建线程的时候尽量不要使用  newCachedThreadPool 这种方式

 

2.查询数据量太大导致的内存溢出

 演示代码:

public class TestOomTooManyObject {
    public static void main(String[] args) {
        //对象本身内存
        long a = ClassLayout.parseInstance(new Product()).instanceSize();
        System.out.println(a);
        // 一个字符串占用内存
        String name = "联想小新Air14轻薄本 英特尔酷睿i5 14英寸全面屏学生笔记本电脑(i5-1135G7 16G 512G MX450独显 高色域)银";
        long b = ClassLayout.parseInstance(name).instanceSize();
        System.out.println(b);
        String desc = "【全金属全面屏】学生商务办公,全新11代处理器,MX450独显,100%sRGB高色域,指纹识别,快充(更多好货)";
        long c = ClassLayout.parseInstance(desc).instanceSize();
        System.out.println(c);
        System.out.println(16 + name.getBytes(StandardCharsets.UTF_8).length);
        System.out.println(16 + desc.getBytes(StandardCharsets.UTF_8).length);
        // 一个对象估算的内存
        long avg = a + b + c + 16 + name.getBytes(StandardCharsets.UTF_8).length + 16 + desc.getBytes(StandardCharsets.UTF_8).length;
        System.out.println(avg);
        // ArrayList 24, Object[] 16 共 40
        System.out.println((1_000_000 * avg + 40) / 1024 / 1024 + "Mb");
    }

    static public class Product {
        private int id;
        private String name;
        private int price;
        private String desc;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }
    }
}

演示结果:

# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.memory.Universe.getNarrowOopBase()
32
24
24
144
157
381
363Mb

 可以看出,占用的内存很大,在高并发的情况下,就有可能出现内存溢出

3.动态生成类导致的内存溢出

演示代码:

public class TestOomTooManyClass {

    static GroovyShell shell = new GroovyShell();

    public static void main(String[] args) {
        AtomicInteger c = new AtomicInteger();
        while (true) {
            try (FileReader reader = new FileReader("script")) {

                shell.evaluate(reader);
                System.out.println(c.incrementAndGet());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这里就不放演示结果了(结果太长)

造成原因:

GroovyShell对象无法被回收,导致对象中的  GroovyClassLoader 类加载器无法被回收,导致元空间的内存无法被释放,直至溢出

解决办法:

将静态变量的GroovyShell改为方法中的局部变量,循环完一次对象不再使用就可以回收其内存,然后就可以回收对象中的类加载器,这样就可以回收其所占的内存

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
黑马Java面试八股文是指在Java开发岗位面试中常被问到的一些基础知识点和常见问题。这些问题涵盖了Java语言的核心概念、面向对象编程、集合框架、多线程、IO流、数据库等方面。回答这些问题可以帮助面试者展示自己的Java基础知识和编程能力。 以下是一些常见的黑马Java面试八股文问题及其答案: 1. Java语言的特点有哪些? - 简单易学 - 面向对象 - 平台无关性 - 安全性 - 高性能 2. 什么是面向对象编程? 面向对象编程是一种编程范式,它将数据和操作数据的方法封装在一起,通过创建对象来实现对数据的操作和管理。 3. Java中的四种访问权限修饰符是什么? - public:公共访问权限,可以被任何类访问。 - protected:受保护访问权限,可以被同一包内的类和子类访问。 - default:默认访问权限,可以被同一包内的类访问。 - private:私有访问权限,只能被本类访问。 4. 什么是多态性? 多态性是指同一操作作用于不同的对象,可以有不同的解释和不同的执行结果。它通过父类或接口的引用指向子类的对象实现。 5. 什么是Java中的抽象类和接口? - 抽象类是一种不能被实例化的类,它可以包含抽象方法和非抽象方法。子类继承抽象类时,必须实现抽象方法。 - 接口是一种完全抽象的类,它只包含常量和抽象方法。类可以实现多个接口,实现接口的类必须实现接口中定义的所有方法。 6. 什么是Java中的异常处理制? 异常处理制是一种用于处理程序运行过程中出现的异常情况的制。Java中的异常分为可检查异常和不可检查异常,通过try-catch-finally语句块来捕获和处理异常。 7. 什么是Java中的线程?如何创建线程? 线程是程序执行的最小单位,它可以独立运行并与其他线程并发执行。在Java中,可以通过继承Thread类或实现Runnable接口来创建线程。 8. 什么是Java中的集合框架?常用的集合类有哪些? 集合框架是Java提供的一组用于存储和操作数据的类和接口。常用的集合类有ArrayList、LinkedList、HashSet、HashMap等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值