class="hljs-ln-code"> class="hljs-ln-line"> public static void main(String[] args){
  • class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="3"> class="hljs-ln-code"> class="hljs-ln-line"> Student stu = new Student();
  • class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="4"> class="hljs-ln-code"> class="hljs-ln-line"> stu.study();
  • class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="5"> class="hljs-ln-code"> class="hljs-ln-line"> stu.hashCode();
  • class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="6"> class="hljs-ln-code"> class="hljs-ln-line"> stu = null;
  • class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="7"> class="hljs-ln-code"> class="hljs-ln-line"> }
  • class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="8"> class="hljs-ln-code"> class="hljs-ln-line">}
  • class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">

    我们自己编写的一段Java代码,指的就是Java Source(源程序),将其编译后,其执行流程如下所示:

    执行 javac 命令编译源代码为字节码

    执行 java 命令

    1. 创建 JVM,调用类加载子系统加载 class(这里就是Demo类),将类的信息存入方法区

    2. 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码。

    3. 如果遇到了未见过的类(这里指的是Student类),会继续触发类加载过程,同样会存入方法区

    4. 需要创建对象(比如这里的 new Student()),会使用内存来存储对象。

    5. 不再使用的对象,会由垃圾回收器在内存不足时回收其内存。

    6. 调用方法时,方法内的局部变量、方法参数(这里指的就是stu、args)所使用的是 JVM 虚拟机栈中的栈帧内存。

    7. 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行。

    8. 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续。

    9. 对于非 java 实现的方法调用,使用内存称为本地方法栈(比如这里的hashCode(),但是这只是这么设计,Oracle公司的HotSpot虚拟机就没有分这么细,而是将两者合二为一都叫做虚拟机栈

    10. 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能。

    JVM内存结构的线程分类

    线程私有

    程序计数器、JVM虚拟机栈(虚拟机栈、本地方法栈)

    线程共享

    堆、方法区

    2. 内存溢出

    什么是内存溢出?什么时候会发生?

    该区域的内存被耗尽了、报错了,就是内存溢出!

    除了程序计数器,其他的区域都有可能发生内存溢出。

    (1)出现 OutOfMemoryError 的情况

    (2)出现 StackOverflowError 的区域

    3. 方法区与永久代、元空间之间的关系

    所以总的来说,方法区是JVM规范中的一种定义(即在内存中需要有能实现其功能的区域),而永久代元空间是方法区的具体实现。

    (1)元空间的元数据创建

    下图中,“箭头”代表时间线,“Heap”就是堆内存,“Metaspace”就是元空间。

    (2)元空间的元数据删除

    二、JVM 内存参数

    我们使用idea编写代码的时候,肯定有查看过其配置文件,里面有jvm内存参数的配置。

    面试题:-Xmx1024m -Xms10240m -Xmn10240m -XX:SurvivorRatio=4 其最小内存只是多少?Survivor区总大小分别是多少?

    最小堆内存和最大堆内存大小都是10GB,新生代内存大小为5GB(总共10GB,新5GB,老5GB)

    new 表示的就是新生代from to 的内存大小总是相等的。

    -XX:SurvivorRatio=4 表示 eden占4份内存,from和to各占 1 份,所以Survivor区总大小就是from + to的总大小即 5GB / 6(份) * 2

    (1)堆内存,按比例设置

    参数含义如下 

    (2)堆内存,按大小设置

    (3)元空间内存设置 

    要将元空间细分,可以将其分成两类,例如:class space non-class space

    class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制。 

    注意:

    (4)代码缓存内存设置(codecache)

    之前提到的 即时编译器 会使用到这个东西! 

    (5)线程内存设置

    参数就是 -Xss

    它的大小与操作系统有关,如果是Linux默认一个线程内存大小是 1MB

    三、JVM 垃圾回收

    1. 三种垃圾回收算法

    (1)标记清除法

    解析

    要点

    (2)标记整理法

    解释

    特点

    (3)标记复制法

    解析 

    特点

    三种垃圾回收算法总述

    标记清除法已经废弃,标记整理法 和 标记复制法 都还在使用!

    标记整理法 主要用于老年代的垃圾回收

    标记复制法 主要用于新生代的垃圾回收

    新生代存活对象比较少,老年代则反之;所以新生代使用 标记复制法 ,需要复制的对象就少,效率高!

    2. GC 与 分代回收算法

    (1)GC 垃圾回收

    GC是垃圾回收的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。

    GC 目的 

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

    GC 超重点

    (2)GC规模

    根据 GC 的规模可以分成 Minor GCMixed GCFull GC

    (3)分代回收

    新生代 = eden + from + to

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

    B. 当伊甸园内存不足,标记伊甸园from(现阶段没有存活对象

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

    D. 将 from 和 to 交换位置

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

    F. 标记伊甸园与 from存活对象

    G. 将存活对象采用复制算法复制到 to

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

    I. 将 from 和 to 交换位置

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

    3. 三色标记

    之前我们提到了,怎么看该对象是否还在被使用,就是将被使用的对象标记起来,不被标记的对象将会被视为垃圾,被回收。所以我们用三种颜色记录对象的标记状态

    (1)起始的三个对象还未处理完成,用灰色表示

    (2)该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色

    (3)依次类推

    (4)沿着引用链都标记了一遍

    (5)最后为标记的白色对象,即为垃圾

    4. 并发漏标问题

    用户编写的业务逻辑代码是通过用户线程来运行的,而JVM中有垃圾回收线程,在早期这两种线程是不允许同时工作的!

    但是现在比较先进的垃圾回收器都支持与用户线程并发运行,都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。  

    问题描述

    (1)所示标记工作尚未完成

    (2)用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾

    (3)但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标

    (4)如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题

    解决方案 

    因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:

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

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

    5. 垃圾回收器

    垃圾回收器 - Parallel GC

    垃圾回收器 - ConcurrentMarkSweep GC(基本废弃)

    垃圾回收器 - G1 GC(优秀)

    从JDK9开始,作为默认的垃圾回收器 

    6. G1 GC详解(重点)

    G1 回收阶段 - 新生代回收

    (1)初始时,所有区域都处于空闲状态

    (2)创建了一些对象,出一些空闲区域作为 eden 伊甸园区存储这些对象

    (3)当 eden 伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程(由于新生代大小受限,幸存对象较少,所以时间很短!)

    注意:

    eden区的大小会受到新生代大小的限制

    在G1里面新生代的内存占比是在 5% ~ 60%之间波动的!

    eden区的数量达到阈值,就会触发垃圾回收。

    (4)复制完成,将之前的伊甸园内存释放

    (5)随着时间流逝,伊甸园的内存又有不足

    (6)将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代

    (7)释放伊甸园以及之前幸存区的内存

    G1 回收阶段 - 并发标记与混合收集  

    并发标记阶段的发生是有条件的!当老年代的占用内存超过阈值(45%)才会发生

    (1)当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程

    (2)并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。

    此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)即先回收存活对象少的老年区

    假设图中标红的区域表示存活对象少的老年代

    (3)混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制

    (4)下图显示了老年代(红)幸存区晋升的存活对象的复制

    eden 未达到晋升预值的幸存区复制到 新的幸存区(蓝色的s)” 

    达到晋升预值的幸存区回收价值较高的老年代 会复制到一个 新的老年代

    (5)复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集

    JVM面试专题 下

    Java虚拟机(JVM)面试专题 下(初级程序员P6)_面向鸿蒙编程的博客-CSDN博客icon-default.png?t=M85Bhttp://iyenn.com/rec/1724020.html

    data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://harmony.blog.csdn.net/article/details/126694782","extend1":"pc","ab":"new"}">>
    注:本文转载自blog.csdn.net的金鳞踏雨的文章"https://blog.csdn.net/weixin_43715214/article/details/126694782"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
    复制链接

    评论记录:

    未查询到任何数据!