JVM认识与优化

认识

JVM定义

虚拟的操作系统,用来承载运行java程序

JVM功能

  • 分配应用程序的线程与对象等分配内存空间
  • 加载类
  • 解析执行编译后的字节码文件

JVM组成

  • 类加载子系统
  • 运行时数据区
  • 字节码执行引擎
    在这里插入图片描述

类加载过程

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的
    main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的
    java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如
    main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过
    程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下
    节课会讲到动态链接
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块
    在这里插入图片描述

JVM内存模型

内存模型主:堆、栈、本地方法栈、方法区(元空间)、程序计数器。

  • 堆:线程所共享区域几乎所有通过new创建的实例对象都会被分配在该区域。

对象一定分配在堆空间吗????????

上面说了几乎所有对象都是在堆空间分配内存,也就是说并不一定。在1.7以前的JDK版本的确是所有对象都在堆空间分配,JDK1.7 及以后,如果对象没有发生逃逸,并且对象的大小满足一定条件。它将会直接在当前方法的栈帧中分配内存,这种特性称为 “逃逸分析”。什么叫发生了逃逸?

1.方法内的对象 被作为参数传递给其他其他方法

2.方法内的对象 作为方法的返回值

当发生这两种情况时,我们称为对象发生了逃逸,除此之外,没有发生逃逸并且对象的大小满足在栈帧分配的条件,这些对象将会直接分配在栈中。事实上程序执行过程中很大一部分对象都是未发生逃逸的

堆一定是线程共享的吗?????????

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-Local Allocation Buffer)。默认设定为占用Eden Space的1%。为什么会有 TLAB?因为 堆 是线程共享数据,多线程环境下可能会出现多个线程申请空间分配对象,这样一来每一次分配对象都得考虑同步,会影响效率。进而诞生了 TLAB分配,JVM 会在 Eden 区划分出一小块区域,为每一个线程创造一个 TLAB 小空间用来分配线程私有对象。由于 TLAB 空间很小,所以大对象无法分配在 TLAB。由此可见,堆一定是线程共享的吗?那么以后就可以回答堆中的 TLAB 空间并不是
  • 栈(虚拟机栈):也可以称为虚拟机线程栈,它是JVM中每个线程所私有的一块空间,每个线程都会有这么一块空间。它的生命周期是与线程的生命周期是绑定的。虚拟机栈描述了Java中方法执行时的内存模型,即每个方法被执行的时候,线程都会在自己的线程栈中同步创建一个栈帧,用于存放局部变量表、操作数栈、动态连接和方法出口等信息,每个方法从调用到完成的过程,就对应着一个栈帧在线程栈中从入栈到出栈的过程。

  • 本地方法栈:本地方法栈与虚拟机栈的作用是相似,不同的是虚拟机栈为JVM执行的Java方法服务,而本地方法栈为JVM调用的本地方法服务

  • 程序计数器:只需要占用一小块的内存空间,每个线程都会有自己独立的程序计数器,主要功能就是记录当前线程执行到哪一行指令了,可以看作是当前线程所执行的字节码行号指示器。

  • 方法区(元空间):在JDK 8之前,方法区也称之为永久代,这部分区域与堆一样,是所有线程所共享的,它主要用于存放被虚拟机加载的类型信息、常量、静态变量以及即时编译器编译后的代码缓存等数据。对于一个Class文件,除了版本、字段、方法、接口等描述信息外,还有常量池表,主要用于编译器生成的各种字面量和符号引用,而这部分内容在Class文件加载后是存放在方法区的运行时常量池中。这个运行时常量池自然还包括了字符串常量池,但需要注意的是,在JDK 7以后的版本中,字符串常量池和静态变量等被移至到了Java堆区,而到了JDK 8,抛弃了之前永久代的概念,通过在本地内存中实现了元空间(Meta-space)来代替永久代,并把JDK 7中永久代剩余内容(主要是类型信息)全部移至到了元空间。

  • 示例

public class Test {

    public int compute(){
        int a = 4;
        int b = 5;
        int c = a * b * 6;
        return c;
    }

    public static void main(String[] args) {
        Test test = new Test();
        int compute = test.compute();
        System.out.println(compute);
    }
}

在这里插入图片描述

当执行Test类的main方法时,首先会将Test.class文件加载进JVM的方法区,主要有常量池和方法定义,而常量池中此时包含一个compute()的符号引用,而在执行main方法,当调用compute()方法时,会将compute()的符号引用变成该方法具体在方法区的内存地址(直接引用),因为这个过程是在程序运行时发生的,所以称为动态链接

