JVM
2023-04-07 00:36:24 3 举报
JVM知识点
作者其他创作
大纲/内容
新生代(New) 占 1/3
引用计数法
基本数据类型数值
子类构造方法
javac源代码编译器
class对象
对象头
加载
当第一个线程试图获取锁资源时会先判断匿名偏向锁是否已开启,如果开启会先将markword内除开锁标志位的信息全部copy到自己的栈内存中锁信息LockRecord中并尝试利用自旋将自己的线程ID设置到markword中,以后每次该线程执行用改对象作为锁资源的代码无需加锁和放锁操作,只是在LR中加一个空的markword
可以理解为PC寄存器,作为用于存储计算的临时数据存储区,当CPU执行load指令时能将数据加载进操作数栈
符号引用:类和结构权限定名字段名称和描述方法名称和描述符
G1(并发)全局整理+局部复制
扩展类加载器ExtClassLoader
运行时数据区
TLAB(线程本地内存)2
本地方法接口(Native Interface)
内存完整度1、复制算法2、标记整理算法3、标记清理算法内存利用率1、标记整理算法2、标记清理算法3、复制算法
指针压缩:在64bit的虚拟机中为了提升内存的利用速,所以出现了指针压缩这一技术,指针压缩的技术会将Java程序中的所有引用指针(类型指针、堆引用指针、栈帧内变量引用指针等)都会压缩一半,而在Java中一个指针的大小是占一个字宽单位的,在64bit的虚拟机中一个字宽的大小为64bit,所以也就意味着在64位的虚拟机中,指针会从原本的64bit压缩为32bit的大小,而指针压缩这一技术在JDK1.7之后是默认开启的。指针压缩失效:指针压缩带来的好处是无可厚非,能够节省很大的内存空间,一般而言如果不开启压缩的情况下对象内存需要14GB,在开启指针压缩之后几乎能够在10GB内存内分配下这些对象。但是压缩技术带来好处的同时,也存在非常大的弊端,因为指针通过压缩技术后被压缩到32bit,而32bit的指针最大寻址为32GB,也就代表着如果你的堆内存为32G时出现了OOM问题,你此时将内存扩充到48GB时仍有可能会出现OOM,因为内存超出32GB后,32bit的指针无法寻址,所有压缩的指针将会失效,发生指针膨胀。
分代收集:现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除
-Xms
堆最小空间
-XX:+PrintGC
在控制台输出GC信息
-Xmx
堆最大空间
-XX:+PrintTLAB
可以查看TLAB的使用情况
-Xss
虚拟机栈大小
-XX:+UseSpining
开启自旋锁
-XX:NewRatio
Old/New的比例,默认值:2(old:2/3,new:1/3)
-XX:MaxGCPauseMillis
设置每次年轻代垃圾回收最长时间
-Xmn
年轻代大小,调整会影响老年代大小,对-XX:newSize、-XX:MaxnewSize两个参数同时进行配置
-XX:EliminateAllocations
开启标量替换
-XX:SurvivoRatio
调整Survivorl区和Eden区的大小比例,默认值:4(Eden:8/10,s1:1/10,s2:1/10)
-XX:+DoEscapeAnalysis
开启逃逸分析
-XX:MetaspaceSize
元空间初始化大小,JVM-64bit默认20.75M
+XX:+EliminateLocks
开启同步消除
-XX:MaxMetaspaceSize
元空间最大大小,逻辑限制为物理内存上限
-XX:+PrintEliminateAllocations
查看标量替换情况
-XX:PretenureSizeThreshold
大对象直接进入老年代的阈值(字节为单位)
-server
代表以server模式运行虚拟机
-XX:MaxTenuringThreshold
进入老年代的分代最大年龄阈值,默认:15
-client
代表以client模式运行虚拟机
-XX:TargetSurvivorRatio:
survivor区的目标使用率(动态年龄判断比例设置,计算TenuringThreshold)
-XX:ThreadStackSize
设置线程栈默认大小
-verbose:gc
输出JVM的gc情况
-XX:-UseTLAB
开启TLAB线程本地空间
-XX:+PrintGCDetails
输出GC详细信息
-XX:TLABWasteTargetPercent
TLAB与Eden区的占比
-XX:+PrintTenuringDistribution
输出对象GC年龄信息
-XX:+CollectGenOFirst
FullGC是否先Young GC
-XX:+PrintHeapAtGC
在进行GC的前后打印出堆的信息
-XX:+UseAdaptiveSizePolicy
自适应Survivorz和Eden大小
-XX:GCTimeRatio
设置GC时间占程序运行时间的百分比
-XX:+UseG1GC
用G1垃圾收集器(其他垃圾收集器也是一样,更换名字即可)
-XX:NewSize
设置年轻代的初始大小
-XX:+PrintCommandLineFlags-version
输出默认的垃圾回收器
-Xloggc
日志文件的输出路径
-XX:+PrintGCTimeStamps
输出GC的时间戳(以基准时间的形式)
在新生代达到一定条件的对象会进入年老代1、s0和s1区域交换一次,年龄+1;当年龄达到15岁进入2、大对象直接进入【设置大小判断标准】3、s区相同年龄的对象大小总和超过s区空间的一般,≥该年龄的所有对象直接进入
线程私有区
1、获取class字节码文件的二进制字节流2、将磁盘文件静态结构载入内方法区转换为运行时数据结构<类信息>3、将载入后的类信息进行组装,在堆空间中生成类对象(class),作为数据入口
old
热点代码缓存(Hot Code Cache)JIT即时编译器
本地内存
失败
Synchronizeds膨胀派/升级过程
偏向锁已启动
当出现大量线程(CPU核数一半+)同时竟争轻量级锁或一个线程CAS10次(默认是十次,可以通过-XX:PreBlockSpin调整)以上时轻量级锁发生膨胀,升级到重量级锁
JVM启动后前四秒new出来的对象并未启动匿名偏向锁
当第二个线程尝试获取锁,但是发现第一个线程还在执行,那么会将对象回归无锁状态并将markword中除开锁标志位的信息全部copy到自己的栈内存中锁信息LockRecord中,然后再尝试利用CAS将对象头内markword中的指针指向自己栈内锁信息,能够替换成功代表获取到锁,替换失败继续自旋(小细节:如果此时之前持有偏向锁的线程来获取锁,优先CAS)
不管当前对象是处于何种锁状态,只要调用了Object.wait()方法,那么该锁会直接膨胀为重量级锁,因为wait方法依赖于monitor实现,所以不管什么时候调用wait都会使得对象头中markword内出现指向monitor的指针,而markword的改变对于用户线程是不可逆的,一旦出现monitor指针,就无法回到之前的锁状态
加载项目classpath的class
验证文件格式/元数据/字节码/符号引用等
运行时常量池
父类变量/代码块
加载/jre/lib/rt.jar
字符串常量池
对象成员分配内存后初始化顺序
操作数栈
准备
类加载检测
类型指针
抽象语法树
加载/jre/lib/ext的jar
执行init
类(class)信息属性方法类元信息【类型信息/类型的常量池/方法信息/字段信息/类加载器的引用信息/class对象实例引用信息/方法表】
局部变量表
匿名偏向锁为开启
安全点的定义: 1. 循环结束的末尾段 2. 方法调用之后 3. 抛出异常的位置 4. 方法返回之前
CMSl(并发)清除
线程共享区
匿名偏向
注解抽象语法树
本地方法栈Native Method Stack
即时编译:是在程序执行过程中能够将频繁执行的代码(热点代码,Hot Spot Code)的字节码转换成可立即执行的指定平台的可执行代码,并缓存已供下次使用,提高热点代码(Hot Spot Code)的执行效率。虽然Java虚拟机规范没有明确必须要实现JIT即时编译技术,但商用的虚拟机里面都实现了JIT即时编译技术。JIT即时编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分在Hot Spot 中 Java 程序通常情况下都是通过解释器( Interpreter )一边解释一边执行的模式运行代码的,当虚拟机检测到某个代码块或者方法体执行非常频繁时,会将这些检测到的代码判定为热点代码。而如果这些热点代码都通过解释器的模式执行,那么每次执行都需要用到解释器解释,为了提升热点代码执行效率,在运行时虚拟机会将这些代码编译成与当前系统+硬件相关的机器码并进行各层面的优化,同时也会将方法的调用地址指向编译后的机器码并且也会将编译后的机器码保存到磁盘,而这个编译热点代码的编译器被成为 JIT即时编译器。 PS :当热点代码被检测到时,不管是代码块还是方法体,虚拟机都是以方法为最小单位进行编译。Hotspot 中实现的两种 JIT 即时编译器: ClientCompiler ( C1 ) :局部优化,追求编译速度;ServerCompiler ( C2) :充分优化,追求编译质量。会根据 JVM 运行模式来决定选择谁。
一、当遇到new指冷时,VM首先会进行类加载检测:①检测new指令的参数是否能在常星池中定位类的符号引用。②检测这个符号引用是否进行过加载解析和初始化,没有则先对该类进行类加载。
Eden 8/10
JVM启动后后四秒new出来的对象默认启动匿名偏向锁
由OOP思想中存在多态的概念使得编译器在编译源代码时无法确定对象类型,只有在运行时才能确定对象,指向常量池中的方法引用
新生代分配
Serial(串行)复制
Epsilon:无作用GC器,当你的程序不需要GC时可以选择使用该GC器
堆空间(Head)
class字节码文件
四、完成初始化操作后接着会对于对象的对象头进行设置:1、mrakword:存储对象自身的运行时数据,如nashcode、GC分代年龄、锁标志、锁信息等;②klassword:类型指针,指向它对应的类元数据,VM用这个确定其属于哪个类的实例。
new
32位JVM的对象头信息
64位JVM的对象头信息
GC
否
<clinit>:方法是类构造器方法,只对类方法(static域和static代码块)进行初始化
Survivor1【占1/10】(To s1)
标记-清除算法:标记阶段通过根可达算法标记出所有存活对象;清除阶段对于所有未标记对象进行同一回收缺点: 1、两个阶段效率都不高 2、会产生大量的内存碎片优点:简单
Survivor0【占1/10】(From s0)
偏向锁标识(biased_lock)
锁标识(lock)
0
01
正常锁
1
偏向锁
类加载子系统
由于CPU在执行的时候会存在线程时间片切换的概念,所以CPU执行指令的时候是会中断,程序计数器会记录当前线程执行停止的字节码指令位置(行号),以便于再次切换到改线程时能够恢复到正确的执行位置而避免重新执行
尝试栈上分配
ParellelScavenge(并行)复制
动态链接
执行类构造器<clinit>,生成了class对象,放入堆中
重量级锁
值初始化
字面量:文本字符串final常量池基本类型数值
Parellel old(并行)复制
空闲空间
JVM调优
轻量级锁
方法出口
wait
java文件
如果第一个线程试图获取锁时判断匿名偏向发现偏向锁未开启时会直接膨胀为轻量级锁,先将markword中除开锁标志位的信息全部copy到自己的栈内存中锁信息LockRecord中,然后再尝试利用CAS将对急头内markword中的指针指向自己栈内锁信息,能够替换成功代表获取到锁,替换失败继续自旋
young
语法分析
初始化
栈帧2………………
对象内存分配
普通对象
元数据空间【方法区/永久区】(Meta Space)
五、最后执行<iit>函数,也就是类的构造函数,主要是对属性赋值
MinorGC
重点竞争耗时过长自旋过多等
安全区(SafePoint)
轻度竞争
语义分析
使用
用于存放byte、short、int、long、float、double、char、boolean八种基本数据类型的数据,存放已变量槽(slot)为最小单位(32bit),double、long数据类型需要2个slot存储,所以会出现线程安全问题基本数据类型存储数值本身,引用数据类型存储指向对内存的应用指针
复制算法:将原有的内存区域一分为二,在同一时间只会使用其中一块内存区域用来分配对象,在发生GC时,首先通过根可达算法判断存活的对象,并将所有的存活对象移动到另一块未使用的内存区域,最后对于原来的内存区域中的对象进行同一回收。缺点: 1、不适用于对象多的情况,移动对象对于空间和时间的开销很大; 2、内存利用率太低了,无论何时永远有一半的内存区域浪费优点:内存完整,不存在内存片段
成功
词法分析
…………
程序计数器Program Counter Register
尝试TLAB分配
生成
用户自定义加载器
逃逸分析:判断变量作用域是否存在于其他栈帧或者线程中。当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸,判断逃递的方法被称为逃逸分析。逃逸的作用域: ①栈帧逃逸:当前方法内定义了一个局部变量逃出了当前方法或栈帧。 ②线程逃逸:当前方法内定义了一个局部变量逃出了当前线程能够被其他线程访问。全局变量赋值逃逸:当前对象被赋值给类属性、静态属性参数赋值逃逸:当前对象被当作参数传递给另一个方法方法返回值逃逸:当前对象被当做返回值return
字节码生成器
对于每个对象都自带一个计数器,有对象引用自己时+1,取消引用时-1。当计数器为0的时该对象标记为垃圾可以被回收,如果a,b相互引用会导致循环问题造成的内存泄漏,无法垃圾回收,所以主流的虚拟机都没有采用这种算法
对象存活判断算法
TLAB(Thread Local Allocation Buffer):不管能不能分配下都是在Eden区进行分配,因为TLAB指的是Hostpot对于new对象的优化。如果多个线程同时在创建(new)对象时,都看中同一块内存地址,会浪费很多争抢资源的时间,所以Hostpot为每一个线程都在Eden区分配一块专属空间供线程使用。
JIT(Just In Time Compiler)即时编译器
引用级别:强引用→软引用→弱引用→虚引用
栈帧1
安全点:无论是在GC中还是线程安全中都会出现安全点(SafePoint)这个概念,当我们需要阻塞一个线程时都需要在安全点停止,简单说安全点就是指当线程运行到这类位置时,堆对象状态是确定一致的,当前线程停止后,JVM可以安全地进行操作,如GC、偏向锁撤销等。当JVM需要发生GC、偏向锁撤销等操作时如果让所有线程到达安全点阻塞?1、主动式中断(JVM采用的中断方式):不中断线程,而是设置一个标志,然后让每个线程执行时主动轮询这个标志,当一个线程到达安全点后,发现中断标志为true时就自己中断挂起2、抢断式中断:先中断所有线程,如果发现线程未执行到安全点则恢复线程让其运行到安全点位置。安全区域(SāfeRegion):当一个线程处于中断或者休眠状态时就不能响应JVM的中断请求走到安全点区域挂起了,所以出现了安全区域的慨念。安全区域是指一个线程执行到一段代码时,该区域的代码不会改变堆内对象的引用,在这区域内JVM可以安全地进行操作。当线程进入到该区域时需要先标识自己进入了,这样GC线程则不会管这些已标识的线程,当线程要离开这个区域时需要先判断GC Roots是否完成,如果完成了则往下执行,如果没有则需要原地等待到GC线程发出安全离开信息为止。
本地方法库(Native Libraries)
实例对象
卸载
对象年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代;只有4位,最大值15,所以参数-XX:MaxTenuringThreshold最大值15unused:保留位,没有被使用 epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁偏向锁:由于正常锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
(并行)复制
同步消除:通过逃逸分析对一个对象进行逃逸判断之后,如果该对急为线程级作用域不可逃逸时,则代表当前这个对象只会有一个线程可访问。如果当前这个对象是被作为同步锁对急的,那么JVM会在编泽时消除加锁和解锁的代码。决定能香同步消除满足一个即可:①当前对象被分配在栈上。②当前对象的无法逃出线程作用域。
GC器 连线代表可搭配使用
老年代(Old)占 2/3
偏向锁未启动
子类静态变量/静态代码块。
标记-整理算法:标记阶段通过根可达算法标记出所有存活对象,然后再清除阶段对于所有未标记对象进行同一回收缺点: 1、两个阶段效率都不高 2、会产生大量的内存碎片优点:简单
直接内存
设置对象头
标记整理算法
高低
根据验证后的类信息初始化类结构变量
是否满足old代分配条件
常量池符号引用替换为直接引用【因为多态】
二、创建一个对象所需要的内存在类加载完成时就能确定,内存分配是指在堆中划出一块和对象大小的对应内存出来,具体的分配方式根据堆内存的整齐性决定,而堆内存的整齐性则由当前程序采用GC机制快定。
空闲列表:与指针碰撞一样,空闲列表同样是Java在为新对象分配堆内存时的一种内存分配方式,一般适用于CMS等一些会产生内存碎片、堆内存不完整的垃极收集器。分配过程:堆中的已用内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的空闲内存块信息,当创建新对象需要分配内存时,从列表中找到一个足够大的内存块分配对象实例,并同步更新列表上的记录,当GC收集器发生GC时,也会将回收的内存功新到内存列表。
根可达算法
根加载器BootStrapClassLoader
Shenandoah(并发):全局整理+局部复制
java对象创建过程
对齐填充【8的整数倍】
MojorGC
是
span style=\"font-size: inherit;\
Mark Work
GC发生时,GC线程和用户线程操作的同一块内存区域,如果用户线程不停止,GC线程刚刚标记完一块区域,用户线程就在这个区域创建一个新对象,当回收的时候,这个对象还是存活,但是仍会被清除,从而影响用户线程,造成线程安全问题。STW(stop-the-world):在GC发生时,所有用户线程都需要挂起停止,对于用户而言就是整个程序停止不动,直到GC完成后用户的线程才能重新恢复执行。
直接引用:①直接指向内存地址的指针。②相对的偏移量(方法指针指向实例变量)③句柄池中间接指向内存地址的句柄符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。在类加裁时的解析阶段,会有一步操作就是将class文件中的符号引用替换为直接引用,简单来说就是:在编泽的时候一个每个Java类都会被编译成一个class文件,但在编译的时候因为类还没有被装载进内存,所以虚拟机并不知道引用类的内存地址,所以就用符号引用来代替,而这个解析阶段就是为了把这个符号引用转化成为真正的地址。
ZGC(并发):全局整理+局部复制
引用(reference)
数组长度
与虚拟机栈相似,但是本地方法栈是为虚拟机的Native方法提供服务;Hotspot将虚拟机栈和本地方法合二为一
双亲委派模型:当前类加载器需要加载XX类时,不会自己直接加载,而是尝试将加载这个类向上委托给父加载器去完成,一直找到根加载器(Bootstrap ClassLoader),如果父加载器能加载则由父加载器加载,否则由父加载器逆向由子加载器加载。好处:如果外部想要重写JDK的系统类【如String】,篡改系统类的实现,因为BootstrapClassLoader已经加载JDK的系统类,不会加载用户编写的类,从而一定程度防止了危险代码的植入
编译
来回交换,同一时间内永远有一个为空
TLAB(线程本地内存)1
TLAB(Thread Local Allocation Buffer):在Eden区为每个线程开辟的一个缓存空间,线程在创建对象时,如果该缓存区的大小能够承载对象大小则直接在该区域分配对象,能够避免多个线程同时创建对象时由于竞争同一块堆内存时产生的资源消耗
解析
子类变量/代码块
指针碰撞:指针碰撞是Java在为对象分配堆内存时的一种内存分配方式,一股适用于Serial和ParNew等不会产生内存碎片、堆内存完整的的垃级收集器。分配过程:堆中已用分配内存和为分配的空闲内存分别会处于不同的一侧,近过一个指针指向分类点区分,当VM要为一个新的对象分配内存时,只需把指针往空闲的一侧移动与对象大小相等的距离即可
垃圾回收算法
老年代分配
应用加载器AppClassLoader
父类构造方法
标记清除算法
记录方法结束时的出栈地址(正常执行结束返回地址或者由于报错结束时的异常地址)
父类静态变量/静态代码块
Full GC
标记复制算法
TLAB分配
通过GCRoots(栈中变量/方法区静态变量/Monitor持有者对象/常驻异常对象/JVM内部引用/方法区常量)对象作为起始点向下搜索,搜索走过的路径被称为引用链,当一个对象到GCRoots没有任何引用链时,即根本不可到达时,该对象则判断为不可用,可以被回收
分配方式:①指针碰撞(堆整齐)②空闲列表(堆不整齐)分配出现并发情况解决方案:①CAS自旋②TLAB本地内存
栈上分配:一般Java对象都在堆上进行内存分配,但其实并不是所有的对象都会在堆中分配,有时候也能够在栈上进行分配。不过想要将对象在栈上进行分配,需要先开启标量替换以及逃逸分析。决定一个对象能在栈上分配的因素: 1、对象能够通过标量替换分解成一个个标量。 2、对象在栈帧级作用域不可逃逸。
验证
栈帧3………………
垃圾回收GC 分代回收GC器 G1承上启下:逻辑分代物理不分代 不分代回收GC器
已使用空间
标量替换:使用标量替换聚合量(对象),对于一个对象进行分解,将其拆解成一个个小的标量的过程(需要先进行逃逸分析,不可逃逸的对急才能进行标量替换)。标量:不可分割的量,指基本数据类型和reference类型好处:1、能够节省堆内存,因为进行标量替换之后的对象可以在栈上进行内存分配。2、相对运行而言省去了在堆中查找对象引用的过程,速度会更快一些。3、对象在栈上分配空间,所以随着方法结束和线程栈的弹出自动销毁,不需要GC的介入。
虚拟机栈(Stack)
ParNew(并行)复制
执行引擎(Execution engine)
①处于并列优先级的按照va程序编写时代码的先后顺序加载。②前面两步为静态过程,程序运行过程中只会执行一次,之后再次创建该类或该子类的对象时并不会再次执行
当第二个线程试图获取锁资源时发现偏向锁已开启,那么会进行上一步操作,尝试将自己的线程D设置到markword中,但是之前第一个线程已经赋值,对象头内存储的是第一个线程的ID,那么此时会判断是否是安全点(第一个线程是否已执行完毕),如果第一个线程已经执行完毕,那么会发生偏向锁撤销(将锁对象清除锁记录并回归无锁态),然后第二个线程重新cas自旋将自己的线程ID设置到markword中,重新偏向
一、调优指标:1、降低回收STW延迟;2、提升最大吞吐量;3、减少内存占用量二、调优原则: 1、MinorGC回收原则:每次MinorGC都要尽可能回收更多垃圾对象,以减少应用程序发生Full GC的频率。 2、GC内存最大化原则:处理吞吐量和低延迟问题的时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅 3、GC调优3选2远程:在性能属性里面,吞吐量、延迟量、内存占用率,只能选择两个进行调优三、JVM调优参数
returnAddress
0 条评论
下一页