Java:JVM深入理解

JVM出现是为了解决什么问题?
  • JVM出现的原因

Windows系统上的软件包后缀是exe,在苹果系统上无法安装。相应的苹果上的安装包是dmg后缀,无法在Windows安装。为啥捏?
exe后缀的软件代码最终编译成Windows系统能识别的机器码,苹果系统亦然。那有没有一个办法可以让一套代码在不同系统上运行?

在这里插入图片描述

  • JAVA为什么可以跨平台?JVM做了什么?

那Java代码为什么可以在Linux、Windows、Mac OSX系统上运行?Java代码不直接编译成机器码而是编译成字节码,JVM虚拟机再解析字节码翻译成操作系统能理解的机器码!所以Java完美实现了跨平台!

在这里插入图片描述

代码进入JVM后都发生了什么

要知道一个Java类的数据类型非常丰富,不同的数据放哪?基本数据类型放哪?对象放哪?函数放哪?常量放哪?数据又是怎么处理的?

  • 运行数据区

在这里插入图片描述

线程私有:虚拟机栈、本地方法栈、程序计数器
线程共享:方法区、堆

先拆解再串起来:
  • 程序计数器

程序计数器是很小的一块内存空间,主要用来记录各个线程执行的字节码的地址,执行到哪儿了(指向当前线程正在执行的字节码指令的地址)。分支、跳转、循环、线程恢复都依赖于计数器。与时间片轮询有关。程序计数器是JVM唯一不会OOM的区域。

  • 虚拟机栈
    • 数据结构是栈,先进后出,只执行栈顶的栈帧。
    • Java虚拟机栈是基于线程的,栈的生命周期与线程是一样的。栈里的每条数据都是一条栈帧,每个栈帧对应一个函数(或方法)。函数调用时都会创建一个栈帧压栈,用完后出栈。所有的栈帧出栈时线程也就结束了。
    • 栈帧:局部变量表、操作数栈、动态连接、返回地址
      • 局部变量表:记录局部变量的表,存储基础类型和对象的引用
      • 操作数栈:CPU计算,操作数栈记录运算的结果,计算的数据来源于局部变量表和堆中的对象
      • 动态连接:Java语言的多态(运行时确定具体方法)
      • 返回地址:正常返回(调用程序计数器的地址作为返回),异常(异常处理表)

在这里插入图片描述

  • 本地方法栈

本地方法栈与虚拟机栈结构相同。区别在于虚拟机栈管理Java函数调用,本地方法栈管理本地方法的调用。HotSpot已把这两区域合二为一

  • 方法区

HotSpot使用永久代实现方法区。方法区主要用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、静态常量、运行时常量池、字符串常量池

堆是JVM上最大的内存区域,我们申请的几乎所有的对象都在这里。我们常说的GC也是针对这块区域

  • 直接内存

与NIO有关,不是JVM规范的一部分


串起来:代码进入JVM后发生了什么
1. JVM根据配置参数或默认配置参数向操作系统申请内存空间。
2. JVM根据配置参数分配堆、栈、方法区内存。
3. 在将.java转为.class文件时,收集所有类初始代码(包括静态变量赋值语句、静态代码块、静态方法、静态变量、常量)放入方法区。
4. 执行方法。启动main()线程,执行main()方法。new出来的对象放在堆中,对象的引用在栈中。每个方法对应一个栈帧。当执行到一个方法时栈帧压栈,方法内部的局部变量如果是基础数据类型则在局部变量表保存;如果是类则对象存在堆中,对象的引用存在局部变量表中。对数据操作时,所需的变量或引用进入操作数栈CPU进行运算。此时如果调用了其他方法,则将新的栈帧压栈做同样的操作。待计算结束后方法返回地址返回退出的位置。当虚拟机栈的栈帧都执行完毕弹出栈时线程也就执行完了。
内存溢出

每块区域分配的空间都是有限的,什么会造成内存溢出?怎么解决这种溢出?

  • 栈溢出

    • 无限递归(栈帧太大)
    • 不断建立线程
  • 堆溢出

    • 申请内存空间,超过最大堆内存空间
  • 方法区溢出

    • 运行时常量池溢出
    • 保存的Class对象没有及时回收掉或Class信息占用的内存超过配置
  • 本机直接内存溢出

那怎么解决?
  • 栈的优化技术—栈帧之间数据共享

在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的JVM在实现中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了。

在这里插入图片描述
在这里插入图片描述

堆中的数据

在这里插入图片描述

  1. 对象的创建过程
    1. 虚拟机接收new命令时先检查是否被类加载器加载,如果没有则先把class加载到JVM
    2. 检查加载:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查类是否已经被加载、解析和初始化过。
    3. 分配内存:把一块确定大小的内存从Java堆中划分出来
      1. 指针碰撞:规整的空间挨个放
      2. 空闲列表:凌乱的空间找地放
      3. 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
    4. 内存空间初始化:虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
    5. 设置:虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes在Java hotspot VM内部表示为类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
    6. 对象初始化:在上面工作都完成之后,从虚拟机的视角来看一个新的对象已经产生了。但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,接着把对象按照程序员的意愿进行初始化(构造方法),这样一个真正可用的对象才算完全产生出来。

