JVM学习笔记

  本文是博主学习《深入理解JVM虚拟机》的学习笔记,部分图用的是网图,侵删。

一、Java内存区域

1.1 JVM的主要组成部分以及作用

hi

  JVM包含两个子系统和两个子组件,两个子系统为类加载(Class Loader)和执行引擎(Execution engine);两个组件为运行时数据区(Runtime data area)、Native Interface(本地接口)

  • 程序计数器(线程私有)
      程序计数器是一块较小的内存空间,是当前线程所执行到的字节码的行号指示器,该内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OOM情况的区域

  • 虚拟机栈(线程私有)
    在这里插入图片描述

  Java虚拟机栈是描述java方法执行时的内存模型,每个方法被调用的同时都会创建一个栈帧,栈帧中存储:局部变量表、操作数栈、动态连接、方法返回地址等信息。每一个方法从调用到执行完成的过程,就对应着虚拟机栈中入栈和出栈的过程。可通过-Xss设置每个线程的栈大小。

​ Java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError

  • StackOverFlowError:若Java虚拟机的大小不允许动态扩展(例如HotSpot虚拟机),那么当线程请求的栈深度超过当前Java虚拟机所允许的最大深度时,就会抛出StackOverFlowError异常。

  • OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,当栈扩展时无法申请到足够内存会抛出OutOfMemoryError异常。

  • 本地方法栈(线程私有)
      和虚拟机栈发挥的作用相似,为虚拟机使用到的本地方法服务。在HotSpot虚拟机中,本地方法栈和虚拟机栈合二为一。本地方法栈也会在栈深度溢出或栈扩展失败时抛出StackOverFlowError和OutOfMemoryError异常。

  • 堆(线程共享)
      存放对象实例,是垃圾回收器管理的重点区域。可通过-Xms指定堆初始内存空间大小和-Xmx指定堆内存空间大小。

  • 方法区(线程共享)
      存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后产生的代码缓存等数据。

    • 永久代是啥,为啥有这东西?

      永久代是HotSpot中才出现的东西,最初HotSpot设计团队为了省去对方法区编写内存管理代码的工作,将收集器的分代设计扩展至方法区。可以这么说,永久代是对方法区的一个实现方式。

    • 方法区在JDK1.6、1.7、1.8上做了哪些改动?

      JDK1.6:计划放弃永久代

      JDK1.7:初步实验放弃永久代,将在永久代中的字符串常量池、静态变量等移到Java堆中。

      JDK1.8:完全放弃永久代,该用在本地内存中实现的元空间替代,把JDK7中剩余内容(类型信息等)移至元空间。

    • 为什么要将永久代替换为元空间?

      永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,这种设计导致了Java应用更容易遇到内存溢出的问题。

      而元空间使用直接内存,受本机可用内存的限制,只要不触碰到内存上限,就不会出现OOM异常

二、HotSpot虚拟机对象探秘

2.1 对象的创建过程

在这里插入图片描述
① 类加载检查:

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

② 分配内存:

  在类加载完成后,接着会在Java堆中划分一块内存分配给对象,虚拟机会根据Java堆的规整状态进行分配。

  • 内存分配的两种方式:

    • 指针碰撞:如果Java堆的内存规整,即用过的内存放在一边,而空闲的内存放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象内存大小相等的距离。
    • 空闲列表:如果Java堆的内存不是规整的,则需要虚拟机维护一个列表来记录哪些内存是可用的,这样在分配时可从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

  选择以上两种方式的哪一种,取决于Java堆内存是否规整,取决于GC收集器的算法是"标记-清除",还是"标记-压缩"。需要注意的是,"复制算法"内存也是规整的。

  • 内存分配并发问题(Java虚拟机采用什么方式保证内存分配线程安全?):

  对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能在给对象A分配内存,指针还没来得及修改,对象又同时使用了原来的指针来分配内存的情况。

  • CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,假设每次操作都没有冲突,接着而去完成某项操作,如果冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
  • TLAB(Thread Local Allocation Buffer):本地线程分配缓冲区,为一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB中分配。当对象大于TLAB中的剩余内存或TLAB的内存用尽时,再采用CAS+失败重试的方式进行内存分配。

③ 初始化零值:

  内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在不附初值时就能直接使用。

④ 设置对象头:

  初始化零值完成后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

⑤ 执行init方法:

  在上面工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,<init>方法还未执行,所有的字段都为零。所以一般来说,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化。

