JVM是java的核心和基础,在java编译器和os平台之间虚拟处理器.
Java编译器只需面向JVM,生成JVM能理解的代码或字节码文件. java源文件经编译器编译成字节码程序通过JVM将每一条指令翻译成不同平台机器码,通过特定的平台运行
(源文件).java--->javac-->.class(字节码文件)--->JVM
字节码文件首先通过jdk帮我们实现的类装载子系统加载到JVM虚拟机运行时数据区(内存模型),但其实类装载子系统也属于JVM的一小部分 ,然后字节码执行引擎会到运行时数据区(也就是内存区)中找字节码文件,运行相关的代码执行相应的功能
栈stack 线程栈 先进后出
main方法为主线程 一个线程运行main方法说白了就是运行主线程
每个线程都有程序计数器内存区域 存放正在执行的程序在JVM中相应的位置,每执行一段修改一下相应的位置,由字节码执行引擎进行相应的修改
栈帧 栈内部的一小块区域
一个方法对应一块栈帧内存区域,栈帧本地线程的开启而创建也就是开始执行该方法的时候创建,方法执行完毕之后栈帧消失.
- 局部变量 --字节码 JVM指令查看, 存放变量名,根据字节码指令由操作数栈中的值进行赋值
- 操作数栈 用于存放临时的操作数,并进行相应运算 先进后出,,运算完之后就出栈(从栈顶弹出最新的元素进行相应的运算)
- 动态链接 将符号引用转变为直接引用 符号引用(方法名) 直接引用(方法名在类中也就是该类的对象中的对象头中对该方法标识的字节码信息在方法区中类元信息中存放的位置)一般对象头中存放的就是该对象在方法区中的类元信息中所对应的位置
- 方法出口 如果主线程也就是main方法中调用另一个方法或者是一个方法中调用另一个方法,那么在编译的字节码中此方法对应的字节码行号也就是程序计数器中记录的位置就是方法的出口,就是说在主线程中执行到此处该调用该方法时,调用的方法执行完毕,又回到主线程继续向下执行,字节码行号是记录此线程中断的位置便于之后继续执行.方法出口就是此时方法执行完毕之后的字节码行号,用于标识在程序中的位置
方法区(jdk1.8之后改的新名称元空间,1.8时还有叫永久代,持久代)
- 常量
- 静态变量 如果静态变量是对象那么方法区中存放的指向堆中对象的地址
- 类元信息 类的元素信息 类的组成信息 类的组成信息例如类名,方法以及修饰符,被类装载子系统进行解析成字节码放入类元信息中
对象中的对象头中存放对象的类元信息的也就是字节码信息在方法区中存放的位置
本地方法栈
本地方法 底层由C语言实现
作用 在早期95时java开始实行,再此之前都是C的天下,,使用java之后还要对之前的C做交互更改,就用本地方法进行交互 由java语言通过本地方法调用C语言,来实现java与C之间的交互,
本地方法栈中存放本地方法的局部变量,本地方法栈也是每个线程都有的
新创建的对象放在Eden区,堆的内存是有限制的,老年代分到3/2,年轻代分到3/1
年轻代:
- Eden (伊甸园区)新创建的对象存放在此处,,当此处的内存放满时,进行minor GC 英 /ˈmaɪnə(r)/未成年的 将无引用,无效的对象(当线程执行完毕,也就是该方法执行完毕那么方法中的引用变量在栈中存放的指针被清除,导致堆中存放的对象没有对应的引用变为游离的对象,此时就是无引用对象,无效对象,,可以通过GC roots根来判断是否是有用的对象{可以通过本地方法栈中变量,常量,静态变量,线程栈的本地变量等变量来向下进行延伸直到的没有引用的根节点形成一条链,除链上的引用之外,剩余的游离的都是要回收的无用对象})进行回收掉,将其他的有引用的,还存活的对象放置其他区域,(survivor区)
如果一个对象经过一次minor GC机制还存活下来,那么该对象中的分代年龄值会+1,将对象移至survivor区中的From区,分代年龄的值存放在对象头中,其实对象头中有很多java虚拟机底层的东西
-
当程序7*24小时不断创建新对象时,如果From区非空时,也会进行minor GC机制回收无用对象,如果此时From中的对象经过minor GC仍存活那么就将其移至To区并且分代年龄+1,其实这些动作都是由字节码执行引擎来做的,不断地存值的过程中在进行minor GC时会加上To区进行一块的minor GC回收,新的对象的分代年龄为0,当Eden区放满时,进行minor GC此时Eden ,From ,To区这三者都进行GC回收,存活下来的对象放入From中,分代年龄都+1,这个时候To区就被清空了,当再创建新的对象时,minor GC回收,将存活的对象再放入To区,分代年龄+1,7*24小时不断创建对象的时候就这么个过程来回轮询,等到对象的分代年龄达到15时,如果还存活,那么这些对象将被放入老年代,在web程序中的静态变量是对象,线程池,连接池,系统初始化的缓存数据,spring容器中的Bean例如被@Service,@Controller修饰的Bean等,这些都会成为老年代,当老年代存放满时,会进行full GC,full GC会对整个堆进行垃圾回收,在GC的时候其他web应用程序功能都暂停,专心进行GC,这种现象成为STW(Stop-The-World),GC会对应用程序造成卡顿现象,所以JVM性能调优
减少GC次数
减少一次GC执行时间
每次的GC都会带动其他区进行GC,,
当老年代放满时,仍然有不断的对象进行创建时会报OOM(out of memory)内存不足的异常
- survivor英 /səˈvaɪvə(r)/ 幸存者
- From
- To
JVM调优 其实最主要的是对堆,方法区进行调优---->JVM调优思路: GC日志
当程序执行的时候给程序加一个打印GC日志的参数,那么在程序运行过程中所有进行GC相关的操作都会打印到日志中
Full GC对应用程序影响是最大的,正常情况下几个小时,几天或几周执行一次才是最好的,如果几秒执行一次对应用程序的启动时间有影响,,有的做web应用系统好多年,war包几个G那每次改个程序重新启动的时候会等好长时间几十秒甚至几分钟,这是war包太大,几十兆几个G,
那么在应用程序在启动的时候加上参数将GC执行的机制打印出来查看Full GC原因,有时会发现大量执行full GC是元空间导致的,这就说明对元空间没有一个好的设置就比如说你设置的默认值就一二十兆,再加上几G的代码量,导致元空间的内存不足,进而执行full GC,那么启动程序当然慢了,对于这样的优化,应该是一次性把元空间的内存调大一点,不使用默认内存,然后再启动GC日志会发现里面几乎没有什么full GC了,应用程序启动绝对会快很多
Minor GC如果过多或过于频繁也是需要调优的
CPU中的摩尔定律每隔18个月更新一次运算速度,存储速度会提高很多至少翻一倍
Volatile:保证多个线程之间共享变量的可见性
- survivor英 /səˈvaɪvə(r)/ 幸存者
-
底层的原子操作:主要是用来表示共享变量在线程之间是如何流转的
多核CPU运行: 每个线程在一个核上运行
多个线程的共享变量不可见----共享变量为被volatile修饰
共享变量被volatile修饰,,多个线程之间的可见性
但是在早期,实现共享变量多个线程之间的可见性-----缓存一致性协议:总线加锁
当第一个到达主线程中获取共享变量进行操作时,会对这个数据进行加锁lock,此时其他线程也要访问这个变量或操作这个变量只能排队等待,等此时第一个线程也就是拿锁的线程执行完毕之后释放锁unlock,才能进行下一个线程的操作,所以主线程加锁方式的效率极低
硬件级别:M修改 E独占 S存储 I无效
总线:CPU与内存之间的交互硬件与主板之间的交互都是通过总线
CPU会对总线做一个嗅探(也就是监听)监听数据的变动,当某个线程对共享变量做出修改,在数据store时经过总线,那么此时总线嗅探机制就会把信息传输到响应的监听出,也就是某个线程会通过对总线的嗅探,根据嗅探到的信息将自己工作内存中的变量副本值变为无效I ,那么此时该线程的执行引擎得知此时工作内存中的变量值无效,就马上再到主内存中获取新的值进行read,load操作
保证多线程之间共享变量之间副本的一致性
Volatile的底层使用C语言实现所以点不进去,java高级语言,低级语言虽然编写起来比较复杂,但是可以看出底层更多的逻辑
C语言先变成汇编语言再变成机器语言
查看java的汇编语言,,解压相应的jar文件放在jdk的bin目录下,然后加上相应的参数
汇编语言:
-
Lock的第一层含义 会将当前处理器缓存行中的数据立即写到系统主内存,不管该数据之后是否还有其他逻辑要执行,先将数据写入主内存
第二层触发总线的缓存一致性协议和总线嗅探机制
也就是当该线程在处理器缓存行中对共享变量进行修改,一旦修改那么立即就会直接写入主内存不管该变量之后是否有其他的逻辑操作,直接写入主内存,写入主内存的过程中经过总线会触发缓存一致性协议和总线嗅探机制,那么此时其他线程对总线的嗅探就监听到信息将其处理器缓存行中对应的变量值设为I无效状态,那么此时该线程的执行引擎感知到变量已无效,就会再去主内存中进行重新的read,load
但是在进行read,load之前还要查看主内存是否有锁,只有此时unlock时才能read,load
Volatile的lock锁是加在store之前,当值赋值完之后释放锁unlock
其实说白了volatile的底层就是通过汇编语言lock的前缀指令实现共享变量的可见性
以上是我的观看笔记,纯属自己的笔记整理,不做任何宣传