• JVM探究


    目录

    常见的面试题:

    (1)说出一个对象实例化的过程(如何创建一个对象)?

    (2)谈谈对JVM的理解?java8 虚拟机和之前的变化更新?

    (3)JVM的调优参数有哪些?

    (4)内存快照如何抓取?怎么分析Dump文件?

    (5)谈谈你对JVM中的类加载器的认识?

    (6)JVM的内存模型和分区,详细到每个区放什么?

    (7)堆里面的分区有哪些,说下他们特点?

    (8)GC算法有哪些?

    (9)轻GC和重GC分别在什么时候发生?

    1. JVM的位置

    2. JVM的体系结构

    3. 类加载器

    4. 双亲委派机制

    5. 沙箱安全机制(问的不多)

    6. Native本地方法栈

    7. PC寄存器

    8. 方法区

    9. 栈

    10. 三种JVM(了解)

    11. 堆Heap

    12. 新生区、老年区

    13. 永久区

    14. 堆内存调优 

    15. GC:垃圾回收机制

    GC常用算法

    引用计数法

     复制算法

    标记清除算法

    标记压缩算法

    16. JMM(Java Memory Model)

    常见的面试题:

    (1)说出一个对象实例化的过程(如何创建一个对象)?

    ①类加载检查:虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的 符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必 须先执⾏相应的类加载过程。

    ②分配内存:在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式指针碰撞空闲列表两种,选择哪种分配⽅式由 Java 堆是否规整决定,⽽ Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定

    ③初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。

    ④设置对象头:初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。

    ⑤执行init方法:在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。

    (2)谈谈对JVM的理解?java8 虚拟机和之前的变化更新?

    jvm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;

    ①程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;

    ②虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;

    ③本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;

    ④堆: java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;

    ⑤方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;

    • 什么是OOM?什么是栈溢出Stack OverFlowError?

    ①OOM:

    除了程序计数器,其他内存区域都有OOM的风险。

    • 栈一般经常会发生StackOverflowError,比如32位的windows系统单进程限制2G内存,无限创建线程就会发生栈的OOM
    • Java8常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;
    • 堆内存溢出,报错同上,这种比较好理解,GC之后无法在堆中申请内存创建对象就会报错;
    • 方法区OOM,经常会遇到的是动态生成大量的类、jsp 等;
    • 直接内存OOM,涉及到-XX:MaxDirectMemorySize参数和 Unsafe对象对内存的申请。

    ②栈溢出:

    • 栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
    • 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;
    • 调整参数-xss去调整jvm栈的大小

    (3)JVM的调优参数有哪些?

    • jps: JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
    • jstat: jstat(VM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
    • jmap: jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候自动生成dump文件。jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
    • jhat: jhat(VM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
    • jstack: jstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。

    (4)内存快照如何抓取?怎么分析Dump文件

    专业工具:内存快照分析工具:Jpro

    •  分析Dump内存文件,快速定位内存泄露;
    1. -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
    2. //-Xms 设置初始化内存分配大小,默认为电脑内存1/64
    3. //-Xmx 设置最大分配内存,默认为电脑内存1/4
    4. //-XX:PringGCDetails 打印GC垃圾回收信息
    5. //-XX:+HeapDumpOnOutOfMemoryError 打印OOM信息

    (5)谈谈你对JVM中的类加载器的认识?

    虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;

    类加载过程如下

    • 加载:加载分为三步:
      • 通过类的全限定性类名获取该类的二进制流;
      • 将该二进制流的静态存储结构转为方法区的运行时数据结构;
      • 在堆中为该类生成一个class对象;
    • 验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到 jvm 的安全;
    • 准备:为class对象的静态变量分配内存,初始化其初始值;
    • 解析:该阶段主要完成符号引用转化成直接引用;
    • 初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程;

    (6)JVM的内存模型和分区,详细到每个区放什么?

    同问题2

    (7)堆里面的分区有哪些,说下他们特点?

    堆里面分为新生代和老生代(java8取消了永久代,采用了Metaspace(元空间)),新生代包Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to,当经过一次或者多次GC之后,存活下来的对象会被移动到老年区,当JVM内存不够用的时候,会触发Full GC,清理JVM老年区。当新生区满了之后会触发YGC,先把存活的对象放到其中一个Survice区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把Eden 进行完全的清理,然后整理内存。那么下次GC 的时候,就会使用下一个Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为JVM 认为,一般大对象的存活时间一般比较久远。

    (8)GC算法有哪些?

    标记清除,标记整理(压缩),复制算法,引用计数

    具体见下文

    (9)轻GC和重GC分别在什么时候发生?

    新生成的对象首先放到年轻代Eden区,当 Eden空间满了,触发轻 GC,存活下来的对象移动到幸存0区,幸存0区满后触发执行轻 GC,幸存0区存活对象移动到幸存1区,这样保证了一段时间内总有一个幸存区为空。经过多次轻 GC仍然存活的对象移动到老年代。老年代存储长期存活的对象,占满时会触发重 GC,GC期间会停止所有线程等待GC完成,所以对相应要求高的应用尽量减少发生Major GC,避免响应超时。

    1. JVM的位置

    • JVM运行在操作系统之上,可以认为是一个软件;
    • JVM能运行的java程序是有限制的;
    • JVM是包含在java运行环境JRE中的;

    2. JVM的体系结构

    •  垃圾回收只会存在在堆中,方法区也是一个特殊的堆;

    •  JVM调优大部分也是在堆中调优;

    3. 类加载器

            作用:加载 Class 文件 

     1. 虚拟机自带的加载器

    2. 启动类(根)加载器

    3. 扩展类加载器

    4. 应用程序加载器

    4. 双亲委派机制

    双亲委派机制:是为了保证安全

    APP(应用程序加载器)----> EXC(扩展类加载器)----->BOOT(启动类(根)加载器)(最终执行)

    双亲委派机制流程:

    1. 类加载器收到类加载请求;
    2. 将这个请求向上委托给父类加载器去完成,一直向上请求,直到启动类(根)加载器;
    3. 启动类加载器检查是否能够加载当前这个类,如果能加载就结束,使用当前的加载器,否则抛出异常(Class Not Found),通知子加载器进行加载;
    4. 重复步骤③

    5. 沙箱安全机制(问的不多)

    Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境
    沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
    沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

    6. Native本地方法栈

    • 凡是带了native关键字的方法,说明java的作用范围达不到,需要回去调用底层c语言的库;
    • 会进入本地方法栈;
    • 调用本地方法接口 JNI
    • JNI 的作用是扩展 JAVA的适应,融合不同的编程语言为java所用;
    • 于是在内存中专门开辟了一块标记区域:Native Method Stack 登记 native方法;

    7. PC寄存器

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

    8. 方法区

    Method Area方法区
    方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
    静态变量Static常量final类信息(构造方法、接口定义)运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。

    9. 栈

    数据结构:先进后出;

    队列:先进先出;

    栈:占内存,主管程序运行,生命周期和线程同步;

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

    一旦线程结束,栈就Over。

    栈中存放的内容:

    • 8大类型
    • 对象引用
    • 实例的方法

    栈运行的原理:栈帧

    这就可以解释程序为啥从main方法运行了,因为main方法是第一个进栈的,会在栈底,其他的方法都在栈顶,每次运行完都会被弹出,直至所有方法都运行结束,main方法也会被弹出;如果方法互相调用,就会陷入死循环,导致栈溢出~

    如果栈满了,会抛出StackOverFlowError

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

    一个对象实例化的过程: (最好能默写出来)

    •  Step1:类加载检查
      • 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
    • Step2:分配内存
      • 类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
      • 选择上面两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
      • 指针碰撞
        • 使用场合:堆内存规整(堆内没有内存碎片)
        • 原理:将用过的内存整合到一边,没用过的内存整合到另一边,中间设置一个分界指针,只需要沿着没用过内存的方向将该指针移动对象内存大小位置即可。
        • GC收集器:Serial、ParNew
      • 空闲列表
        • 使用场合:堆内存不规整(堆内有内存碎片)
        • 原理:虚拟机会维护一个列表,该列表中记录那些内存块是可以用的,在分配的时候,找一块足够大的内存块划分给实例对象,最后更新列表记录。
        • GC收集器:CMS
      • 内存分配并发问题:在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
        • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
        • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
    • Step3:初始化零值
      • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
    • Step4:设置对象头
      • 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
      • 对象头分为两部分:Mark Word(运行时元数据)和类型指针。

        • Mark Word:用于存储对象自身的运行时数据信息,如上述的哈希码、GC分代年龄、线程持有的锁等。

        • 类型指针:类型指针指向类的元数据,虚拟机通过这个指针确定该类在哪个变量中

    • Step4:执行init方法
      • 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行  方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
    • 对象的内存布局图

    10. 三种JVM(了解)

    • Sun公司:HotSpot(我们学的)
    • BEA公司:JRockit
    • IBM公司:J9VM

    11. 堆Heap

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

    类加载器读取了类文件后,一般会把什么东西放在堆中?类,方法,常量,变量,保存我们所有引用类型的真实数据。

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

    • 新生区(伊甸园区)younger
    • 养老区 old
    • 永久区 perm

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

    假设内存满了,OOM,堆内存不够-----》java.lang.OutOfMemoryError:java heap space

    在 jdk8 以后,永久存储区改了个名字(元空间);

    12. 新生区、老年区

    新生区

    • 类:诞生和成长的地方,甚至死亡
    • 伊甸园区:所有对象都是在伊甸园区new出来的
    • 幸存者区:

    13. 永久区

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

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

    jdk1.6以前:永久代,常量池是在方法区中

    jdk1.7:永久代,慢慢的退化了,去永久代,常量池在堆中

    jdk1.8:无永久代,常量池在元空间

     逻辑上存在,物理上不存在

    新生代 + 老年代 = 总空间(通过下面结果发现元空间确实是在物理上不存在)

    14. 堆内存调优 

    默认情况下:分配的总内存是电脑内存的 1/4,而初始化的内存是 1/64

    遇到OOM该如何排除?

    1. 尝试扩大堆内存,看结果
    2. 分析内存,看哪个地方出现了问题(专业工具:内存快照分析工具:Jpro)

    内存快照分析工具:Jpro作用:

    • 分析Dump内存文件,快速定位内存泄露;
      1. -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
      2. //-Xms 设置初始化内存分配大小,默认为电脑内存1/64
      3. //-Xmx 设置最大分配内存,默认为电脑内存1/4
      4. //-XX:PringGCDetails 打印GC垃圾回收信息
      5. //-XX:+HeapDumpOnOutOfMemoryError 打印OOM信息
    •  

    • 分析堆中的数据;
    • 获得大的对象~

     修改代码堆存分配为8M,打印结果:

    15. GC:垃圾回收机制

    GC的作用区:垃圾回收大部分发生在堆和方法区(方法区本质也是堆)

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

    • 新生代
    • 幸存区(from、to)就是所说的会发生交换的 0 区和 1 区
    • 老年区

    GC分为两种类型:

    • 轻GC(普通GC)
    • 重GC(全局GC)

    GC常用算法

    引用计数法

    给每个对象分配一个计数器,用一次加一次,清理计数为 0 的对象,下图中 C 就会被干掉~

     复制算法

    • 首先将伊甸园GC一次,将活着的对象放到幸存区;
    • 每次进行G的时候L将幸存区 from 的对象复制到幸存区 to ,可以理解为:谁空谁是to;(每一次垃圾回收之后Eden和一个幸存区 to 区 都是空的)
    • GC过程中,然后两个幸存区的对象调换,每次保证一个幸存区为空,在一个对象经历(默认)15 (也可以自行设定次数)次之后,如果对象还活着,将进入老年区;

    • 好处:没有内存碎片;
    • 坏处:浪费了内存空间~ 因为一直会有一半空间是空的,对象存活度较低的时候比较适合。

    标记清除算法

    扫描整个对象空间并对存活的对象标记,然后清除没有标记的对象。

    • 优点:不需要额外的空间;
    • 缺点:两次扫描浪费时间,会产生内存碎片。

    标记压缩算法

    是标记清除的再优化!

    压缩:通过再次扫描,减少内存碎片,将空的坑位平移。

     最后两张算法可以合并为一种算法——标记-清除-压缩算法

    但是扫描时间长的缺点还是没有克服!

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

    16. JMM(Java Memory Model)

    1. 什么是JMM?Java内存模型
    2. JMM是干什么的?

            是缓存一致性协议,用于定义数据读写的规则。JMM定义了线程工作内存和主内存之间的抽象关系∶线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

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

  • 相关阅读:
    STM32SDIO外设详解
    Google Coding style
    极速指南:在 SpringBoot 中快速集成腾讯云短信功能
    初识EFCore
    Linux常用基本命令详解(二)-------磁盘分区和磁盘管理类命令
    JAVA Swing 与 GUI Form
    【排序算法】详解冒泡排序及其多种优化&稳定性分析
    猿创征文|Java-泛型(Generic)
    Java Lambda 表达式
    直播回顾 | 云原生混部系统 Koordinator 架构详解(附完整PPT)
  • 原文地址:https://blog.csdn.net/weixin_44564247/article/details/126459475