JVM
2020-09-03 13:50:13 0 举报
AI智能生成
JVM知识点整理
作者其他创作
大纲/内容
JVM
内存与JVM虚拟机
1.JVM整体架构
示意图
1.类加载器(classLoader)
2.运行时数据区(runtime data area)
线程共享的
堆(heap)
Java对象存储的地方
介绍
Java堆是所有线程共享的区域
在虚拟机启动时创建
Java堆是虚拟机管理的内存中最大的一块
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常,即内存泄漏 / 内存溢出
由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代
分代原因
新生代(Young)
使用复制-清除算法
原因是新生代每次GC都要回收大部分对象
新生代里面分成一份较大的Eden和两份较小的Survivor空间,默认按*8:1:1比例分配
每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor空间,清空Eden和之前使用的Survivor空间
内存不足时发生Minor GC
一块Eden(伊甸区)
Survivor1(幸存者区,From)
Survivor2(幸存者区,To)
老年代(Old)
采用标记-整理算法
原因是老年代每次GC只回收少部分对象
方法区(Method Area)-(也叫非堆)
该区域有时(HotSpot虚拟机上)被称为持久代/永久代(PermGen)
不需要连续内存,可以动态扩展
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
存放已经被虚拟机加载的类信息,如常量、静态变量,即时编译后的代码等数据
运行时常量池是方法区的一部分
永久代
JDK1.8中移除,使用MetaSpace代替
元空间(MetaSpace)
用来存储类的元数据,也就是方法区
元空间的本质和永久代类似,都是对JVM规范中方法区的实现
MetaSpace存放在本地内存中
原因是永久代经常内存不够用,或者发生内存泄露
元空间与永久代最大的区别
元空间并不在虚拟机中,而是使用本地内存
线程私有
栈区
程序计数器(Program Counter Register)
指向当前线程正在执行的字节码文件,可看做字节码文件的行号指示器
虚拟机栈(JVM Stack)
虚拟机栈描述的是:Java执行方法的内存模型
栈帧
栈帧存储方法的相关信息
局部变量表
font color=\"#ffffff\
操作数栈
操作变量的内存模型
操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的 max stacks项中)
动态连接
方法出口(返回值)
异常情况(2种)
如果线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
调用本地Native的内存模型
本地方法栈(Native Method Stack)
直接内存
3.执行引擎(execution engine)
2.类加载器
1.类生命周期
1.加载
把class字节码文件从各个来源通过类加载器装载入内存中
2.链接
1.验证
保证加载进来的字节流符合虚拟机规范,不会造成安全错误
2.准备
为类变量(注意,不是实例变量)分配内存,并且赋予初值 (注:不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。)
3.解析
将常量池内的符号引用替换为直接引用的过程。
符号引用
即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息
直接引用
可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针
3.初始化
主要是对类变量初始化,是执行类构造器的过程
4.使用
5.卸载
2.加载机制
1.类加载器
启动类加载器(BootStrap Classloader)
1.使用C++编写,是虚拟机的一部分
2.该类没有继承java.lang.classLoader,不能被java程序调用
3.作用:用来复制加载\\lib\\目录下的类到虚拟机内存中,即用来加载java的核心类到内存
扩展类加载器(Extension ClassLoader)
1.开发者可直接使用该类加载器
2.作用:用来加载java的扩展库,即加载\\lib\\ext\\目录下的类库到内存
应用程序类加载器(App ClassLoader)
1.一般情况下,是系统默认的类加载器,即开发者编写的类都是由该类加载器加载
2.该类加载器中的getSystemClassLoader方法的返回值就是该类加载器,也叫系统加载器
3.作用:负责加载用户类路径下的类库
自定义类加载器(Custom ClassLoader)
2.JVM类加载机制
全盘负责
父类委托
缓存机制
3.类加载
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
2.双亲委派模型
1.原理:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载
如Object类,无论哪个加载器都要加载这个类,因此由父类加载器加载,即所有加载器加载Object类的请求都会汇聚到同一个层级,就不会每个都去加载导致出现多个Object类
3.类的实例化顺序
3.内存分配机制
1.所有线程共享的
方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码
类型的完整有效名
类的直接父类完整有效名
类型的修饰符
类型的常量池
域信息
方法信息
除了常量外的所有静态变量
堆
对于大多数应用来说java堆是java虚拟机所管理的内存中最大的一块
java堆是被所有线程共享的一块内存区域,虚拟机启动时创建
此内存区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
OutofMemory Error异常,如果在堆中没有内存完成实例分配并且堆无法再扩展时
2.线程隔离的数据区
当jvm每遇到一个线程时就会为他创建程序计数器、虚拟机栈、本地方法栈,当线程终止时内存会释放
程序计数器
程序计数器可以被看作是当前线程所执行的字节码的位置指示器
在虛拟机模型里字节码指示器是通过改变程序计数器的值来指定下一条要实现的指令
java虚拟机是通过轮流切换并分配处理器来实现的,所以每条线程都需要一个记录下一条指令的计数器,所以程序计数器是线程私有的
虚拟机栈
代码中每个方法在执行的同时,都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息,每个方法调用直至执行完成的过程,都对应一个栈帧在虚拟机中入栈到出栈的过程
局部变量表存放了编译期间可知的各种基本数据类型
JMM这个区域规定了两种异常情况,如果线程请求的栈深大于虚拟机所允许的深度将抛出 dStatackOverFlowError
如果虚拟机可以动态扩展如果扩展时无法申请倒足够的内存会抛出 OutofMemory Error
本地方法栈
4.内存垃圾回收(GC)
当栈运行完成时,jvm会直接销毁栈的信息,也就是说引用已经回收了,堆中的对象还存在,这就需要垃圾回收机制来执行回收对象
1.内存分配与回收策略
内存分配策略
Eden区
新生小对象进入Eden区
Survivor(幸存者) From区 (S0区)
Survivor(幸存者) To区(S1区)
老年代
大对象直接进入老年代
长期存活的对象进入老年代
动态对象年龄判断
如果在 Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
各区域回收规则(GC)
新生代(Minor-GC),默认占比8:1:1
Eden(伊甸区)
满了才GC
复制到其中一个Survivor中
Survivor(幸存者区)
满了才GC(GC 对象是 Eden+活动的Survivor)
将满足晋升阈值的对象复制到老年代
不满足晋升的复制到另一个Survivor
fullgc
2.对什么区域进行回收
堆中主要是对象存放的区域
当没有引用指向该对象时,就会执行回收(对象存活判断)
引用计数
给对象添加一个引用计数器,每当对这个对象进行一次引用计数器就加1
每当引用失效的时候引用计数器就减1
当引用计数器等于0的时候表示这个对象不会再被引用
该方案简单,但无法解决对象相互循环引用的问题
可达性分析
引用计数器来判断对象是否已“死”,而可达性分析是判断对象是否还活着
通过一系列 GCroots对象作为起点进行搜索,如果 GCroots和一个对象之间没有可达路径,则该对象是不可达的,并标记一次,当有两次标记时该对象会被判定为可回收对象从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象
不可达对象不一定会成为可回收对象,进入DEAD状态的线程还可以恢复,GC不会回收它的内存
把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的
在java语言中可作为 GC roots的对象有以下四种
1.虚拟机栈
栈帧中本地变量表中引用的对象
2.方法区静态属性引用对象
3.方法区常量引用的对象
4.本地方法栈Native方法引用的对象
最终判定
当第一次判定可达性分析算法为不可达时,会进行第一次标记并进行筛选,筛选对象是否有必要执行 finalize方法,当对象没有覆盖该方法或者已经执行过该方法时,虚拟机将这两种情况都视为“没有必要执行”,该对象不可被救活,并进行回收
第二次标记,如果这个对象被判定为有必要执行finalize方法时,那么这个对象会被放在一个队列中并在稍后由jvm虚拟机建立的 finalize线程中执行
Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F- Queue中的对象进行第二次小规模标记,如果对象要在 finalize() 中成功拯救自己--只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合
finalize()
GC 垃圾回收要回收一个对象的时候,调用该对象的finaize()方法,在下一次垃圾回收的时候,采取回收这个对象的内存
可在该方法内,指定一些对象在释放前的操作
引用
方法区中的垃圾回收
常量池中一些常量、符号引用没有被引用,则会被清理出常量池
被判定无用的类,会被清理出方法区
该类的所有实例被回收
加载该类的ClassLoader被回收
该类的Class对象没有被引用
栈
栈是线程私有的,栈只会在运行结束时销毁
3.垃圾收集算法
1.引用计数算法
缺点:无法处理循环引用的问题
2.根搜索算法(常用)
设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的
GC roots
虚拟机栈中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI的引用的对象
4.垃圾回收算法(理论)
1.标记-清除算法(Mark-Sweep)
1.该算法分为标记和清除阶段,首先标记出所有需要回收的对象,然后一起清除被标记对象的空间
2.缺点空间连续性低,如果要分配大容量对象会出现内存空间不够用的情况,提前发出垃圾收集
3.效率不高;清除和标记效率都低(在进行GC的时候,需要停止应用程序)
优缺点:实现简单,容易产生内存碎片
2.复制算法(Copying)
将可用内存分为大小相等的两块,每次只使用其中的一块,当进行垃圾回收的时候,把使用的块中存活着的对象复制到另外一块中,然后将已使用了的块内存空间全部清除
优缺点:不容易产生内存碎片,可用内存空间少,内存缩小了一半内存利用率太低,当存活对象多的情况下,效率低
3.标记-整理算法(Mark-Compact)
标记整理算法过程与标记清除算法一样
但后续步骤并不是直接对可回收对象进行淸理,而是让所有存活对象都向一端移动,然后清理掉端边界以外的内存
优缺点:不容易产生内存碎片,内存利用效率高,存活对象多并且分散的情况下,移动次数多,效率低
4.分代收集算法
目前大部分JVM的垃圾收集器采用该算法
java sun hotspot虚拟机将内存分为新生代(堆)、老年代(堆)、永久代(方法区、常量池、即时编译代码)几个区域, 新生代主要使用基于复制算法的垃圾回收,老年代和永久代主要使用标记-整理算法进行垃圾回收。具体每个区域使用哪种垃圾回收算法还要视收集器的实现制约。
一般ava堆分为新生代和老年代这样就可以根据各个年代的特点采用最适当的收集算法
新生代:大多数对象都是‘朝生夕死’,只有少量存活,所以选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
老年代:因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记清理或标记整理算法来进行回收
5.收集器分类(回收实现)
吞吐量
即CPU用于运行用户代码的时间与CPU总消耗时间的比值
吞吐量 = 运行用户代码的时间/(运行用户代码的时间 + 垃圾收集的时间)
例如:虚拟机共运行100分钟,垃圾收集花掉1分钟,则吞吐量为99%
1.串行收集器(serial collector)
概念:它只有一条GC线程,且它在运行的时候需要暂停用户程序(stop the world即STW)
serial(用于新生代,采用复制算法)
使用一个CPU或一条收集线程去完成垃圾收集工作
暂停其他所有的工作线程(STW stop the world)
JDK1.3之前是HotSpot新生代收集的唯一选择
serial old(用于老年代,采用标记/整理算法)
它同样是一个单线程收集器
这个收集器的主要意义在于给 client模式下的虚拟机使用
Server模式,则有两种用途
1.在JDK1.5之前,与Parallel Scavenge收集器搭配使用
2.作为CMS收集器的后备预案
2.并行收集器(parallel collector)
概念:它有多条GC线程,且它也需要暂停用户程序(stop the world)
指多条垃圾收集线程并行工作,此时用户线程处于等待状态
ParNew(用于新生代,采用复制算法)
使用多条线程收集。其余的和Serial—样
Serve模式下的虚拟机首选新生代收集器
Parallel Scavenge(用于新生代,采用复制算法)
它和 parNew的区别是用户可以控制GC时用户线程停顿时间
停顿时间越短就越适合需用与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率的利用cpu时间尽快的完成程序运箅
适用场景:后台计算不需要太多交互
Parallel old(用于老年代,采用标记/整理算法)
ParalleleOld是 ParallelScavenge收集器的老年代版本
这个收集器从jdk1.6开始提供
3.并发收集器(concurrent collector)
概念:它有一条或多条GC线程,且它需要在部分阶段暂停用户程序(stop the world),部分阶段与用户程序并发执行。
指用户线程与垃圾收集线程同时工作(不一定是并行的,可能会交替工作),用户程序在继续工作,垃圾收集线程运行在另一个CPU上
concurrent mark sweep[CMS](用于老年代,采用标记/清除算法)
适用场景:互联网站或Web服务器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器
CMS收集器是基于标记-清除算法实现的整个过程分为4个步骤
初始标记,并发标记,重新标记,并发清除
过程
初始标记(CMS initial mark)
初始标记会进行STW暂停时间
这个过程从垃圾回收的“根对象”开始,只扫描到能够和“根对象”直接关联的对象,并作标记
这个过程只标记了一次所以执行速度快
并发标记 (CMS concurrent mark)
在初始标记的基础上继续向下溯源标记
此阶段是并发阶段,应用线程也在并发执行,用户不会感受到停顿
并发预清理
并发处理
这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象
减少下一个阶段线程停止的时间
重新标记
重新标记会二次进行STW
收集器线程扫描在CMS堆中剩余的对象,扫描从根对象向下溯源并处理对象关联
并发消理
清理对象
并发执行
并发重置
这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收
特点
与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间。
缺点
用的是清理,不会整理压缩堆空间,因此CMS不在采用指针,而是把未分配的空间汇成列表,当JVM分配对象空间时,会搜索列表找到足够大的空间Hold住这个对象
需要更多cpu回收线程,默认为(CPU数量+3)4
需要更大的堆空间,因为CMS标记阶段应用程序的线程还在执行,那么为了保证CMS回收完堆之前还有空间分配给正在运行的应用程序需要—部分预留空间
因此CMS不会在老年代满的时候才开始收集,而是在默认为老年代使用68%时就开始行动了
对CPU资源非常敏感
无法处理浮动垃圾
产生大量内存碎片
G1
整堆收集器
G1算法将堆划分为若干个大小相等的独立区域,但它仍然属于分代收集器
新生代的垃圾收集依然采用暂停所有线程的方式,将存活对象拷贝到老年代或者Survivor空间
老年代也分成很多区域,G1收集器通过从一个区域复制到另外一个区域,完成了清理工作,这就意味着完成了堆的压缩,也就不会有CMS内存碎片的问题存在了
G1提供了两种模式
YoungGC
扫描根GCRoots
更新 RememberSet,记录回收对象的数据结构
检测 RememberSet,哪些数据要从新生到老年
拷贝对象,要么往幸存区,要么往老年代
清理工作
MixedGC
初始标记:主要利用了常规的年轻代垃圾回收暂停
根区域扫描:在初始标记的年轻代存活区扫描对老年代的引用,并标记被引用的对象,只有完成该阶段后,才能开始下一次STW年轻代垃圾回收。
font color=\"#5c5c5c\
并行与并发
分代收集(收集范围包括新生代和老年代)
结合多种垃圾收集算法(空间整合,不产生碎片)
可预测的停顿(低停顿的同时实现高吞吐量)
4.实验收集器
Z收集器
JDK11实验收集器
Shenandoah 收集器
JDK12 实验收集器
常用收集器组合
JDK默认收集器
查看默认垃圾收集器
java -XX +PrintCommandLineFlags -version
JDK1.7-1.8
-XX:+UseParallelGC
Parallel Scavenge+ Parallel old
JDK11
默认
-XX: +Use G1 GC
启用Z GC
6.GC
1.Minor GC
针对新生代
从新生代空间(包括Eden 和Survivor 区域)回收内存被称为Minor GC
复制和标记-清除垃圾收集算法
Serial收集器
ParNew收集器
ParallelScavenge
当JVM无法为一个新的对象分配内存的时候,越容易触发该垃圾回收Minor GC,所以分配率越高,内存越来越少,则越频繁执行Minor GC
2.Major GC
针对清理老年代
MajorGC的时候会同时执行 MinorGC,但当新生代收集器是 ParallelScavenge,不会执行 MinorGC
SerialOld收集器
ParalleleOld收集器
coucurrentMarkSweep收集器(CMS)
3.Full GC
清理整个堆空间-包括年轻代和老年代
=MinorGC + MajorGC
G1收集器
当老年代内存不足时,触发FUllGC
发现虚拟机频繁FullGC怎么办?
首先用命令查看触发GC的原因是什么
jstat -gccause 进程id
如果是System.gc(),则查看代码哪里调用了这个方法
如果是heap inspection(内存检查),则可能是哪里执行了 jmap -histo[:live] 命令
如果是GC locker,可能是程序依赖的JNI库的原因
6.JVM调优
-Xms
启动时占用内存大小
-XX:SurvivorRatio
控制Eden区大小
Eden:S0:S1=SurvivorRatio:1:1
100m * (SurvivorRatio/(SurvivorRatio+1+1))
尽量把GC控制在MinorGC
-Xmn 100m
Xmn区大小为100m
Xmn为Eden + Survivor0(From区) +Survivor1(To区)
Xmn内存分配为:总内存 * (Survivor/Survivor +S0(默认为1) +S1(默认为1))
-XX PretenureSizeThreshold=n
可以令大于这个设置值的对象直接在老年代分配
只用用于ParNewGC和Serial这两种收集器
MaxTenuringThreshold
一个对象经历多少次GC进入老年代
默认为5
对象年龄的动态判断
如果 Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于可以直接进入老年代,无需等到 MaxTenuring Threshold要求年龄
空间分配担保
检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
如果大于,将尝试进行一次 MinorGC
如果小于,或者设置不允许冒险,那这时要改为进行一次FullGC
流程图
1.在MinorGC之前检查老年代最大可用连续空间是否大于新生代所有对象的大小
2.大于执行MinorGC
3.空间不够,检查HandlePromotion是否开启
4.没有开启,执行FullGC
5.开启了,检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小
6.大于,试着执行MinorGC
7.小于执行FullGC
8.保证尽量减少FullGC频率
-Xss
指设走每个线程的堆栈大小
需要根据系统查看每个线程大约需要多少内存,可能会有多少个线程同时运行
-Xmx
整个设定程序运行期间最大可占用内存大小
如果程序运行需要占用更多的内存,超出设定值,就会抛出 OutofMemory异常
垃圾回收机制不会主动进行回收,只有当内存不足时才会进行,因为回收内存也会消耗内存所以只在必要时候进行
1.引用状态
强引用
代码中普遍存在的类似“Object obj=new Object()”,即强引用就是我们常用的new
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虛拟机宁愿抛出 OutofMemory Error错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
弱引用
描述 非必需对象。被弱引用关联的对象 只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉 只被弱引用 关联的对象。 Java 中的类 WeakReferance 表示弱引用
软引用
描述 有些还有用但并非必须的对象。再系统将要发生溢出异常之前,将会把这些对象列进回收范围 进行二次回收。如果这次回收 还没有足够的内存,才会抛出内存溢出异常。Java 中的类SoftReference 表示软引用。
虚引用
这个引用存在的唯一目的就是 在这个对象被收集器回收时 收到一个系统通知,被虚引用 关联的对象,和生存时间完全没关系。 Java中的类 PhantomReference表述虚引用
2.JAVA内存模型(JMM,JAVA Memory Model)
主要目的是定义程序中各个变量(共享变量)的访问规则
线程通信机制
内存共享
Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通信。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现
消息传递
主内存与工作内存
1.JAVA内模型规定所有的变量都存储在主内存中,每个线程都有自己的工作内存,线程的工作内存中保存的是当前线程使用到的变量值的副本(主内存拷贝过来的)。2.线程对变量的所有操作都必须在工作内存中进行,不能直接与主内存进行读写交互,线程间相互的传值需要通过主内存完成。
主内存主要对Java堆中实例数据部分,工作内存对应于虚拟机栈中部分区域
内存间的交互
lock(锁定):作用于主内存的变量。把一个变量标识为一条线程独占的状态
unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来
read(读取):作用于主内存的变量。把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的值放入工作内存的变量副本中
use(使用):作用与工作内存的变量,它把工作内存中一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时,就会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎收到的赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 wirte操作使用。
wIrte(写入):作用于主内存的变量,它把 store操作从工作内存中得到的变量值放入主内存中
内存模型规则
重排序
volatile重排序规则
1.当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序
2.当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。
3.当第一个操作是volatile写,第二个是volatile读时,不能重排序
final重排序规则
JMM禁止将final域内的写重排序到构造函数之外
为了程序的性能,处理器、编译器都会对程序进行重排序处理
条件
在单线程环境下不能改变程序运行的结果
存在数据依赖关系的不允许重排序
问题
重排序在多线程环境下可能会导致数据不安全
顺序一致性
多线程环境下的理论参考型
为程序提供了极强的内存可见性保证
特性(原子、有序、可见)
一个线程中的所有操作必须按照程序的顺序来执行
每个操作都必须原子执行且立刻对所有线程可见
Happens-before原则(先行发生原则)
理论
8大规则
程序次序规则
锁定规则
一个 unLock(解锁)操作先行发生于后面对同一个锁的Lock(加锁)操作;
volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则
线程启动规则
Thread对象的 start0方法先行发生于此线程的每一个动作
线程中断规则
对线程 Interrupt0方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则
线程中所有的操作都先行发生于线程的终止检测
对象终结规则
一个对象的初始化完成先行发生于他的 finalize0方法的开始
as-if-serial
所有的操作均可以为了优化而被重排序,但是必须要保证重排序后执行的结果不能被改变
Java线程之间通信由内存模型JMM控制
JMM决定一个线程对变量的写入何时对另一个线程可见
线程之间共享变量存储在主内存中
每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本
JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证
可见性、有序性
当一个共享变量在多个本地内存有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性
保证线程的有序执行,此为有序性,保证了线程安全
4.JVM参数
常用设置
1.-Xmx =256m(设置堆最大值)
2.-Xms=128m(设置堆的最小值,初始化堆大小)
即启动时占用内存大小
3.-XX:NewSize=1024m(设置年轻代 初始值,最小值)
4.-XX:MaxNewSize=1024(设置年轻代最大值)
5. 没有设置老年代空间的参数,但老年代空间大小=堆空间大小-年轻代空间大小
6.-XX:PermSize=256m 设置永久代初始值,最小值
7.-XX:MaxpermSize=256m 设置永久代最大值
8.-xss128k(设置线程栈大小)
9.-Xmn2g(设置年轻代大小)
10.-XX:NewRatio=4(设置年轻代和年老代的比值)
11.-XX:SurvivorRatio=4设置Suvivor区和Eden区的比值
12.-XX:MaxTenuringThreshold=7 (表示一个对象从年轻代移入年老代的年龄)
7.栈区
栈是先进后出的最先进来的是man函数
虚拟机只会对java栈执行两种操作出栈和入栈先入栈的最后出栈
9.SafePoint
垃圾回收器都有个阶段需要暂停所有线程对内存对象引用关系网络进行更新,这个机制称为 safe Point
抢先式中断
STW = stop the world
主动中断
没有执行到安全点的用户线程,继续跑直到safePoint
缺点:Thread.sleep wait
当GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标志为真时就自己中断挂起
安全区域
安全点的衍生概念
安全点以后的一段区域都是安全的
10.内存泄露及监控
程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。 大量的内存泄露会导致内存溢出
11.JVM调优
1.大对象fllgc导致的stop the world 时间过长
1.降低进入老年代的对象数
1.增大年轻代空间
2.增加对象晋升的年龄阈值
2.缩短fullgc时间
1.减少 老年代空间
2.oom
调大permsize
1合理的编写程序
2充分并合理的使用硬件资源
3.合理的进行JVM调优
非堆内存的释放
堆外内存
文件句柄
文件IO如果太大,导致JVM宕机
限制文件大小
异步方式
限制一台服务器的吞吐量
数据连接
堆内存
大对象
避免使用大对象
尽量减少大对象的生存时间
提高大对象进入老年代的门槛
降低FullGC频次
重启服务器,一个定时任务触发FullGC
SLA服务等级协议
尽量使用32位版本
32位效率高于64位
一线互联网做法
虚拟机或者docker拆分内存
使用CMS垃圾回收器
GC短暂停:适合对于延时要求较高的网络请求
缺点:用的是标记-清除算法,内存碎片多
12.虚拟机执行策略
判断是否是热点代码,如果是热点代码就编译执行,不是就解释执行
基于采样的热点判定
虛拟机主动周期性检查各个线程栈顶,若某个方法经常出现在栈顶
优点:实现简单
缺点:很难精确一个方法的热度
基于计数器的热点判定
每个方法都有一个计数器,超过一定次数就是热点方法
方法调用计数器:在 client模式下的阈值是1500次,server是10000次
回边计数器:主要统计方法中循环体代码执行次数
编译执行
JIT编译器:及时编译器
Hotspot:热京技术,常用方法直接编译成机码,省去每次编译的时间
完全编译成机器码
完全编译的话时间久,1G项目两三个小时编译时间
解释执行
解析器
翻译一行,执行一行,启动快,整体效率低
方法调用
方法调用不等同于方法执行
该阶段唯一能确定的是任务,就是确定调用哪—个方法
非虚方法
类加载是时候就会把符号引用解析为该方法的直接引用,在解析 class阶段就可以确定唯一调用的版本
静态方法
JVM虚拟机中为invokestatic
私有方法
实例构造器
JVM中为invokespecial
父类方法
虚方法
除去fnal和非虚方法其他方法称为虚方法
虚函数调用JVM中为 invokevirtual
虚拟机动态分派机制
虚拟方法表(vtabel)
使用虚方法表索引来代替元数据查找以提高性能
如果是class文件需要一层一层在常量表找下去,影响性能,所以直接把虚方法放在一张表里,然后去虚方法表检索
itable是接囗方法表,也是类似的
7.JVM常见面试题
OutOfMemoryError异常
内存泄露和内存溢出?
区别
0 条评论
下一页