JVM学习总结

JVM总结

JVM总体结构,逐步详细总结每个结构,结合程序总结每个结构都存储了什么内容,何时存入何时取出,生命周期。程序运行过程与jvm动作如何对应。常遇到的问题,如何调优,查找问题。

历史

Jrocket,不使用解释器,只使用JIT,性能高启动慢
hotspot,与上边的结合,使用解释器与JIT。等公交车的例子。
J9,与硬件绑定 IBM开发。

JVM结构

栈式结构,0地址指令,指令集小,指令多,内存读写次数多,栈顶缓存技术,栈顶元素缓存到寄存器中。

一. 加载

准备阶段初始化0,0L,null,false,final变量直接赋字面值。初始化阶段以静态资源声明顺序生成clinit方法,初始化阶段先执行clinit,并保证父类先执行。bootstrap,ext,system三个类加载器,双亲委派,沙箱机制。自定义加载器的意义,1.中间件防止类冲突,隔离。2.修改加载方式,比如某些类按需加载。3.字节码存储在各种地方需要灵活存取。4.安全(字节码加解密)ClassLoader findClass。有某个包名类名相同冲突类已经被加载,这是类冲突的根源,需要排除。打破双亲委派的方法是自定义类加载器重写findclass,或者使用当前线程上下文类加载器ContextClassLoader,实际就是AppClassLoader,线程可以在任何位置使用这个加载器加载需要的类。打破双亲委派的意义是下层类加载器可以依赖上层加载器加载的类,但是反过来却不行。此时如果上层类依赖下层加载的类,比如SPI机制(Service Provider Interface)比如MySql Driver的加载,上层jdk,DriverManager依赖App加载器加载的第三方驱动类。此时使用Class.forName传入当前线程上下文加载器。SPI的意义在于可以方便的让第三方框架实现扩展。SpringBoot使用SPI思想,自定义类加载器SpringFactoriesLoader加载所有jar包中的META-INF/spring.factories。

二. 运行时数据区

