【JVM】JVM

2 篇文章 0 订阅

JVM

  • JVM的组成及流程

    JVM包括运行时数据区、类加载器、执行引擎、本地库接口

    首先编译器将Java代码转为字节码,类加载器再把字节码加载到JVM内存即运行时数据区的方法区内,再由执行引擎将字节码翻译为底层指令,交由CPU执行,这个过程需要调用其他语言的本地库接口。

  • JVM运行时数据区

    首先是线程独占的,包括程序计数器、Java虚拟机栈、本地方法栈

    ​ 程序计数器:记录当前线程所执行字节码指令的地址,以便切换线程时可以恢复到正确的执行位置

    ​ Java虚拟机栈:每个方法被执行时,Java虚拟机都会创建一个栈帧用于存储局部变量表、操作数栈、方法出口、动态链接等。每一个方法被调用到执行完毕就对应着一个栈帧在Java虚拟机栈中入栈到出栈的过程,正在执行的方法位于栈顶。线程请求的栈的深度超过了虚拟机所允许的深度会抛出StackOverflowError异常;Java虚拟机栈动态扩展时无法申请到足够内存会抛出OutOfMemoryError异常。局部变量表包括8种基本数据类型,指向对象或对象句柄的引用,以及指向一条字节码指令的地址。

    ​ 本地方法栈:和Java虚拟机栈相似,Java虚拟机栈时为虚拟机执行Java方法,即字节码服务;而本地方法栈为虚拟机执行本地方法,即native方法服务。

    然后还有线程共享的,Java堆和方法区

    ​ Java堆:用于存放对象实例和数组,被各个线程共享使用,占据着最大的内存空间,由垃圾收集器回收管理,可以通过-Xmx和-Xms设定堆的大小。

    ​ 方法区:存储被虚拟机加载的类型信息、常量、静态变量等。运行时常量池就是方法区的一部分,存放各种字面量,例如final变量,和符号引用,就是指类和接口的全限定名、字段名、方法名、访问修饰符等。反射机制获取的类型、方法名、字段名就是从方法区得来的。jdk1.8后方法区被元空间metaspace替代,类增强时会产生大量class文件,可能出现OOM异常,就可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置元空间大小。

  • 什么情况下会发生堆溢出?什么情况下会发生栈溢出?

    栈溢出StackOverflowError,可能导致栈溢出的情况就是线程调用方法请求Java虚拟机栈的深度超过了虚拟机所允许的深度。一般出现这种情况就是代码中出现了方法递归,导致某个方法无限被调用,致使Java虚拟机栈内存不足;或者就是编写的功能会调用大量的方法。解决办法就是改善代码或者通过**-Xss**设置虚拟机栈的大小。

    堆溢出OutOfMemoryError,可能导致堆溢出的原因就是对象过多。一般是因为代码中创建了太多的对象,可以通过改善代码,或-Xmx和-Xms设置堆的大小解决。

  • 对象在堆内存中的分配

    当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数能否在方法区的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

    指针碰撞:

    假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距。

    空闲列表:

    如果Java堆中的内存并不是规整的,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例。

    选择哪种分配方式又Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理的能力决定。

  • 并发分配的线程安全问题

    由于Java堆是线程共享的区域,在并发情况下可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。有两种解决方案:

    同步锁定:

    对分配内存空间的动作进行同步处理,虚拟机采用CAS配上失败重试的方式保证内存分配的线程安全;

    本地线程分配缓冲:

    每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

  • 对象的访问定位

    Java程序会通过栈上的reference(指向对象的引用)数据来操作堆上的具体对象。具体访问方式有两种:

    使用句柄:

    如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了指向堆中对象实例数据的指针与指向方法区中对象类型数据的指针。

    直接指针:

    如果使用直接指针访问,引用中存储的直接就是对象实例数据地址,实例数据中再包含指向方法区中对象类型数据的指针。如果只是访问对象本身的话,就减少了一次间接访问的开销。

  • 对象是否存活判断
    1.引用计数算法

    在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时候计数器值为零的对象就是不可能再被使用的。

    • 优点:实现简单,效率高
    • 缺点:无法解决对象相互循环引用的问题——会导致对象的引用虽然存在,但是已经不可能再被使用,却无法被回收。
    2.可达性分析算法

    通过一系列的称为”GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots不可达(也就是不存在引用链)的时候, 证明对象是不可用的。如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

    在这里插入图片描述

    在Java, 可作为GC Roots的对象包括:

    • 在虚拟机栈中引用的对象
    • 在方法区中类静态属性引用的对象
    • 在方法区中常量引用的对象
    • 在本地方法栈中JNI(Native方法)引用的对象
    • Java虚拟机内部的引用
    • 所有被同步锁(synchronized关键字)持有的对象
  • 垃圾收集算法
    1.标记-清除算法

    分为标记和清除两个阶段先标记出需要回收的对象(可达性分析算法或者引用计数算法),在标记完成后统一回收所有被标记的对象。

    标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

    2.标记-复制算法

    将可用内存划分为大小相等的两块,每次只使用其中的一块。当这块用完了,就将还存活的复制到另一块上,然后将已使用过的另一半内存一次性清除。

    对整个半区进行回收,不会出现空间碎片。

    可用内存缩减了一半,造成空间浪费。

    适用于存活率较低、复制开销小的新生代

    3.标记-整理算法

    标记过程同标记清除一样,但不是直接对可回收对象进行清理,而是让存活对象朝着一端移动,然后直接清理掉边界以外的内存。老年代存活率高,适用标记整理算法。

  • 垃圾收集器
    • 新生代:Serial收集器  ParNew收集器  Parallel Scavenge收集器
    • 老年代:Serial Old收集器  Parallel Old收集器  CMS收集器
    1.Serial收集器

    标记复制。单线程收集器

    2.ParNew收集器

    标记复制。是Serial收集器的多线程并行版本

    3.Parallel Scavenge收集器

    标记复制。并行收集的多线程收集器。

    老年代:
    4.Serial Old收集器

    标记整理。Serial 收集器的老年代版本,单线程收集器。

    5.Parallel Old收集器

    标记整理。Parallel Old是Parallel Scavenge的老年代版本,多线程收集器。

    6.CMS收集器(Concurrent Mark Sweep并发标记清除)

    标记清除。

    并发收集,回收停顿时间短。

    步骤:

    1. 初始标记:停掉用户其他线程,仅标记GCRoots能直接关联到的对象,速度很快。
    2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,耗时长但不需要停顿用户线程,与垃圾收集线程一起并发执行。
    3. 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。耗时比初始标记长一点,但远低于并发标记。
    4. 并发清除:清理删除掉那些标记阶段判断为死亡的对象,因为标记清除算法不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

    总体而言,CMS收集器的内存回收过程是与用户线程一起并发执行的。

    在这里插入图片描述

    缺点:

    • CMS对处理器资源非常敏感。
    • CMS无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。
    • CMS是标记清除,会产生大量碎片空间,对大对象内存分配带来麻烦。
    7.G1收集器(Garbage First)

    与其他收集器不同,G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新胜达的Eden空间、Survivor空间,或者老年代空间。收集器对不同角色的Region采用不同的策略去处理。优先处理回收价值收益最大的那些Region,也就是Garbage First的由来。

    • 从整体来看:“标记-整理” 算法
    • 从局部(两个Region之间)来看:“复制”算法
  • 分代垃圾回收机制

    JVM内存分为新生代、老年代、元空间。

    新生代包括Eden,from survivor,to survivor,比例为8:1:1。对象主要分配在新生代的Eden区,需要连续内存空间的大对象直接进入老年区。每次采用标记复制的minor gc都会将Eden和from survivor的存活对象放到to survivor,然后清空Eden和from,转换from和to区。每次minor gc后放到to的对象年龄年龄都会+1,加到15会将对象放入老年代。

    Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果成立,那么Minor GC确保是安全的,如果不成立,就要进行一次Full GC,回收整个堆内存。

  • 类加载过程

    编译器会将Java代码编译为class文件,而类加载器就是将class文件加载到JVM虚拟机的内存即方法区中,再由执行引擎将字节码翻译为底层指令,交由CPU执行。

    主要为 5 个步骤:

    • 加载:根据查找路径找到相应的 class 文件然后导入;
    • 验证:检查加载的 class 文件的正确性;
    • 准备:给类中的静态变量分配内存空间;
    • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用为一个标示,直接引用直接指向内存中的地址;
    • 初始化:对静态变量和静态代码块执行初始化工作。

    在new一个对象、调用类的静态方法、或使用反射机制时会触发类加载。

  • 双亲委派模型
    类加载器分类:
    • Bootstrap ClassLoader:负责加载JDK自带的rt.jar包中的类文件
    • Extension ClassLoader:负责加载java的扩展类库从jre/lib/ect目录或者java.ext.dirs系统属性指定的目录下加载类
    • System ClassLoader:负责从classpath环境变量中加载类文件
    双亲委派模式:

    在加载类文件的时候,子类加载器首先将加载请求委托给它的父加载器,每层向上传递直到Bootstrap ClassLoader。父加载器会检测自己是否已经加载过类,如果已经加载则加载过程结束,没有加载,则Bootstrap ClassLoader开始尝试让其子加载器加载该类文件,如果失败则由子类加载器继续尝试加载,直到加载成功。

    优点:

    采用双亲委派模式可以保证类型加载的安全性,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,只有父类无法加载时自己才尝试加载,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。

  • 引用类型
    • 强引用:发生 gc 的时候不会被回收。
    • 软引用:系统将要发生内存溢出之前会回收这些对象,回收后内存仍然不足再报内存溢出异常。
    • 弱引用:在下一次GC时会被回收。
    • 虚引用:无法通过虚引用获得对象,主要起标识作用。
  • 常用JVM参数

    -Xmx(最大堆的空间)

    -Xms(最小堆的空间)

    -Xss(设置栈空间的大小)

    -XX:+PrintGC(开启打印 gc 信息)

    -XX:+UseConcMarkSweepGC(指定使用 CMS + Serial Old 垃圾回收器组合)

  • 打印某个线程的堆栈信息及内存、CPU过高分析

    可以在服务器使用top查看所有进程状态或控制台输入jps -m 获取进程id,通过top -p <pid>查看具体进程的cpu使用情况。大写H获取进程下每个线程的CPU情况。找到内存和CPU占用较高的线程id。执行jstack <pid> | grep <16进制的线程id>(字母小写)得到线程的堆栈信息。定位具体出错的类。

    在这里插入图片描述

    主要列:

    PID:当前运行进程的ID

    S:进程的状态。S表示休眠,R表示正在运行,Z表示僵死状态

    %CPU:进程占用CPU的使用率

    %MEM:进程使用的物理内存和总内存的百分比

    查看线程状态:
    • idea的Terminal终端中:

      ​ jps -m 查看进程pid

      ​ jstack pid 查看进程的所有线程信息

    • 或者使用jdk1.8.0_221\bin\目录下的jvisualvm.exe工具,查看各线程状态,哪行代码线程阻塞一目了然。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值