jvm是什么?

Java Virtual Machine —-java程序的运行环境。
JVM是虚拟机的英文简称。它是java运行环境的一部分,是一个虚构出来的计算机,它是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
好处:一次编写,到处运行;自动内存管理,垃圾回收功能;数组下标越界检查;多态。

jvm jre jdk 比较?
image.png
常见的jvm
image.png

jvm体系结构?

image.png
image.png

PC寄存器

程序计数器:program counter register
jvm指令 > 二进制字节码 > 解释器 > 机器码 > CPU
作用:记住下一条jvm指令的执行地址。
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的字节码方法(用来存储指向下一条指令的地址,也就是即将要执行的指令代码),由执行引擎读取下一条指令,它是一块很小的内存空间,几乎可以忽略不计。不会有内存溢出。

栈 (线程运行时需要的内存空间)

栈:数据结构
程序 = 数据结构 + 算法
关于栈:先进后出,后进先出,桶
队列:先进先出(FIFO:first input , first output )管道
为什么main方法先执行后结束?

栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就over!
栈中:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧,每个方法运行时需要的内存,每个方法的调用就是一个栈帧。每个线程都只能有一个活动栈帧,对应着当前正在执行的那个方法。
栈内存溢出:StackOverflowError
· 栈帧过多导致
· 栈帧过大导致
-Xss 设置栈内存参数
线程运行诊断:
image.png
栈+堆+方法区:交互关系
image.png

本地方法栈(Native)

native :凡是带了native 关键字的,说明Java的作用范围达不到了,会去调用底层c语言的库!
会进入本地方法栈,调用本地方法接口 JNI (java native interface)
JNI的作用:扩展java 的使用,融合不同的编程语言为java所用! 最初 : c 和c++。
java诞生的时候 c和c ++ 横行,想要立足必须要有调用c和c++的程序,
它在内存中专门开辟了一块标记区域,Native Method Stack 登记native 方法
在最终执行的时候,加载本地方法库中的方法通过JNI。
常用在java程序驱动打印机,管理系统,在企业级应用中较为少见。

三种JVM

image.png

堆(也叫运行时数据区)

Heap,一个jvm只有一个堆内存,堆内存的大小是可以调节的。
通过new关键字,创建对象都会使用堆内存。
他是线程共享的,堆中的对象需要考虑线程安全的问题。
类加载器读取了类文件以后,一般会把什么东西放到堆中?类,方法,常量,变量,保存我们所有引用类型的真实对象。堆内存中从GC的角度还细分为三个区域:
新生区(伊甸园区)Young/New ( Eden Space)
养老区 Old
永久区 Perm
image.png
GC垃圾回收主要是在新生区和养老区。
假设内存满了,oom报错(OutOfMemoryError),堆内存不够。
在JDK8以后,永久存储区改了名字叫元空间。

新生区

一个类:诞生和成长的地方,甚至死亡;
伊甸园,所有的对象都是在伊甸园区new出来的!
幸存者区(0,1)
image.png
真理:经过研究,99%的对象都是临时对象!

永久区

这是常驻内存的,用来存放jdk自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟就是释放这个区的内存。
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类,不断地被加载,知道内存满,就会出现OOM。
jdk1.6之前:永久代,常量池在方法区中;
jdk1.7 :永久代,但是慢慢退化了,去永久代,常量池在堆中;
jdk1.8之后:无永久代,常量池在元空间(metaSpace) ;
image.png
image.png
元空间逻辑上存在,物理上不存在。
元空间并不在虚拟机中,而是使用本地内存。

默认情况下:虚拟机分配的总内存是 电脑内存的四分之一 ,而初始化的内存是六十四分之一;
OMM报错?(堆内存溢出)
1、尝试扩大堆内存看结果 (-Xms1024m -Xmx1024m -XX:+printGCDetails)
2、分析内存,看一下哪个地方出现了问题(内存快照分析工具Jprofiler 专业工具)

jps工具:查看当前系统中有哪些java进程
jmap工具:查看堆内存占用情况 jmap - heap 进程id
jconsole工具:图形界面的,多功能检测工具,可连续监测