2.2 对象的访问定位

  建立对象就是为了使用对象,我们的Java程序通过栈上的reference数据来操作对象的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有句柄直接指针方式

  1. 句柄:如果使用句柄的话,那么Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含对象实例数据与类型数据各自的具体地址信息;
    在这里插入图片描述

  优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时会改变句柄中的实例数据指针,而引用本身不需要修改。

  1. 直接指针:如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关类型,而reference中存储的直接就是对象的地址。
    在这里插入图片描述
      优势:访问时速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot采用的就是这种方式。

三、内存溢出异常

3.1 内存溢出和内存泄漏的区别?

  • 内存溢出:程序在申请内存时,没有足够的内存空间供其使用。
  • 内存泄漏:程序在申请内存后,无法释放已申请的内存空间。

3.2 Java会存在内存泄漏吗?

  理论上来说,Java是有GC垃圾回收机制的,也就是说不再使用的对象,会被GC自动回收掉。

  但即使这样,Java也还是存在内存泄漏的情况,比如长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。

四、垃圾收集器与垃圾收集算法

4.1 简述垃圾回收机制

  在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将他们添加到要回收的集合中,进行回收。

4.2 Java中的引用类型有哪些?

  • 强引用:永不回收,是最普遍的引用。
  • 软引用:适用于有用但不是必须的对象,可用来实现内存敏感的高速缓存,在发生内存溢出之前会进行回收。
  • 弱引用:适用于有用但不是必须的对象,在下一次GC时会进行回收。
  • 虚引用:无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的是为了能在这个对象被垃圾回收器回收时收到一个系统通知。

4.3 如何确定垃圾?

  • 引用计数算法

      在对象中添加一个引用计数器,每当有一个对象引用它时,计数器值就加一;当引用失效时,计数器值就减一。当计数器值为零,则该对象就不可能再被使用。但单纯的引用计数算法难以解决对象之间循环引用。

  • 可达性分析算法

    ​ 为了解决引用计数法的循环引用问题,Java使用了可达性分析算法。通过一系列的"GC Roots"对象作为起点搜索,如果在"GC Roots"与一个对象之间没有可达路径,则称该对象就是不可达的。

4.4 垃圾回收算法

  • 标记-清除算法

      算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法。

    • 优点:

      1. 不会移动对象
      2. 算法简单易实现
    • 缺点:

      1. 效率问题:执行效率随着要被回收的对象的数量增长而降低
      2. 空间问题:标记清除后会产生大量不连续的碎片,虚拟机需要维护一个内存空闲列表
        在这里插入图片描述
  • 复制算法

      “复制”收集算法将内存分为大小相等的两块,每次只使用其中的一块,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

    • 优点:
      1. 不会发生碎片化
      2. 可实现高速分配
    • 缺点:
      1. 堆使用效率低下
      2. 需要移动对象,重写指针

在这里插入图片描述

  • 标记-整理算法

      标记过程与"标记-清除算法"一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

    • 优点:可有效利用堆,不会产生内存碎片化问题
    • 缺点:需要移动对象,重写指针,计算成本高

在这里插入图片描述

  • 分代收集算法

      顾名思义是根据对象的存活周期将内存分为几块,其中包括年轻代老年代永久代,接着就可以根据各个年代的特点选择合适的垃圾收集算法。

      比如在新生代中,大多数对象都是朝生夕死的,所以可以选择"复制算法",只需要复制少量对象就可以完成垃圾收集。而老年代的对象存活几率是较高的,而且没有额外的空间供它进行分配,所以我们必须选择"标记-清除"或者"标记-整理算法"

4.5 JVM有哪些垃圾回收器?

