jvm是什么?
Java Virtual Machine —-java程序的运行环境。
JVM是虚拟机的英文简称。它是java运行环境的一部分,是一个虚构出来的计算机,它是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
好处:一次编写,到处运行;自动内存管理,垃圾回收功能;数组下标越界检查;多态。
jvm体系结构?
PC寄存器
程序计数器:program counter register
jvm指令 > 二进制字节码 > 解释器 > 机器码 > CPU
作用:记住下一条jvm指令的执行地址。
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的字节码方法(用来存储指向下一条指令的地址,也就是即将要执行的指令代码),由执行引擎读取下一条指令,它是一块很小的内存空间,几乎可以忽略不计。不会有内存溢出。
栈 (线程运行时需要的内存空间)
栈:数据结构
程序 = 数据结构 + 算法
关于栈:先进后出,后进先出,桶
队列:先进先出(FIFO:first input , first output )管道
为什么main方法先执行后结束?
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就over!
栈中:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧,每个方法运行时需要的内存,每个方法的调用就是一个栈帧。每个线程都只能有一个活动栈帧,对应着当前正在执行的那个方法。
栈内存溢出:StackOverflowError
· 栈帧过多导致
· 栈帧过大导致
-Xss 设置栈内存参数
线程运行诊断:
栈+堆+方法区:交互关系
本地方法栈(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
堆(也叫运行时数据区)
Heap,一个jvm只有一个堆内存,堆内存的大小是可以调节的。
通过new关键字,创建对象都会使用堆内存。
他是线程共享的,堆中的对象需要考虑线程安全的问题。
类加载器读取了类文件以后,一般会把什么东西放到堆中?类,方法,常量,变量,保存我们所有引用类型的真实对象。堆内存中从GC的角度还细分为三个区域:
新生区(伊甸园区)Young/New ( Eden Space)
养老区 Old
永久区 Perm
GC垃圾回收主要是在新生区和养老区。
假设内存满了,oom报错(OutOfMemoryError),堆内存不够。
在JDK8以后,永久存储区改了名字叫元空间。
新生区
一个类:诞生和成长的地方,甚至死亡;
伊甸园,所有的对象都是在伊甸园区new出来的!
幸存者区(0,1)
真理:经过研究,99%的对象都是临时对象!
永久区
这是常驻内存的,用来存放jdk自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟就是释放这个区的内存。
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类,不断地被加载,知道内存满,就会出现OOM。
jdk1.6之前:永久代,常量池在方法区中;
jdk1.7 :永久代,但是慢慢退化了,去永久代,常量池在堆中;
jdk1.8之后:无永久代,常量池在元空间(metaSpace) ;

元空间逻辑上存在,物理上不存在。
元空间并不在虚拟机中,而是使用本地内存。
默认情况下:虚拟机分配的总内存是 电脑内存的四分之一 ,而初始化的内存是六十四分之一;
OMM报错?(堆内存溢出)
1、尝试扩大堆内存看结果 (-Xms1024m -Xmx1024m -XX:+printGCDetails)
2、分析内存,看一下哪个地方出现了问题(内存快照分析工具Jprofiler 专业工具)
jps工具:查看当前系统中有哪些java进程
jmap工具:查看堆内存占用情况 jmap - heap 进程id
jconsole工具:图形界面的,多功能检测工具,可连续监测
方法区
Method Area 方法区 ,也被称为永久代 。
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实际变量存在堆内存中,和方法区无关。
static final Class类 常量池
运行时常量池:
常量池,就是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
StringTable 串池特性:
· 常量池中的字符串仅是符号,第一次使用时才变为对象
· 利用串池的机制,来避免重复创建字符串对象
· 字符串变量拼接的原理是StringBulider(1.8)
· 字符串常量拼接的原理是编译期优化
· 可以使用intern方法,主动将串池中还没有的字符串对象放入串池:
1.8中,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入,会把串池中的对象返回
1.6中,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份放入串池,就是两个不同的对象,会把串池中的对象返回
StringTable垃圾回收:
StringTable调优:
调整-XX:StringTableSize=桶个数
考虑字符串对象是否入池
直接内存

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

jvm在垃圾回收时,并不是对这三个区域统一回收,大部分时候回收都是新生代。
GC两种类:轻GC(普通的GC),重GC(全局GC)
题目:
1、JV内存模型和分区~详细到每个区放什么?
2、堆里面的分区有哪些?Eden,from,to,老年区,说说他们的特点?
3、GC的算法有哪些?标记清除法,标记压缩,复制算法,引用计数器,怎么用的?
4、轻GC和重GC分别发生在什么时候?
如何判断对象可以回收
引用计数法
可达性分析算法
java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
哪些对象可以作为GC Root
四种引用
垃圾回收算法
复制算法
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 设定参数)这些对象就会进入老年代。