方法区

Method Area 方法区 ,也被称为永久代 。
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实际变量存在堆内存中,和方法区无关。
static final Class类 常量池
image.png
运行时常量池:
常量池,就是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
StringTable 串池特性:
· 常量池中的字符串仅是符号,第一次使用时才变为对象
· 利用串池的机制,来避免重复创建字符串对象
· 字符串变量拼接的原理是StringBulider(1.8)
· 字符串常量拼接的原理是编译期优化
· 可以使用intern方法,主动将串池中还没有的字符串对象放入串池:
1.8中,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入,会把串池中的对象返回
1.6中,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份放入串池,就是两个不同的对象,会把串池中的对象返回
StringTable垃圾回收:
StringTable调优:
调整-XX:StringTableSize=桶个数
考虑字符串对象是否入池

直接内存

image.png
DirectMemory(直接内存)
常见于NIO操作,用于数据缓冲区
分配回收成本高,但读写性能高
不受JVM内存回收管理
直接内存溢出 :image.png
分配和回收原理
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用的freeMemory方法
ByteBuffer的实现类内部,使用的Cleaner(虚引用)来监测ByteBuffer对象,一旦使用ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存

GC垃圾回收

image.png
jvm在垃圾回收时,并不是对这三个区域统一回收,大部分时候回收都是新生代。
GC两种类:轻GC(普通的GC),重GC(全局GC)
题目:
1、JV内存模型和分区~详细到每个区放什么?
2、堆里面的分区有哪些?Eden,from,to,老年区,说说他们的特点?
3、GC的算法有哪些?标记清除法,标记压缩,复制算法,引用计数器,怎么用的?
4、轻GC和重GC分别发生在什么时候?

如何判断对象可以回收

引用计数法
image.png
可达性分析算法
java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
哪些对象可以作为GC Root
四种引用

image.png

垃圾回收算法

复制算法
Minor GC 会把Eden(伊甸园)中的所有活的对象都移到Survivor(幸存区)区域中,如果Survivor区中放不下,那么剩下的活的对象就被移动到老年代中,也就是说,一旦收集后,Eden就是变成空的了。
当对象在Eden(包括一个Survivor区域,这里假设是From区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳 (上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden 和 From 区域中存活的对象),则使用复制算法将这些仍然还活着的对象复制到另外一块Survivor区域(即 to 区域)中,然后清理所使用过的Eden 以及Survivor区域(即form区域),并且将这些对象的年龄设置为1,以后对象在Survivor区,每熬过一次Minor GC,就将这个对象的年龄 + 1,当这个对象的年龄达到某一个值的时候(默认是6,最大值是15,通过-XX:MaxTenuringThreshold 设定参数)这些对象就会进入老年代。
image.png
image.png
坏处:浪费内存空间,多一半空间永远是空的。
标记清除算法:
image.png
优点:不需要额外空间。
缺点:两次扫描,严重浪费时间,会产生内存碎片。
标记压缩算法(标记整理):
再优化
image.png
总结:
内存效率:复制>标记清除>标记压缩
内存整齐度:复制算法 = 标记压缩算法 > 标记清除
内存利用率:标记压缩 = 标记清除 > 复制

分代垃圾回收

image.png
相关VM参数
image.png

垃圾回收器

串行
· 单线程
· 堆内存较小,是个个人电脑
开启串行垃圾回收器:-XX:UseSerialGC = Serial(复制算法) +SerialOld(老年代)
image.png
吞吐量优先
· 多线程
· 堆内存较大,多核CPU
· 让单位时间内,STW的时间最短
开启吞吐量优先垃圾回收器:-XX:+UseParallelGC(新生代) ~ -XX:UseParallelOldGc(老年代)
自适应大小策略(新生代),动态调整伊甸园和幸存区比例大小:-XX:+UseAdaptiveSizePolicy
调整吞吐量目标,垃圾回收时间和总时间的占比:-XX:GCTimeTatio=radio
垃圾回收暂停时间:-XX:MaxGCPauseMillis=ms
控制线程数的参数:-XX:ParallelGCThreads=n
image.png
响应时间优先
· 多线程
· 堆内存较大,多核CPU
· 尽可能让单次STW的时间最短
开启响应时间优先垃圾回收器:-XX:+UseConMarkSweepGC(并发标记清除垃圾回收,老年代) ~
-XX:UseParNewGC(新生代) ~ SerialOld
并行垃圾回收线程数:-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads(并发垃圾回收线程数)
执行CMS垃圾回收的内存占比:-XX:CMSInitatingOccupancyFracton=percent
重新标记之前对新生代进行垃圾回收:-XX:+CMSScavengeBeforeRemark
老年代:
image.png
G1(Garbage First)
image.png
相关JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1垃圾回收阶段:
image.png
YoungCollection + Concurrent Mark阶段:
YoungGC 是会进行GC Root 的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会swt),由JVM参数决定。
-XX:InitiatingHeapOccupancyPercent(默认45%)
Mixed Collection阶段:
会对E、S、O进行全面回收
最终标记会STW(Remark)
拷贝存活会STW(Evacuation)
-XX:MaxGCPauseMillis=ms
Full GC
image.png
G1垃圾回收器优化:
JDK8u20字符串去重image.png
JDK8u40并发标记 类卸载
image.png
JDK8u60回收巨型对象
image.png
JDK9并发标记起始时间的调整
image.png
JDK9有更高效的回收