在这里插入图片描述

  • Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效
    在这里插入图片描述

  • ParNew收集器(复制算法):新生代并行收集器,是Serial的多线程版本,在多核CPU环境下有着比Serial更好的表现。
    在这里插入图片描述

  • Parallel Scavenge收集器(复制算法):新生代并行收集器,追求高吞吐量,高效利用CPU。吞吐量 = 用户线程时间 / (用户线程时间 + GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用对交互相应要求不高的场景。
    在这里插入图片描述

  • Serial Old收集器(标记-整理算法):老年代单线程收集器,Serial收集器的老年代版本。

  • Parallel Old收集器(标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavange收集器的老年代版本。

  • CMS(Concurrent-Mark-Sweep)收集器(标记-清除算法):低延迟垃圾收集器,老年代并行收集器,关注点是尽可能缩短垃圾收集时用户线程的停顿时间。
    在这里插入图片描述

    • 工作原理:
      1. 初始标记:暂停所有用户线程,并标记出所有和GC Roots直接相连的对象,速度很快。
      2. 并发标记:同时开启GC和用户线程,从GC Roots直接关联的对象开始遍历整个对象图,这个过程耗时较长但不需要停顿用户线程。
      3. 重新标记:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
      4. 并发清除:开启用户线程,清理删除掉标记阶段判断已经死亡的对象,释放内存空间。
    • 优点:
      1. 并发收集
      2. 最耗费时间的并发标记和并发清理阶段都不需要暂停工作,所以是低停顿
    • 缺点:
      1. 对CPU资源敏感,在并发标记阶段GC线程需要占用一定的CPU资源,从而影响系统的吞吐量。
      2. 收集后会产生内存碎片
      3. 并发清理时会产生浮动垃圾,只要在下一次GC时才能清理
  • G1(Garbage-First)收集器:

      G1是一款面向服务器的垃圾收集器,主要针对多核处理器、大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量的特征。

    它具备以下特点:

    • 并行与并发:

      • 并行性:G1回收期间,可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程STW。
      • 并发性:部分工作可以与用户程序同时执行。
    • 分带收集:

      ​ 将整个堆空间分成若干个区域。这些区域中包含了逻辑上的年轻代和老年代。

    • 空间整合:

      ​ 各个region之间是复制算法,从整体上看是基于“标记-压缩”算法实现的。两种方法都能解决内存碎片化问题。

    • 可预测的停顿:

      ​ 能让使用者指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

五、内存分配策略

5.1 新生代、老年代内存大小的占比是多少?

  • 新生代和老年代内存的大小比例默认是1:2
  • 新生代中eden区和survivor区是8:2

六、虚拟机类加载机制

6.1 简述JVM的类加载机制?

  虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

6.2 类加载的过程有哪些?

  类加载的过程:加载 -> 链接 > 初始化。链接过程又可分为三步:验证 -> 准备 -> 解析
在这里插入图片描述

  • 加载:根据查找路径找到相应的class文件,通过全限定类名获取二进制字节流,然后在内存生成一个该类的Class对象
  • 验证:检查加载Class文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,可在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码执行初始化工作。

6.3 知道哪些类加载器?

​ JVM中内置了三个重要的ClassLoader,除了BootstrapClassLoader其他类加载器均有Java实现且全部继承自java.lang.ClassLoader

  1. BootstrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类或者呗-Xbootclasspath参数指定的路径中的所有类。
  2. ExtensionClassLoader(扩展类加载器):主要负责加载%JAVA_HOME%/lib/ext目录下的jar包和类,或被java.ext.dirs系统变量所指定的路径下的jar包。
  3. AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

6.4 什么是双亲委派机制?

​  每个类都有自己的类加载器,如果一个类收到了类加载请求,系统会判断当前类是否被加载过,如果已被加载过,则直接返回,否则才会去尝试加载。AppClassLoader在加载的时候,会请求委派给ExtClassLoader处理,而ExtClassLoader又会委派给启动类加载器BootstrapClassLoader中。当上级类加载器无法处理时,会委托让下一级类加载器去完成类加载,可以处理时,则直接返回。
在这里插入图片描述

6.5 双亲委派机制的好处?

  双亲委派机制保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分类的方式不仅仅是根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。如果不使用双亲委派模型,而是根据每个类加载器自己加载的话就会出现一些问题,比如自己编写一个Java.lang.Object类的话,程序运行时,系统就会出现多个不同的Object类。

6.6 如何破坏双亲委派机制?

  类加载器的父子关系不是通过继承来实现的,而是通过类中的parent成员变量指向父类对象,所以自定义一个类加载器时,只需要继承ClassLoader类,重写loadClass()findClass()方法即可,而要打破类加载机制,只需重写loadClass()方法。

七、JVM调优

7.1 JVM调优的工具有哪些?

  JDK自带了很多监控工具,都位于JDK的bin目录下,最常用的是jconsolejvisualvm这两款视图监控工具。

  • jconsole:用于对JVM的内存、线程和类进行监控。
  • jvisualvm:JDK自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc变化等。

7.2 常用的JVM调优的参数有哪些?

  • -Xms2g:初始化内存堆大小为2g
  • -Xmx2g:堆最大内存为2g
  • -XX:NewRatio=2:设置年轻代和老年代的内存比例为1:2
  • -XX:SurvivorRatio=8:设置新生代Eden和Survivor比例为8:2
  • -XX:UseParNewGC:指定使用ParNew + Serial Old垃圾回收组合
  • -XX:+PrintGC:开启打印gc信息
  • -XX:+PrintGCDetails:开启打印gc详细信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值