说下JVM的内部组成与作用

JVM

jvm包括两个子系统和两个组件,可以看图
两个子系统为类加载器执行引擎
两个组件为运行时数据区域本地接口

联系:编译器会讲java代码转为class字节码,类加载器将字节码加载到内存中,放在方法区内,字节码是JVM的一套指令集规范,并不能作用于操作系统,需要由jvm的解释执行引擎翻译为cpu的底层指令,整个过程需要调用本地方法库来实现这个功能

jvm运行时区域

简单来说就是堆栈方法区和程序计数器

程序计数器

作用

  1. 读取指令,实现流程控制(选择、循环、顺序、异常处理)
  2. 多线程情况下,记录线程当前位置
  3. 唯一不会出现OOM的地方,随线程生命周期决定

虚拟机栈

会出现两个错误。StackOverFlowErrorOutOfMemoryError

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
  • 每次调用方法都会创建栈帧(包括局部变量表、操作数栈、动态链接、返回地址)
  • 用来保存局部变量、参加方法计算与返回

本地方法栈

  • 使用Native方法服务,也会出现上述两种错误
  • Native方法使用C语言编写

  • 内存中最大的一块。共享区域,存放对象和数组,都会在这里分配内存
    不是所有对象,有个逃逸的概念。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
  • 是垃圾回收的主要区域
  • 1.8以前,堆内存通常分为:新生代、老生代、永久代
  • 1.8之后移除永久代,加入元空间,使用的直接内存(物理内存)
  • 新生代又分为eden区和survivor区,survivor区分为from survivor 和 to survivor又叫s0,s1

方法区

  • 放常量、静态变量,类加载信息,又被称为永久代。于1.8之后移除,用元空间代替
  • 类信息包含版本、字段、方法、接口、父类等信息

元空间

  • 1.8后出现,使用本地内存

运行时常量池

方法区的一部分,存放类信息和常量池表,当无法申请到内存时会OOM。字符串常量池在堆

直接内存

也会出现OOM的问题,受到本机总内存大小和处理器寻址空间的限制

聊聊垃圾回收

  • 使用Java只创建对象而不销毁对象,C的话需要,这块JVM帮我们做了。
  • 什么是垃圾?对象不再使用了,就应该被回收
  • 如何判断是否还在使用了?引用计数法可达性算法。引用简单,有对象引用为+1,无对象为-1.但无法处理对象相互依赖的情况。可达性算法类似于树的结构,一直往下搜,如果没有引用(直接或间接),那么对象就是不可用的,就可以被回收了
  • 不懂,具体在聊聊?如果栈帧位于栈顶说明栈帧是活跃的,也就是线程被调用,栈帧指向堆的引用也是活跃的
  • 然后就是标记,不关联的都会被清除,但就出现了内存碎片的问题。看着明明内存足够,却申请不下来
  • 那如何解决?标记完之后,不直接清除,将有效的先复制到一份空间,再把另外一部门全部干掉,这有个明显缺点,前提得有空间去进行复制。
  • 那如何解决?可以在当前区域内整理,存活的放一边,不存活的另一边,这也叫做整理
  • 在堆中的大部分对象存活时间都很短,在gc的时候程序会短暂不能运作。
  • 死的快的对象存于年轻代中,死的慢的在老年代中
  • 年轻代的gc都是标记复制算法
  • 新对象在eden区。那什么情况下会进入老年代中了?
  • 对象过大(比如数组) 或者对象太老没发生一次monor gc年龄+1,15岁到老年代。当eden不足的时候就会触发monor gc

介绍下CMS回收器吧

牺牲吞吐量来获得最短停顿时间的回收器,多应用于服务器的响应速度上

在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器

使用标记---清除算法来实现,在GC的时候会产生大量的碎片,当剩余内存不能满足要求的时候,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。

整个GC过程可以分为4步

  1. 初始标记:标记 GC ROOT 能关联到的对象,需要 STW
  2. 并发标记:从 GCRoots 的直接关联对象开始遍历整个对象图的过程,不需要 STW
  3. 重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生改变的标记,需要 STW
  4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要 STW

并发时间最长,但是不需要停止用户线程,其余两个相反,总体而言造成的停顿还是比较少的,大部分时候可以和用户线程一起工作

谈谈G1回收器

JDK9之后的默认回收器,不在区分年轻代和老年代进行垃圾回收

它把内存划分为了多个区域,可以通过-XX:G1HeapRegionSize 设置,大小为1~32M

对于大对象存储,超过区域大小的一半被认为是大对象,超过整个区域大小被认为超级大对象,会连续存储在N个Humongous Region中

G1 在进行回收的时候会在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先回收收益最大的 Region

整个GC过程可以分为4步

  1. 初始标记:标记 GC ROOT 能关联到的对象,需要 STW
  2. 并发标记:从 GCRoots 的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发标记过程中产生变动的对象
  3. 重新标记:短暂暂停用户线程,再处理一次,需要 STW
  4. 筛选回收:更新 Region 的统计数据,对每个 Region 的回收价值和成本排序,根据用户设置的停顿时间制定回收计划。再把Region 中存活对象复制到空的 Region,同时清理旧的 Region。需要 STW

总的来说除了并发标记之外,其他几个过程也还是需要短暂的 STW。G1 的目标是在停顿和延迟可控的情况下尽可能提高吞吐量

