JVM JRE JDK的关系
-
JVM(java虚拟机),将 .class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。JVM不仅可以运行java程序,只要是能编译成.class的文件都能运行。
-
JRE (Java 运行时环境),包含了jvm和core lib。
-
JDK (Java 开发工具包),它集成了jre和一些工具。比如javac.exe,java.exe,jar.exe等。大家都知道,要想执行java程序,需要安装jdk。
JVM 初识
JVM其实是一种规范,它提供可以执行Java字节码的运行时环境。不同的供应商提供这种规范的不同实现。 常见的JVM实现有
- Hotspot oracle官方提供
- TaobaoVM 阿里对hotspot深底定制版
- J9 ibm实现
- Jrockit 号称是世界上最快的JVM
- openJDK
- azul zing
- LiquidVm 直接针对硬件
- Microsoft JVM
- 等
JVM的内存模型
虚拟机在执行文件的时候将内存分为不同的区域,它们各司其职。
- 程序计数器
- java虚拟机栈栈
- 堆
- 方法区
- 本地方法栈
-- 来源《深入理解java虚拟机》--
程序计数器 / 行号指示器
可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,任何一个确定的时刻,一个处理器都只执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。所以这类内存区域为“线程私有”的内存。---《深入理解java虚拟机》
它是一块较小的空间,也是唯一一个在java虚拟机规范中没有定义任何OOM的区域。正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是Natice方法,则为空。
java虚拟机栈
也为线程私有,生命周期与线程相同,它描述的是Java方法执行的内存模型。每个方法在执行的时候都会创建一个栈,每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。它存储局部变量表、操作数栈,方法出口等信息。局部变量表的大小在编辑期间完成,所以进入执行方法时,栈的大小是确定的。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。
更多信息可参考 Java虚拟机运行时栈帧结构
** 本地方法栈** 和java虚拟机栈类似,只不过它表示的是Native方法。
堆
这是java虚拟机中最大的一块内存,是被所有线程共享的一块内存区域,在虚拟机启动的时候被创建。几乎所有的对象实例的内存都在这里,这也是它存在目的。Java堆还可以细分为新生代和老年代。新生代有可以分为eden伊甸区、from servivor,to servivor。
根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。
当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误。
** 方法区**
这也是一块共享区。存储了已被虚拟机加载的类信息、常量、静态变量、即使编辑器编辑后的代码等数据。在老版jdk,方法区也被称为永久代「HotSpot虚拟机以永久代来实现方法区」。jdk8真正开始废弃永久代,而使用元空间(Metaspace)。
** 当然上面的区分是JVM规范,每个虚拟机实现可能有不同的划分。有时候,我们可以粗略的把区域分为堆区和栈区。这也是程序员最关心的2个部分。**
java内存模型,JMM(java memory model)
- JMM作用
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
详情参考 Java内存模型(JMM)总结
** JVM GC **
** 如何寻找到垃圾**
主要有引用计数和根可达分析法。
- 引用计数
给对象中添加引用计数器,每次被引用都+1,当引用失效就-1,当gc的时候,引用为0,就会被当作垃圾回收掉。
优点: 实现简单,判断效率高。
缺点:很难解决对象之间循环引用的问题。也就是因为这个缺点,主流的jvm不选用这种计数法。
- 根可达 root searching
就是从“GC Roots”对象作为起点开始往下搜索,搜索所有的“叶子”。那些没有根的叶子就是垃圾。 GC Roots对象包括一下几种:
- 虚拟机栈中引用的对象
- 方法区中静态属性和常量引用的对象
- 本地方法栈中引用的对象
找到垃圾如何清理?也就是垃圾回收的算法
- 标记-清除(Mark-Sweep)
就如同它的名字,分为2个阶段,先标记后清除。这也是最基础的算法,其他算法都在它的基础上优化而来。
优点:
1.存活对象比较多的时候效率高(老生代)。 2.算法简单。
缺点:
- 2遍扫描,效率偏低。 第1遍标记有用的,第2遍清除没用的。
- 容易产生碎片。
- 复制(Copying) 为了提高效率,复制算法出现了。它将内存分为2块,每次只使用其中的一块,这块用完了,就把其中存活的对象复制到另一块。然后把这块内存的对象全部清掉。
优点:
适用于存活对象比较少的情况(新生代),只扫描一次,效率提高,没有碎片。
缺点:
1.空间浪费; 2.移动复制对象,需要调整对象引用。
- 标记-整理算法(Mark-Compact)
如果存活对象比较多,移动对象效率变低。为了不浪费另一块内存。程序在运行过程中,很难有100%对象存活的极端情况。 引出了标记整理算法。就是在标记-清除算法的基础上多了一步整理。先把垃圾对象标记出来,然后把所有存活的对象移到内存中一块连续的内存,然后把这块内存意外以外的部分清除掉。
优点:
不会产生碎片,方便对象分配,不会产生内存减半。
缺点:
扫描2次,需要移动对象。
有哪些垃圾回收器
上面介绍了垃圾回收的算法理论,垃圾回收器就是它们的具体实现。java虚拟机规范中对垃圾回收器如何实现并没有任何规定。所以不同厂商,不同版本的JVM提供的垃圾回收器也不相同。它们会提供参数供用户自定义自己的垃圾回收器组合。下面是HotSpot虚拟机的垃圾回收器。来源于「深入理解java虚拟机」
3-5图中,如果俩个虚拟机之间有连线,说明它们可以搭配使用。没有最好的垃圾回收器,只有最适合自己的垃圾回收器。 JDK1.8默认垃圾回收Parallel Scavenge + ParallelOld
-
Serial
单线程执行,只会使用一个CPU或一条收集线程区完成垃圾收集工作。它执行垃圾回收的时候,必须暂停其他工作线程,直到收集结束。其使用的是复制算法。内存要求几十兆。
应用场景: 不需要线程交互的开销,可以获得最高效的收集效率,适用于限定单个CPU的环境。
显式的使用设置参数: "-XX:+UseSerialGC"
-
ParNew 是Serial的多线程版,除了使用的是多线程外,其他和Serial基本一样。这2种垃圾回收器也公用了大量代码。
应用场景:在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
设置参数
-XX:+UseParNewGC":强制指定使用ParNew;
-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;
-
Parallel Scavenge 也是一个新生代收集器,用的复制算法。其他回收器关注的是尽可能的缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控的吞吐量。
应用场景 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间; 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互; 例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序,数据挖掘。
- Serial Old Serial的老年代实现,同样是单线程,使用标记-整理算法。
- Parallel Old Parallel的老年代算法,使用标记-整理算法。
- CMS 是一种获取最短停顿时间为目标的的回收器。一些应用尤其重视服务的相应速度,希望停顿时间最短,给用户带来较好的体验。基于标记-清除算法。
回收4个阶段:
- 初始标记 initial mark。找gc roots,这个阶段是STW,但是STW时间很短。
- 并发标记 concurrent mark (虚拟机中concurrent代表了工作线程和垃圾回收线程一起工作),最耗时间,不产生STW。标记垃圾。
- 重新标记 remark,上一次标记的垃圾可能被修改变成可用,需要重新标记。改变的情况比较少,这个过程也是STW。
- 并发清除 concurrent sweep,标记完了,并发清除。
起到了承上启下的作用。
缺点:
- 使用标记-清除算法,会产生内存碎片
- 对cpu要求比较高
- 无法处理浮动垃圾。在垃圾回收的时候,工作线程也在执行,新产生的垃圾无法清理。
- G1
之前的内存划分都是分大块的。G1采用分而治之的思想,和之前的分代思想完全不同。分成一小块一小块的regions(里面存活对象最少)。G1的内存区域不是固定的E区或O区。
特点
- 并发标记,并回收
- 压缩空闲空间,不延长GC的暂停时间,能控制GC时间
- 更容易预测的GC暂停时间
- 高响应时间,不需要实现很高的吞吐量的场景 新老年代比例
5%-60% 一般不手动指定,也不要手动指定,这是G1预测停顿时间的基准。
GC什么时候触发
- YGC Eden空间不足。多线程并行执行
- FGC Old空间不足。 System.gc();
*如果G1产生Fgc,你应该这么做
内存分配不下就会出现FGC。
- 扩内存
- 提高cpu性能
- 降低MixedGC触发的阀值,让MixedGC提早发生(默认45%)
**三色标记 **
- 白色:未被标记的对象
- 灰色:自身被标记,成员变量未被标记
- 黑色:自身和成员变量均已被标记
调优
先了解2个概念
- 吞吐量:(用户代码时间/用户代码时间+垃圾回收时间)。虚拟机大部分时间是用在用户时间上,而不是垃圾回收上。
- 相应时间:SWT越短,响应时间越好。
调优第一步就是要确定吞吐量优先,还是响应时间优先。或者是找到俩者的平衡点。它们俩个是互相矛盾的,熊掌与鱼不能兼得。
调优的目的
- 根据需求进行jvm规划和预调优
- 优化jvm运行环境(慢、卡顿)
- 解决jvm运行过程中出现的各种问题(OOM),面试主要问这个。
调优,从规划开始
- 从业务场景开始,没有业务场景的调优就是耍流氓
- 无监控(压力测试、能看到结果),不调优。
优化步骤
- 选择回收组合
- 计算内存需求
- 选定CPU(越高越好)
- 设定年代大小、升级年龄
- 设定日志参数
调优常用命令
面试
- JVM 接口慢如何排查原因
线上接口过慢,排除网络的原因之外无非有以下三点: 1.内存使用过高,频繁gc导致cpu占满 2.内存使用不高,出现了类似死循环场景 3.死锁 一般在遇到问题的时候先使用top -c 命令查看cpu是否占满,然后再使用free -m查看内存使用率,初步判断是上面问题的哪一种,然后再针对这一种问题深入排查。当线上接口请求过慢如何排查?
- 内存结构,那些是线程私有的,那些是共有的
方法区、堆是共有的; 程序计数器、虚拟机栈、本地方法栈是私有的。
-
内存模型、CPU有这套架构,为什么JVM实现一遍 为了跨平台
-
Java 对象结构
-
有没有JVM调优的经验、如何JVM 调优,Dump 日志如何分析
-
为什么要避免 FullGC FullGC 会扫描整个old区,效率低。所以JVM设计了CardTable。如果O区CardTable指向Y区。就将它设为Dirty。 下次扫描时只需要扫描Dirty Card table。在结构上card 用bitmap实现。Rset记录了其他Region中的对象到本Region的引用。Rset的价值在于使得垃圾回收器不需要扫描整个堆找到谁引用了当前分区中的对象,只扫描Rset即可。
-
新生代垃圾收集算法,会不会 STW(stop the world)
会的
- 老年代GC和FullGC的关系
老年代的gc 就是fullgc
- 线上cpu报警,原因及排查方式(系统CPU经常100%,如何调优)
- 找出哪个进程CPU高(top)
- 该进程哪个线程CPU高(top -Hp),通过进程id查询哪个线程cpu高。
- 导出该线程的堆栈(jstack)
- 查找哪个方法消耗时间(jstack)
- 工作线程占比高还是垃圾回收线程占比高
-
什么会触发fullgc,达到多大堆内存会触发?
-
G1垃圾回收器介绍,都用了什么回收算法?配置回收时间好处?和cms怎么选择?为什么? G1响应时间要比CMS要短,但是CMS+PN要比G1吞吐量大。
-
OOM和stack over flow 的区别
-
生产环境中能随时dump吗 小堆影响不大,大堆会有服务暂停或卡顿,dump前会有FGC
-
常见的OOM问题有哪些
堆 栈 methodArea 直接内存
- java 对象是如何被创建的
- 检查对象在虚拟机中是否存在,如果不存在执行类加载
- 分配内存
- 成员变量赋值
- 执行构造方法 < init >
- class类加载过程
- loading 把class文件加载到内存中
- verification 检验文件是否符合jvm规定
- preparation 静态变量赋值,是将class文件静态变量赋默认值而不是初始值,例如static int i =10;这个步骤并不是将i赋值为10,而是赋值为默认值0;
- resolution 是把class文件常量池中用到的符号引用转换成直接内存地址,可以访问到的内容;
- initializing 成为初始化,静态变量在这个时候才会被赋值为初始值;
- gc
- 对象在内存中的布局
对象
- mark word 对象头 ,主要有对象的hashcode,锁,对象GC分代年龄,偏向锁。
- Klass Pointer 类指针
- 数据
- 对齐 一个对象占用的字节数必须是8的倍数,不足的用padding对齐
数组比对象多了数组长度
- 对象的访问定位
目前是2种方式
1.通过句柄访问对象,栈中的refernce存储了对象的引用,指向了句柄池中的列表,列表存了java对象实际的地址。好处就是在对象移动的时候,只需要修改句柄池的地址就可以,不需要修改refernce。 2. 通过直接指针访问对象 ,refernce存储的就是对象内存地址。它的好处就是访问快。
-- 《深入理解java虚拟机》
- 对象什么时候会到old区
-
大对象
-
大年龄,默认15岁,可以通过参数-XX:MaxTenuringThreshold设置。
-
动态分配,比如g1 s1->s2 超过50% 把年龄最大的放入Old
参考
「深入理解Java虚拟机」
「马士兵课堂」