垃圾回收调优

image.png
最快的GC是不发生GC
image.png
新生代调优
新生代的特点
所有的new操作的内存分配非常廉价
TLAB thread-local allocation buffer
对象死亡的回收代价是零
大部分对象是用过即死
MinorGC的时间远远低于FullGC
新生代能容纳所有(并发量 (请求-相应))的数据
幸存区大到能保留(当前活跃对象+需要晋升对象)
晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold配置晋升阈值
-XX:+PrintTenuringDistirbution显示晋升阈值详细信息
*老年代调优

.CMS的老年代内存越大越好
先尝试不做调优,如果没有Full GC那么已经…,否则先尝试调优新生代观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
-XX:CHSInitiatingOccupancyFraction=percent设置老年代内存达到设置比例是进行FullGC

类加载与字节码技术

类文件结构

image.png

字节码指令

image.png
image.png
javap工具
自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译class文件。
图解方法执行流程
原始java代码
image.png
编译后的字节码文件
image.png
image.png
image.png
image.png
类文件常量池载入运行时常量池
image.png
方法字节码载入方法区
image.png
mian线程开始运行,分配栈帧内存
image.png
执行引擎开始执行字节码
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
编译期处理(语法糖)
语法糖就是指java编译器把.java源代码编译为.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利。
默认构造器:
image.png
自动拆装箱:
image.png
泛型集合取值:
image.png
可变参数:
image.png
foreach循环:
image.png
image.png
Switch字符串:
image.png
image.png
switch枚举:
image.png
image.png
image.png
枚举类:
image.png
image.png
try with resource:
image.png
image.png
image.png
方法重写时的桥接方法:
image.png
image.png
匿名内部类:
image.png
image.png
image.png

类加载

加载
将类的字节码载入方法区中,内部采用C++的instance Klass来描述java类,它的重要field有:
_java_mirror即java的类镜像,例如对String来说,就是String.class,作用就是把klass暴露给Java使用。
_super即父类
_fields即成员变量
_methods即方法
_constant即常量池
_class_loader即类加载器
_vtable虚方法表
_itable接口方法表
· 如果这个类还有父类没有加载,先加载父类
· 加载和链接可能是交替运行的
instanceKlass这样的【元数据】是存储在方法区(1.8以后的元空间内),但_java_mirror是在堆内存中image.png
链接
验证:验证类是否符合JVM规范,安全性检查。
准备:为static变量分配空间,设置默认值。
static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
如果static变量是finall的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
如果static变量final的,但属于引用类型,那么赋值也会在初始化阶段完成
解析:将常量池中的符号引用解析为直接引用。
初始化
v方法
发生的时机
概括的说,类初始化是懒惰的
main方法所在的类,总会被首先初始化
首次访问这个类的静态变量或静态方法时
子类初始化,如果父类还没被初始化,会引发
子类访问父类的静态变量,只会出发父类的初始化
Class.forName
new会导致初始化
不会导致初始化的情况
访问类的static final 静态常量(基本类型和字符串)不会触发初始化
类对象.calss不会触发初始化
创建该类的数组不会触发初始化
类加载器的loadClass方法
Class.forName的参数2为false时