分代收集器是如何工作的?

新生代占比三分之一,老年代三分之二

新生代中 eden : s0 : s1 = 8:1:1

简述minor GC和major GC

java内存分配通常在堆上分配,主要分配在新生代的eden区,如果启动了本地缓存,则优先在TLAB分配,少数情况下也会直接在老年代分配

对象在eden区分配,如果空间不足虚拟机将进行一次Minor GC,如果还是不够,则启用分配担保机制在老年代中分配内存。Minor GC的特点是比较频繁,回收速度比较快

Major GC和Full GC是发生在老年代的GC,出现Major GC一般也会伴随一次Full GC,Major GC通常比Minor GC慢10倍以上

大对象直接进入老年代中,如果在新生代中,会频繁的发生GC,长期存活的对象也存于老年代中,对象经过一次Minor GC年龄+1,默认到达15就会晋升为老年代

什么是双亲委派机制?它的工作流程是什么样的?模型的好处是什么?

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。

双亲委派

双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去加载,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载器无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载此类。

自下而上检查类是否已经被加载,自上而下尝试加载类

双亲委派模型工作流程:

  1. 当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。

  2. 当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。

  3. Bootstrap ClassLoader尝试加载此类,如果Bootstrap ClassLoader加载失败,就会让Extension ClassLoader尝试加载。

  4. Extension ClassLoader尝试加载此类,如果Extension ClassLoader也加载失败,就会让Application ClassLoader尝试加载。

  5. Application ClassLoader尝试加载此类,如果Application ClassLoader也加载失败,就会让自定义加载器尝试加载。

  6. 如果均加载失败,就会抛出ClassNotFoundException异常。

双亲委派模型的好处:保证核心类库不被覆盖。如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统将会出现多个不同的Object类, Java类型体系中最基础的行为就无法保证,应用程序也将会变得一片混乱。

说下JVM是怎么调优的

数据库 - 代码 - jvm - 操作系统

JVM的调优工具有哪些

位于JDk的bin下jconsole

jconsole:JDK 自带的可视化管理工具,用于对 JVM 中的内存、线程和类等进行监控,对垃圾回收算法有很详细的跟踪,功能简单

谈谈常用命令及作用

  • jps:显示系统中所有的虚拟机进程
  • jstat:收集虚拟机各方面的运行数据
  • jinfo:显示虚拟机配置信息
  • jmap:生成虚拟机的内存转储快照
  • jhat:分析堆内存转储快照,不推荐使用,消耗资源而且慢
  • jstack:显示线程堆栈快照

讲讲你的调优思路

调优主要看场景,通常关注三个方面,内存占用(footprint),延时(latency)和吞吐量(throughput)

假设开发了一个应用服务,偶尔出现性能抖动,比较长的卡顿,评估用户可接受的响应时间和业务量,将目标简化为,希望GC暂停尽量控制在200ms之内,并且保证一定的吞吐量

利用工具。比如,用jstat查看日志的状态,开启GC日志,查看暂停时间段而导致的应用响应不及时

  • 工具的使用。使用jps可查看Java进程基础信息。常用于看当前服务器有多少Java进程在运行,进程号及加载主类
  • jstat看GC情况
  • jinfo看运行参数
  • jmap看内存信息
  • jstack看线程信息,排查死锁相关问题

是Minor GC时间过长还是Mixed GC等出现异常停顿,如果不是,考虑切换到什么类型,如CMS和G1都是侧重于低延迟的GC选项

通过分析具体的参数和软硬件配置

验证是否达到调优目标,如果没有,重复进行调整和验证

系统运行日志(catlinna.out) —> 堆栈错误信息(java.lang.OutOfMemoryError: PermGen space) —> GC日志(设置命令,full gc 回收了多少空间) —> 线程快照(jstack) —> 堆转储快照(设置命令,jmap)

JVM调优的一些参数可以举例一些吗

-Xms2g:初始化堆大小为 2g;

-Xmx2g:堆最大内存为 2g;

-Xmn1g:新生代内存大小为1g;-XX:NewSize 新生代大小,-XX:MaxNewSize 新生代最大值,-Xmn 则是相当于同时配置 -XX:NewSize 和 -XX:MaxNewSize 为一样的值;

-XX:NewRatio=2:设置新生代的和老年代的内存比例为 1:2,即新生代占堆内存的1/3,老年代占堆内存的2/3;

-XX:SurvivorRatio=8:设置新生代 Eden 和 两个Survivor 比例为 8:1:1;

–XX:+UseParNewGC:对新生代使用并行垃圾回收器。

-XX:+UseParallelOldGC:对老年代并行垃圾回收器。

-XX:+UseConcMarkSweepGC:以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

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

-XX:+PrintGCDetails:打印 gc 详细信息。

java会内存泄漏吗?

java拥有GC机制,但还是会出现内存泄漏的情况,如果GC无法进行回收,那么就会导致泄漏

java对象引用类型有哪些?具体使用场景是什么?

  • 强引用:new出来的就是,就算OOM也不会进行内存回收
  • 软引用:发生内存溢出前会被回收。通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂存缓存,当内存不足时清除掉,这样就可以保证使用缓存的同时,不会耗尽内存
  • 弱引用:gc的时候就会被回收,同样可用于内存敏感的缓存
  • 虚引用:无法通过虚引用获得对象,作用在于在GC的时候返回一个系统通知