坏处:浪费内存空间,多一半空间永远是空的。
标记清除算法:
优点:不需要额外空间。
缺点:两次扫描,严重浪费时间,会产生内存碎片。
标记压缩算法(标记整理):
再优化
总结:
内存效率:复制>标记清除>标记压缩
内存整齐度:复制算法 = 标记压缩算法 > 标记清除
内存利用率:标记压缩 = 标记清除 > 复制
分代垃圾回收
垃圾回收器
串行
· 单线程
· 堆内存较小,是个个人电脑
开启串行垃圾回收器:-XX:UseSerialGC = Serial(复制算法) +SerialOld(老年代)
吞吐量优先
· 多线程
· 堆内存较大,多核CPU
· 让单位时间内,STW的时间最短
开启吞吐量优先垃圾回收器:-XX:+UseParallelGC(新生代) ~ -XX:UseParallelOldGc(老年代)
自适应大小策略(新生代),动态调整伊甸园和幸存区比例大小:-XX:+UseAdaptiveSizePolicy
调整吞吐量目标,垃圾回收时间和总时间的占比:-XX:GCTimeTatio=radio
垃圾回收暂停时间:-XX:MaxGCPauseMillis=ms
控制线程数的参数:-XX:ParallelGCThreads=n
响应时间优先
· 多线程
· 堆内存较大,多核CPU
· 尽可能让单次STW的时间最短
开启响应时间优先垃圾回收器:-XX:+UseConMarkSweepGC(并发标记清除垃圾回收,老年代) ~
-XX:UseParNewGC(新生代) ~ SerialOld
并行垃圾回收线程数:-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads(并发垃圾回收线程数)
执行CMS垃圾回收的内存占比:-XX:CMSInitatingOccupancyFracton=percent
重新标记之前对新生代进行垃圾回收:-XX:+CMSScavengeBeforeRemark
老年代:
G1(Garbage First)
相关JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
G1垃圾回收阶段:
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
G1垃圾回收器优化:
JDK8u20字符串去重
JDK8u40并发标记 类卸载
JDK8u60回收巨型对象
JDK9并发标记起始时间的调整
JDK9有更高效的回收
垃圾回收调优

最快的GC是不发生GC
新生代调优
新生代的特点
所有的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
类加载与字节码技术
类文件结构
字节码指令


javap工具
自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译class文件。
图解方法执行流程
原始java代码
编译后的字节码文件



类文件常量池载入运行时常量池
方法字节码载入方法区
mian线程开始运行,分配栈帧内存
执行引擎开始执行字节码














编译期处理(语法糖)
语法糖就是指java编译器把.java源代码编译为.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利。
默认构造器:
自动拆装箱:
泛型集合取值:
可变参数:
foreach循环:

Switch字符串:

switch枚举:


枚举类:

try with resource:


方法重写时的桥接方法:

匿名内部类:

类加载
加载
将类的字节码载入方法区中,内部采用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是在堆内存中
链接
验证:验证类是否符合JVM规范,安全性检查。
准备:为static变量分配空间,设置默认值。
static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
如果static变量是finall的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
如果static变量final的,但属于引用类型,那么赋值也会在初始化阶段完成
解析:将常量池中的符号引用解析为直接引用。
初始化
发生的时机
概括的说,类初始化是懒惰的
main方法所在的类,总会被首先初始化
首次访问这个类的静态变量或静态方法时
子类初始化,如果父类还没被初始化,会引发
子类访问父类的静态变量,只会出发父类的初始化
Class.forName
new会导致初始化
不会导致初始化的情况
访问类的static final 静态常量(基本类型和字符串)不会触发初始化
类对象.calss不会触发初始化
创建该类的数组不会触发初始化
类加载器的loadClass方法
Class.forName的参数2为false时
类加载器

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




自定义类加载器
运行期优化
分层编译
JVM将执行状态分成了5个层次:
0层,解释执行(Interpreter)
1层,使用C1即时编译器执行(不带profiling)
2层,使用C1即时编译器执行(带基本的profiling)
3层,使用C1即时编译器执行(带完全的profiling)
4层,使用C2即时编译器执行
profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回编次数】等。
即时编译器(JIT)与解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
解释器是将字节码解释为针对所有平台都通用的机器码
JIT会根据平台类型,生成平台特定的机器码
方法内联
反射优化
JMM (Java Memory Model)
Java内存模型
简单地说,JMM定义了一套在多线程读写共享数据时(成员变量,数组),对数据的可见性、有序性 和原子性的规则和保证。
原子性(synchronized)
语法:
用synchronize解决并发问题:

如何理解呢:把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对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。
可见性

volatile(易遍关键字)
它可以修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取他的值,线程操作volatile变量都是直接操作主存。
可见性保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程课件,不能保证原子性,仅用在一个写线程多个读线程的情况。
有序性
同一个线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序。
这种特性称为指令重排,多线程下指令重排会影响正确性,例如著名的double-checked locking模式实现单例模式。
使用volatile修饰可以禁用指令重排,但是要注意在JDK5以上的版本使用volatile才会有效。
happens-before
规定了哪些写操作对其他线程的读操作可见,他是可见性和有序性的一套规则总结。





CAS与原子类
CAS即Compare and Swep,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行+1操作:
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CUP的场景下。
因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
乐观锁和悲观锁
CAS就是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronize是基于悲观锁的思想:最悲观的估计,得放着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
synchronize优化
java Hotspot虚拟机中,每个对象都有对象头(包括class指针和Mark word)。Mark word平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就很据情况被替换为标记位、、线程锁记录指针、重量级锁指针、线程ID等内容。
轻量级锁(无竞争)
每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark word。
锁膨胀
如果在尝试加轻量级锁的过程中,CAS无法操作成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
重量锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程可以避免阻塞。
在Java6之后的自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次;反之,就自旋少甚至不自旋。总之,比较智能。
自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)。
Java7之后不能控制是否开启自旋功能。
偏向锁
其他优化
减少上锁时间
同步代码块中尽量短
减少锁的粒度
将一个锁拆分为多个锁提高并发度
锁粗化
多次循环进入同步块不如同步块内多次循环
锁消除
读写分离