在执行main()和compute()方法时会分别在main线程栈中创建两个栈帧,因为main()先调用,所以其栈帧处于栈的底部。

JVM堆内存划分

在这里插入图片描述

JVM堆分为新生代和老年代两个区域,在默认情况下。这两个区域的大小比例为 1:2。新生代中又分为三部分,Eden、Survivor 0、Survivor 1。默认情况下,这三个区域的大小比例为 8:1:1

垃圾回收机制

当实例化一个对象时,会将对象首先分配在Eden区,如果Eden区满了,此时会执行一次 Minor GC ,将 Eden 区中存活的(还需要被程序使用的)对象复制一份到 S0 区,然后清除掉 Eden 区所有对象,对象的分代年龄 +1。当 Eden 区再次满的时候,此时会再执行 Minor GC,把 S0 中分代年龄大于 15 的对象转入老年代,将 Eden 和 S0 区的存活对象复制一份到 S1 ,如果 S1 放不下,剩下的对象直接转入老年代,然后清空 Eden 和 S0 区。后面循环这个步骤在 S0 和 S1 之间复制存活对象。

在这里插入图片描述

垃圾回收算法

  • 标记-清除

首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象

在这里插入图片描述

  • 复制算法
    将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复

在这里插入图片描述

  • 标记–整理算法
    因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法

在这里插入图片描述

垃圾回收条件根据

  • 可达性分析
    通过一系列称为"GC Roots"的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当GC Roots到对象不可达时证明此对象是不可用的
  • 引用计数算法

垃圾收集器

在这里插入图片描述

  • Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)
    Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
    新生代采用复制算法,老年代采用标记-整理算法。
    在这里插入图片描述
    虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短
    (仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
    但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial
    收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
    Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5
    以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

  • Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
    Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-
    XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。
    Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停
    顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel
    Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
    新生代采用复制算法,老年代采用标记-整理算法。
    在这里插入图片描述
    Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及
    CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集
    器)。

  • ParNew收集器(-XX:+UseParNewGC)
    ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
    新生代采用复制算法,老年代采用标记-整理算法。
    在这里插入图片描述
    它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

  • CMS收集器(-XX:+UseConcMarkSweepGC(old))
    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程
    (基本上)同时工作

    从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面
    几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
    初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
    并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但
    是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的
    对象状态发生改变。
    重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对
    象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三
    色标记里的增量更新算法(见下面详解)做重新标记。
    并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑
    色不做任何处理(见下面三色标记算法详解)。
    并发重置:重置本次GC过程中的标记数据。
    在这里插入图片描述从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:
    对CPU资源敏感(会和服务抢资源);
    无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
    它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
    执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并
    发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent
    mode failure",此时会进入stop the world,用serial old垃圾收集器来回收

  • 会产生多标与漏标

多标-浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过
(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动
垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental
Update) 和原始快照(Snapshot At The Beginning,SATB) 。
增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之
后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向
白色对象的引用之后, 它就变回灰色对象了。
原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
可以参考 https://zhuanlan.zhihu.com/p/664929804

  • 总结

(Java虚拟机)中常见的垃圾收集器有以下几种,它们在垃圾回收算法、内存分配策略、吞吐量等方面存在一些区别:

Serial收集器:

单线程收集器,使用“标记-复制”算法 适用于单核、小内存的环境 停顿时间较长,适合对响应时间要求不高的应用 Parallel收集器:

多线程收集器,使用“标记-复制”或“标记-清除”算法 适用于多核、大内存的环境
在GC时会暂停所有用户线程,适合对吞吐量要求较高、对延迟要求不敏感的应用
CMS(Concurrent Mark Sweep)收集器:

多线程收集器,使用“标记-清除”算法 以获取最短回收停顿时间为目标,采用并发标记和并发清除两个阶段进行
对响应时间要求较高,但在某些情况下可能导致碎片问题
G1(Garbage-First)收集器:

并发、分代的垃圾收集器,使用“标记-整理”算法 采用区域化的内存布局,根据应用需求动态调整回收区域 目标是实现低延迟和高吞吐量

优化

工具

  • jmap (对象)
  • jstack(线程)
  • jprofiler
  • arthas

案例

1.使用命令获取dump文件
使用jmap命令

jmap -dump:format=b,file=service.hprof 28201

报错无命令

yum install java-1.8.0-openjdk-devel.x86_64 -y

2.使用可视化工具查看分析
jprofiler工具进行分析

在这里插入图片描述

具体案例:排除应用程序内存占用过高
1)top 命令查看进程
2)jmap -dump:format=b,file=service.hprof 28201
3)运用jporile工具查看最大对象
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫果冻

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值