在这里插入图片描述
2. 对象的访问定位

  1. 句柄:Java堆中划出一块内存作为句柄池。句柄中包含了对象实例数据与类型数据各自的具体地址信息。

  2. 直接指针:reference中存储的直接就是对象地址。

  3. 这两种对象访问方式各有优势:使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改;使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。HotSpot使用直接指针访问方式进行对象访问。
    在这里插入图片描述

  4. 判断对象的存活:内存空间是有限的,垃圾回收时怎么判断这个对象是否还被使用着呢?哪些已经使用完了呢?

    1. 引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效时,计数器减1
    2. 可达性分析:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
    3. Finalize方法:即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。但是这个方法比较废物,一般不用~
  5. 各类引用

    1. 强引用:在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象
    2. 软引用 SoftReference:将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收,图片可以用这个,别的不常用
    3. 弱引用 WeakReference:GC时会被回收,用的比较多
    4. 虚引用 PhantomReference:最弱,随时会被回收掉。垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作

在这里插入图片描述

对象的分配策略
  1. 可否分配在栈上?
    1. 逃逸分析:如果对象只在本线程使用则存放在栈中,该对象的生命周期跟随线程不需要垃圾回收,频繁调用时可以很大提高性能;如果被其他线程使用,则存放在共享区域的堆中
  2. 堆中怎么放?
    1. 堆内存分为两块,一块是新生代,一块是老年代,内存空间比例 新生代:老年代=1:2。新生代内存又分为三部分Eden、From、To,内存空间比例8:1:1
      1. 为什么Eden、From、To的比例是8:1:1?
        1. 经大量研究数据显示,80%的对象都是朝生夕死,换言之大多数对象只使用一次就会失去引用
        2. 第一次GC没被回收的进入From或To区,兜兜转转不死的可以进入老年代
      2. 为啥新生代与老年代的比例是1:2?
        1. 考虑一个极端情况,如果新生代的一个没死,全进入老年代咋办~再加之本身他持有一些对象,所以老年代必然是比新生代大的
        2. 新生代内存满了触发young GC,对新生代进行垃圾回收;老年代内存满了触发full GC,对整个堆进行垃圾回收
    2. 对象优先进入Eden区
    3. 大对象直接进入老年代
      1. 典型的大对象是很长的字符串和数组
      2. 避免大量内存复制
      3. 避免提前进行垃圾回收
    4. 对象年龄动态判定(新生代的对象满足什么条件才能进入老年代?)
      1. 一般而言对象经历 MaxTenuringThreshold 次GC不死就能进入老年代
      2. 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
    5. 空间分配担保(新生代的对象要进入老年代,老年代的内存够吗?)
      1. 在发生Minor GC前虚拟机会检查老年代最大可用的连续内存是否大于新生代所有对象的总空间,若此条件成立课确保Minor GC是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
垃圾回收算法
  1. 复制算法(新生代)
    1. 原理:将内存空间分为两部分,每次只使用一块。当前这块满了把还存活的对象移到另一块内存上,本块内存一次清理(类似格式化)
    2. 专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
    3. HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
    4. 优劣(空间换效率)
    1. 好处:不需要考虑内存碎片
    2. 坏处:浪费内存
  2. 标记清除算法
    1. 原理:算法分为“标记”、“清除”两个阶段,首先标记处要清除的对象,在标记完成后统一回收被标记过的对象
    2. 优劣:
    1. 坏:容易产生内存碎片,效率不稳定
    2. 好:速度快
  3. 标记整理算法
    1. 首先标记出所有要回收的对象,然后让所有存活的对象像一端移动,之后清理掉端边界外的内存
    2. 优劣:
    1. 劣:速度慢,效率低
    2. 有:没有内存碎片
下图为标记清除算法图

在这里插入图片描述


下图为标记整理算法

在这里插入图片描述

Stop The World(STW)

  1. 原理:在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在From和To之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。native代码可以执行,但不能和JVM交互。
  2. 什么情况会触发:
    1.老年代空间不足
    2. 永生代(jkd7)或者元数据空间(jkd8)不足。
    3. System.gc()方法调用。
    4. YoungGC时晋升老年代的内存平均值大于老年代剩余空间
    5. 有连续的大对象需要分配
    6. 死锁检查
  3. 危害:
    1. 长时间服务停止,没有响应
    2. 新生代的gc时间比较短,危害小
    3. 老年代的gc有时候时间短,但是有时候比较长几秒甚至100秒–几十分钟都有
    4. 堆越大花的时间越长
常用垃圾回收器
CMS在这里插入图片描述

CMS广范应用于B/S系统的服务端,系统停顿时间短,用户体验更好!CMS基于“标记-清除”算法实现。

  1. 实现过程分4个步骤:

    1. 初始标记:标记GC Roots能直接关联的对象,时间很快
    2. 并发标记:和用户的应用程序同时进行,进行GC Roots追踪的过程,标记从GCRoots开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)
    3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
    4. 并发清除:由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
  2. 优劣

    1. 优:并发收集、低停顿
    2. 劣:
      1. CPU敏感:CMS对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足4个时,CMS对用户的影响较大。
      2. 浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
      3. 空间碎片:“标记-清除”算法会导致产生不连续的空间碎片
  3. 如果分配不了大对象,就进行内存碎片的整理过程。这个地方一般会使用Serial Old ,因为Serial Old是一个单线程,所以如果内存空间很大、且对象较多时,CMS发生这样情况会很卡。

在这里插入图片描述

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值