类加载器

image.png
双亲委派机制
所谓的双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则
这里的双亲,翻译为上级似乎更合适,因为他们并没有继承关系
1、类加载器收到类加载的请求
2、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3、启动类加载器检查是否能加载当前这个类,能加载就结束,使用当前的加载器,否则抛出异常,通知子加载器进行加载
4、重复步骤3
image.png
线程上下文类加载器
image.png
image.png
image.png
image.png
image.png
自定义类加载器
image.png

运行期优化

分层编译
JVM将执行状态分成了5个层次:
0层,解释执行(Interpreter)
1层,使用C1即时编译器执行(不带profiling)
2层,使用C1即时编译器执行(带基本的profiling)
3层,使用C1即时编译器执行(带完全的profiling)
4层,使用C2即时编译器执行
profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回编次数】等。
即时编译器(JIT)与解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
解释器是将字节码解释为针对所有平台都通用的机器码
JIT会根据平台类型,生成平台特定的机器码
方法内联
image.png
反射优化

JMM (Java Memory Model)

Java内存模型

简单地说,JMM定义了一套在多线程读写共享数据时(成员变量,数组),对数据的可见性、有序性 和原子性的规则和保证。

原子性(synchronized)

语法:
image.png
用synchronize解决并发问题:
image.png
image.png
如何理解呢:把obj想象成一个房间,t1,t2想象成两个人。
当线程t1执行到synchronized(obj)时就好比t1进入了这个房间,并反手锁住了门,在门内执行count++代码。
这时候如果t2也运行到了synchronized(obj)时,他发现门被锁住了,只能在门外等待。
当t1执行完synchronized{}块内的代码,这时候才会解开 门上的锁,从obj房间内出来。t2线程这时才可以进入obj房间,反锁住门,执行他的count—代码。
注意:上例中t1和t2线程必须用synchronized锁住同一obj对象,如果t1锁住的是m1对象,t2锁住的是m2对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。

可见性

image.png
volatile(易遍关键字)
它可以修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取他的值,线程操作volatile变量都是直接操作主存。
可见性保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程课件,不能保证原子性,仅用在一个写线程多个读线程的情况。

有序性

同一个线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序。
这种特性称为指令重排,多线程下指令重排会影响正确性,例如著名的double-checked locking模式实现单例模式。
使用volatile修饰可以禁用指令重排,但是要注意在JDK5以上的版本使用volatile才会有效。
happens-before
规定了哪些写操作对其他线程的读操作可见,他是可见性和有序性的一套规则总结。
image.png
image.png
image.png
image.png
image.png
image.png
image.png

CAS与原子类

CAS即Compare and Swep,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行+1操作:image.png
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CUP的场景下。
因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
乐观锁和悲观锁
CAS就是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronize是基于悲观锁的思想:最悲观的估计,得放着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

synchronize优化

java Hotspot虚拟机中,每个对象都有对象头(包括class指针和Mark word)。Mark word平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就很据情况被替换为标记位、、线程锁记录指针、重量级锁指针、线程ID等内容。
轻量级锁(无竞争)
每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark word。
锁膨胀
如果在尝试加轻量级锁的过程中,CAS无法操作成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
重量锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程可以避免阻塞。
在Java6之后的自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次;反之,就自旋少甚至不自旋。总之,比较智能。
自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)。
Java7之后不能控制是否开启自旋功能。
偏向锁
image.png
其他优化
减少上锁时间
同步代码块中尽量短
减少锁的粒度
将一个锁拆分为多个锁提高并发度
锁粗化
多次循环进入同步块不如同步块内多次循环
image.png
锁消除
image.png
读写分离
image.png