一问一答之JVM
说下JVM的内部组成与作用
JVM包括两个子系统和两个组件,如上图所示
两个子系统为类加载器
和执行引擎
两个组件为运行时数据区域
和本地接口
记这个(按顺序)
- 执行引擎(java -> class)
- 类加载器(加载class)
- 运行时数据区域(加载到内存中指这里)
- 本地库接口(运行需要本地接口支持)
- 本地方法库(接口需要依赖本地方法库)
联系:编译器会将java代码转为class字节码,类加载器将字节码加载到内存中,放在方法区内,字节码是JVM的一套指令集规范,并不能作用于操作系统,需要由JVM的解释执行引擎翻译为CPU的底层指令,整个过程需要调用本地方法库来实现这个功能
简单来说就是堆栈方法区
和程序计数器
程序计数器
作用
- 读取指令,实现流程控制(选择、循环、顺序、异常处理)
- 多线程情况下,记录线程当前位置
- 唯一不会出现OOM的地方,随线程生命周期决定(不涉及对象内存分配,也不承载对象引用)
虚拟机栈
会出现两个错误。StackOverFlowError
和 OutOfMemoryError
- StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError错误。
- 每次调用方法都会创建栈帧(包括局部变量表、操作数栈、动态链接、返回地址)
- 用来保存局部变量、参加方法计算与返回
本地方法栈
- 使用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步
- 初始标记:标记 GC ROOT 能关联到的对象,需要 STW
- 并发标记:从 GCRoots 的直接关联对象开始遍历整个对象图的过程,不需要 STW
- 重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生改变的标记,需要 STW
- 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要 STW
并发时间最长,但是不需要停止用户线程,其余两个相反,总体而言造成的停顿还是比较少的,大部分时候可以和用户线程一起工作
谈谈G1回收器
JDK9之后的默认回收器,不在区分年轻代和老年代进行垃圾回收
它把内存划分为了多个区域,可以通过-XX:G1HeapRegionSize 设置,大小为1~32M
对于大对象存储,超过区域大小的一半被认为是大对象,超过整个区域大小被认为超级大对象,会连续存储在N个Humongous Region中
G1 在进行回收的时候会在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先回收收益最大的 Region
整个GC过程可以分为4步
- 初始标记:标记 GC ROOT 能关联到的对象,需要 STW
- 并发标记:从 GCRoots 的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发标记过程中产生变动的对象
- 重新标记:短暂暂停用户线程,再处理一次,需要 STW
- 筛选回收:更新 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是怎么调优的
数据库 - 代码 - jvm - 操作系统
JVM的调优工具有哪些
位于JDk的bin下jconsole
jconsole:JDK 自带的可视化管理工具,用于对 JVM 中的内存、线程和类等进行监控,对垃圾回收算法有很详细的跟踪,功能简单
VisualVM:如下图所示
在短短的运行时间中,Ede进行了49次GC,虽然时间短,但是能说明一个问题,新生代堆内存分配的空间太小,
导致频繁GC
同时,OId老年代也进行了33次GC,虽然运行时间也在不需要优化的范围内,而且从Survivor可以看出,基本没有GC,说明这些 都是大对象,直接进入到了Old老年代,导致GC频繁
所以,我们需要进行的优化就是加大新生代和老年代堆内存的大小
,同时减少大对象的产生
可将VM参数改为:-Xms512m-Xmx512m-Xmn128m-X:+HeapDumpOnOutOfMemoryError
参考链接 — https://mikechen.cc/15386.html
谈谈常用命令及作用
- 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的时候返回一个系统通知
扩展
更多内容可点击参照这篇文章