1.方法区
线程共享,类虚方法表,防止动态分派invokevirtual从子类到父类逐个查找方法,类加载链接阶段创建。jdk7以前叫永久代是堆的一部分,jdk8叫元空间使用直接内存,初始大小21M,最大为物理内存大小。初始化高水平线21m容易触发fullgc,每次触发fullgc根据释放空间的多少都会动态调大小,初始值设置稍微大些防止频繁fullgc调整大小。
存储类加载器信息(谁加载了我)类加载器都加载过谁。类信息(完整类名,直接父类名,接口列表,修饰符),静态变量(堆中),运行时常量池,JIT代码缓存,类的域信息和顺序,方法信息。方法信息包括方法名,返回类型,参数数量类型,修饰符,字节码,操作数栈深度、局部变量表大小,异常表(处理的开始结束位置,程序计数器偏移地址,捕获的异常类常量池引用。
StringTable特性,在堆中,存引用,实际对象在堆中 。字符串声明直接存入,字符串相加只存结果,引用相加通过stringbuider在堆中生成新对象,internString1.6,1.8区别是池中没有的话,1.6是复制一个到table中返回table中的引用,一个是将对象的引用复制一个到stringtable中,返回这个引用。后边如果声明相同字面量,会直接指向stringtable中的这个引用。G1可以开启相同字符串堆对象的去重以节省内存。Stringtable会有垃圾回收.intern方法在存在大量重复字符串对象时可以节省内存。1.8的String是char数组,jdk9改为byte加字符编码,以节省内存。stringtable的结构是固定大小的hashtable,1.6大小1009,1.8最小1009,设置太小会导致hash冲突多,大量rehash。
运行时常量池在方法区中,启动时加载类文件常量池。
此处调优就是调整合适的初始方法区大小避免频繁fullgc,并且将常量池大小配置合适大小,减少rehash次数。
2.堆
线程共享,新生代(eden,s1,s2),老年代,1.6(永久代)。-Xms,堆(年轻+老年)初始大小,-X为虚拟机运行参数,ms,memory start,-Xmx,最大堆大小。单位默认字节,k,m,g。初始默认大小为64分之一物理内存(减操作系统等占用后可用空间),最大内存是4分之一。Xms,Xmx一般设置成相同,垃圾回收后防止频繁调整大小,新生老年比例1:2。s1,s2始终有一个是空的8:1:1,但其实是6:1:1,原因是自适应机制,可关闭,显式指定XX:servivorratio才是8:1:1。oom,java heap space。大对象直接进老年带,80%对象都在eden区死,Xmn设置新生带内存大小.minor,major,fullgc。
fullgc5种触发机制,system.gc,老年代不足,方法区不足,进入老年代平均大小大于堆剩余大小,s1到s2,s2不够大老年代地方又不够。
晋升老年代的情况:
熬过一次Minorgc年龄+1,年龄计数器阈值15,晋升老年代。s区相同年龄对象的大小总和超过s区一半,大于这个年龄的对象都进入老年代,大对象直接进入老年代XX:PretenureSizeThreshold参数设置。TLAB,Thread local allocation buffer,每个线程在eden区的小块私有区域,用完了再用eden公共空间,以增加吞吐量。逃逸分析(传入的返回的依赖外部创建的都逃逸),不逃逸栈上分配,JIT同步省略(锁消除),标量替换(聚合量替换为标量)。Hotspot其实时栈上标量替换。
调优方式是合理设置堆大小,初始最大设置成一样,手动设置servivorratio。
3.程序计数器
非共享,指令地址和指令,多线程切换时需要使用线程挂起时记录的指令地址继续执行。执行本地方法时,计数器中无值,因为本地方法可能不是java写的,内存管理由实现语言决定。
4.虚拟机栈
非共享,栈帧为基本单位,只有入栈出栈两个动作 FILO,LIFO。xss(stacksize),最大栈大小,可设置固定值或动态值,超出报stackoverflow。内存不够报oom。递归调用容易出现。
包含如下结构:
局部变量表
编译时确定大小,保存方法的局部变量和参数,基本类型(部分转int存储),引用类型,retrunaddress。全局变量是在堆中。最小单位是变量槽,长度32位,超出32位的变量64位使用连续两个槽,高位对齐。槽可复用,节省空间但影响垃圾回收。实例方法调用0位置存this,静态方法无this,垃圾回收根节点相关。代码块内的变量在局部变量表中,出了作用域,变量表占用的槽不会被回收,如果代码块外再声明一个变量,将会复用这个槽,之前代码块中的变量将会被回收
操作数栈
编译时确定深度,FILO,数组实现,运算时使用,从局部变量表取出,运算,再存入局部变量表。栈帧初始时为空。64位变量占深度2,方法的返回值会压入,之后更新程序计数器。栈顶缓存技术减少读写。i++ ++i的四类问题。
动态链接
加载时的解析阶段将符号引用转换直接引用叫静态解析
运行时动态转换成直接引用叫动态链接。保存了一个指向运行时常量池中的该方法的引用。大部分字节码运行都需要访问常量池.
方法返回地址
用于返回方法被调用的地方,正常返回时,计数器中的地址可作为返回地址,但是异常退出时,异常处理器表记录了调用地址,而不是栈帧的返回地址记录。

直接内存,读写更快,直接内存默认与Xmx大小相同。

----方法的调用
符号引用转直接引用,早期绑定,静态链接,非虚方法(静态,私有,final,构造器,子类重写父类并使用super调用父类的话父类方法是非虚)不能重写,编译期间确认运行期不变。晚期绑定,动态链接,虚方法,编译期确认不了的。
invokestatic,invokespecial非虚方法,private是special,
invokevirtual,调用虚方法,如果方法有final标注,那这个方法即使是virtual调用也是非虚方法。没加super.是虚方法
invokeinterface调用接口方法
invokedynamic,lambda表达式

5.本地方法栈
非共享,与虚拟机栈类似,用于调用本地方法时入栈出栈,会stackoverflow,oom,无垃圾回收。

对象在虚拟机中如何存储
创建对象的方式6种,new,class.newInstance只能是public无参,Construactor.newInstrans无权限要求可有参,clone,反序列化,第三方库。
对象创建步骤(规整(指针碰撞),不规整(空闲列表),处理并发安全问题,初始化这块内存,设置对象头,调用init
内存布局:
对象头,实例数据,对齐填充
对象头里有什么
运行时元数据(哈希值,垃圾分代年龄,锁状态,对象持有锁,偏向线程id,偏向时间戳),类型指针指向方法区类元信息,标记清除算法 在对象头。

多态
重载规则(静态分派)
字面量会先按照char->int->long->float->double这样的顺序去查找相应的方法,如果找不到,会按照自动装箱的类型(int对应Integer、char对应Character)进行查找,如果还没有相应的方法,会找自动装箱后的对象的接口作为参数的方法,如果还没有,会找相应的父类作为参数的方法,直到Object,如果还没有,则会选择变长参数的方法。

重写(动态分派)
字节码将对象压入操作数栈,invokevirtual指令取栈顶元素查找其实际类型信息,从类元信息中找到要执行的方法,如果有就校验权限,没权限报IllegalAccessError。如果类元信息没有,找父类,父类也没有报AbstractMethodError。动态分派优化方案为方法区的虚方法表和接口方法表。没重写,子类虚方法表索引指向父类方法表对应方法索引,重写了,子类虚方法表指向自己方法索引。只需在实际类型子类中找一遍就定位到要执行的方法。

三. 执行引擎

半编译半解释型语言。
JIT
栈上替换OSR,On stack replacement,将热点字节码编译为机器指令缓存以提高运行速度。热点探测,基于计数器。方法调用计数器(执行次数)Client模式1500次,Server模式1万次,热度衰减是垃圾收集顺便进行,半衰周期-XX:CounterHalfLifeTime单位秒。回边计数器(循环体),默认是混合模式执行。
即时编译器的优化策略:
C1方法内联,去虚拟化,冗余消除
C2标量替换,栈上分配,同步消除
JDK9 AOT 编译时直接生成机器码。

四. 垃圾收集

安全点,抢先中断先中断所有再看谁没到继续跑到安全点,主动中断设置中断标志。安全区域一段长时间的安全区域。
引用: 内存够就留着,内存不够就抛弃。Reference类的子类weekxxx,softxxx,finalxxx终结器引用,phantomxxx虚。
1.强
2.软 内存不够就回收,get方法获取引用
3.弱 下次回收之前 weekhashmap
4.虚 必须给一个队列,回收后会被自动放入队列以通知应用对象被回收
标记算法
判断对象是否存活
引用计数,简单高效无延迟,引用计数器占用空间,加减开销,无法处理循环引用,在JAVA中没有使用。循环引用比如两个引用分别引用两个对象,对象自己的属性,互相又引用对方,此时两个引用指向Null也不会被回收。如何解决循环引用,弱引用,手动解除
可达性分析,能解决循环引用。GC Roots(栈中的引用就是局部变量表中指向堆对象的引用,本地方法栈的引用,静态属性引用,StringTable中的引用,被synchronized持有的对象引用,基本类型Class,常驻异常类,根据不同虚拟机和回收区域不同有分代收集和局部回收),引用链。稳定快照所以必须STW,枚举根节点。可触及,可复活,不可触及(finalize之后不可触及)。finalize线程队列,finalize方法只执行一次。

清除算法
1.标记清除 可达性分析记录到对象头,之后将垃圾记录在空闲列表,新对象直接替换垃圾空间。
2.复制清除 将存活对象复制到新空间,清空旧空间速度最快,新生代使用。
3.标记压缩(标记整理) 先可达性分析之后移动后按顺序排列,清除旧空间,无需维护空闲列表,新对象直接指针碰撞即可。
4.分代收集 因为对象生命周期不一样,将内存分代,不同代采用不同算法
5.增量收集算法 一次收集一小部分,不够再收集更多(G1),与用户线程交换增加收集成本降低吞吐。
6.分区算法 将堆分为多个连续小区域,减少停顿时间。
垃圾收集器
Serial串行,复制算法,老年代使用标记整理,CMS收集器FullGC后备,优势简单而高效。最小化内存和并行开销
ParNew并行,新生代收集,老年代用cms(被干掉之后parnew比较尴尬)。
Parallel吞吐量优先 与cms不能共用框架不一样,老年代采用标记整理,jdk8默认,主打吞吐量
CMS Concurrent mark sweep老年代收集器,低延迟标记清除,不使用标记整理因为用户线程在执行,jdk9中被标记为废弃,jdk14 hotspot中已经被干掉。初始标记,STW,标记出ROOTS直接关联的对象,速度很快。并发标记阶段,从ROOTS直接关联对象触发遍历整个对象图,耗时较长,不需要STW。重新标记阶段STW,修正并发标记用户线程运行产生变动的对象记录。时间比初始标记长,比并发标记短。因为用户线程没暂停所以需要有足够的空间让CMS运行,堆使用率达到某个阈值开始回收jdk6默认92%,预留内存不够会回收失败,临时使用serial old回收老年代,如果使用率增长快,阈值适当降低。缺点会产生内存碎片,运行时导致吞吐量降低,无法回收新产生的浮动垃圾。可以用参数指定多少次fullgc用标记整理整理内存碎片,占用cpu线程数为+3/4。主打低延迟
G1 区域分代,全堆收集,支持更大容量的内存(32×2048=64g)和cpu数量,进一步降低暂停时间。将内存划分为多个区域,不同区域表示eden,s1,s2,old等,根据回收可获得的内存大小及所需时间维护一个优先列表,根据允许的时间优先回收价值最大区域。多线程同时收集用户线程STW,可以与用户线程并发执行。不再要求eden s1 s2,old是连续整块固定的。Region之间是复制算法,整体是标记压缩,堆空间高于6-8GB后G1优势更明显。优势是可预测的停顿时间(软实时softrealtime尽量在给定时间内完成,STW实时)。区域大小为2的倍数1-32mb范围,默认1/2000堆大小,目标是划分出2048个区域。默认200ms最大停顿时间。STW并行最多设置8个工作线程。并发可以是1/4STW线程数
第一步开启G1,第二步设置最大堆,第三步设置最大停顿时间。可以使用用户线程进行GC。Humongous区用于存放大对象,超过1.5个区就放到H区,一个区放不下,用连续的区,都放不下触发fullgc。当堆内存使用45%阈值,开始老年代并发标记。不同区的对象可能被其他区引用,避免扫描整个堆,使用remembered set,每个区有一个set,记录谁引用了此区对象。1 年轻代gc
2.年轻代gc+并发标记3.混合回收,4fullgc。如果发现老年代对象引用新生代对象,先放入脏卡表dirty card queue,新生代回收时先遍历脏卡表更新记忆表,避免直接更新记忆表因为需要同步性能低。
步骤:1初始标记STW触发YGC,2.根区域扫描S区直接可达的老年代对象必须在YGC之前。3.并发标记,可能被YGC打断,计算对象活性,都是垃圾直接回收。4.再次标记STW,因为上一环节并发执行所以这个环节进行修正。5。独占清理STW计算各区存活对象与回收比例并排序为混合回收做铺垫。6.并发清理。混合回收会回收垃圾占比大于65%的区域
超过50%堆对象活跃,年代提升频率高,GC停顿时间长时可以替代CMS

ZGC。
在这里插入图片描述

OOM
dump,使用工具查看占用较多内存的对象,看是否有大对象,可直接定位导致oom的那行代码。或标记当前内存情况,再根据运行看占用增多的对象,尝试gc,锁定某几个对象,单独查看对象引用链。长生命引用短生命对象,需要资源释放的对象容易引起内存泄露
8种内存泄露:
静态集合,单例模式,内部类持有外部类,各种连接、池,变量不合理作用域,改变哈希值(set中的对象改变参与hash字段),缓存泄露,监听器和回调

五、字节码

自动装箱Integer 127~-128缓存使用原对象,超出创建新对象,引用==值,因为自动拆箱的原因合法并不报错。

字节码文件内容
魔数,版本号,常量池表,类、父类、接口索引,访问标识,字段表,方法表,属性表。
在这里插入图片描述
类型标识符
在这里插入图片描述

表的结构有两个,1是长度,1是表结构内容。
常量池jdk1.8有15种内容
在这里插入图片描述
constant_dynamic_info 18,动态方法调用点

方法表有局部变量表长度和操作栈深度

加载与存储
xload(ilfdan) n索引0-3超出去掉_用索引5…,局部变量表的值压入操作数栈
常量入栈:const(-1,5) push ldc,常量表示数值范围依次变大
bipush(8位,-128,127),sipush(16位),ldc,ldc_w,ldc2_w,aconst_null,iconst_m1(-1),iconst
,lconst_…f,d
在这里插入图片描述

出栈入局部变量表:xstroe_
扩充局部变量表访问索引wide
算术,运算结果再次入操作数栈
add,sub,mul,div,rem,neg,inc,位运算,cmp比较栈顶俩元素
i++先入栈后修改局部变量表,++i先修改局部变量表,后入栈,没有其他运算时都是inc x by 1
类型转换
宽化i2f…精度损失并不报运行时异常,char,byte,short,boolean存储都占一个曹32位局部变量表。instanceof将结果压入栈,checkcast检查是否可转,不操作栈
对象创建访问
new anewarray [n][]只初始化一唯数组,数组操作xastroe(弹出3元素) xaload(弹出两元素),是修改堆。
方法调用返回
返回时将栈顶元素压入调用者方法栈帧的操作数栈。
操作数栈管理
pop弹出 pop2 dup复制 dup2 dup_x1 dup2_x1 dup_x2 dup2_x2。nop,swap
比较控制
tableswitch连续的效率高,lookupswitch,会排序,String先比较hash
异常处理
athrow,清空操作数栈,将异常实例压入调用者的操作数栈
同步控制
monitorenter,monitorexit,exit后必执行释放监视器防止死锁
类加载
方法区存类信息,在堆中创建Class模板对象。
什么时候没有clinit:
1.没有静态字段
2.静态字段没有显式赋值
3.静态字段是基本类型且是final。引用类型常量是在初始化阶段clinit中赋值
String字面量常量在准备阶段赋值,new String()常量是在clinit中赋值。
clinit是带同步的,静态代码块中如果有互相加载的情况,可能产生死锁。
主动使用:会调用clinit
各种方式创建对象,反射,调用静态方法,访问依赖外部方法赋值的静态成员,父类先于子类初始化但不会先初始化接口
被动使用:不会调用clinit
通过子类访问父类的静态成员,访问常量,classloader加载类,创建有clinit类型的数组
后续new对象调用构造函数。
不同类加载器可分别加载相同的class文件,这个特性可实现应用隔离比如tomcat

六. 调优命令与工具

X:UserPerfData会导致jps jstat无法查看java进程
-help查看参数

  1. jps 查看java进程,-v
  2. jinfo 查看和修改jvm参数 -flags 只有manageble的才能修改
  3. jstat option参数-class类装载信息,-gc,interval连续间隔输出信息 count输出次数上限。gc时间/程序执行总时间是gc时间占的百分比。
  4. javap -verbose(-v) xx.class查看字节码
  5. jmap 导出内存映像文件与内存使用情况,-dump -heap,-dump:live只导出存活对象可以缩小.hprof文件大小。
  6. jhat jdk自带堆分析工具,内置http服务器,端口7000,分析.hprof文件,jdk9被删除,官方建议用VisualVM 支持oql语句,生产环境不直接用这个分析dump文件
  7. jstack 打印jvm线程快照,解决线程长时间停顿的问题。重点关注的问题:死锁(持有一个锁等待对方锁)、等待资源、等待获取监视器、阻塞、死循环。WAITING等待,BLOCK阻塞,TIMEWATING,定时等待
  8. jcmd 多功能命令,可实现除jstat外几乎所有功能,官方建议用此替代jmap等功能。Thread.print打印线程信息。GC.heap_dump生成dump文件。jcmd 线程号 -help列出这个线程能够用的功能
  9. jstatd 远程主机信息收集
    常用工具
    jconsole
    visualVM 打开、对比、生成堆dump或线程dump。抽样器各方法占用cpu时间。
    jprofiler
    java Flight Recorder
    GCViewer
    GCEasy
    MAT
    arthas linux安装,启动选java进程。页面方式端口8563
    JMC
    Btrace
    浅堆:一个对象消耗内存的大小,不包括引用消耗。32位系统,引用占4字节,int4字节,long8字节,对象头8字节,向8字节对齐。
    保留集:在垃圾回收某个对象时,该对象及其引用对象都能被回收的部分。
    深堆:保留集包括对象本身浅堆之和
    实际对象大小为能触及的所有对象浅堆和
    支配树:只能通过A到B,支配者。A是最近的,直接支配者。

七、运行时参数

标准 -:

非标准:
-X
-Xms,-Xmx默认4分之一物理内存,两个要设置相同
-Xss等价于-XX:ThreadStackSize,JDK1.5后默认1M
-Xmn,官方推荐3/8堆大小,等价于后两者-XX:MaxNewSize,NewSize
非stable
-XX
-XX:NewSize=1024m。key value形式
打印XX选项及设置的值
-XX:+PrintFlagsInitial,PrintFlagsFinal,PrintVMOptions,PrintCommandLineFlags
SurvivorRatio=8默认,NewRatio=2默认
-XX:+UseAdaptiveSizePollicy默认开启,SurvivorRatio配置8不一定是8,因为前边这个参数自动配置了,关掉自动配置并且加SurvivorRatio才是8:1:1。
方法区:-XX:MetaspaceSize

八、面试题

jvm内存分哪些区分别干什么
jdk8内存分代改进
对象进入老年代条件
eden过大会如何,过小会怎样
哪些区会oom
如何调优
根据运行程序的特点与采用的垃圾收集方式来调,结合压测,排除死锁和OOM可能。
设置合理的运行时数据区各部分大小
方法区最大和初始设置相同,根据加载数据的大小设置。有些java程序使用直接内存,比如kafka使用堆外内存,容易导致物理内存不足。堆初始与堆最大相同避免垃圾收集缩容扩容。新生老年代默认比例2,根据程序特点做微调。如果gc晋升老年代的大小在某个年龄之后都差不多,可以适当降低晋升阈值。打开并行引用处理ParallelRefProcEnabled
减少fullgc的发生,对象尽量在新生代阈值内消亡,不要有太大的对象。
对象生命周期很短的可以提高新生代的大小,减少对象进入老年代和minorgc

解决线上问题
OOM,网关压测CPU占用高,压测频繁FullGC
什么情况发生fullgc
老年代不足,方法区不足,System.gc(),大对象进入老年代可用空间却不足。
i++ ++i区别,先入栈后修改局部变量表,与先修改局部变量表再入栈。两个操作如果没有后续操作,字节码是相同的没区别。
什么是空间分配担保,有什么用
Minor GC必须保证假如新生代所有对象都存活,堆空间有足够的连续内存来存储这些对象。所以Minor GC前先检查。如果JVM配置允许担保失败:HandlePromotionFailure=true,则只要堆的空间大于历次进入老年代平均大小,就执行Minor,否则都会触发fullgc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值