【深入解析JVM】--最新版
JVM
jdk、jre、jvm的关系
JDK:是Java开发工具包,是Sun Microsystems针对Java开发员的产品。
JDK中包含JRE,在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
JRE:是java程序的运行环境,它包含JVM。
三者的关系:JDK(JRE(JVM))
什么是JVM
可以简单的理解为:就是运行编译好的java文件生成 的.class文件,并且解析为当前运行系统所对应的指令。
1. Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行,
- 粗略分来,JVM的内部体系结构分为三部分,分别是:类装载器(ClassLoader)子系统,运行时数据区,和执行引擎。
JVM 的位置在哪里?
JVM架构图
一、类装载子系统
1.通过一个类的全限定明获取定义此类的二进制字节流;
2.将这个字节流所代表的的静态存储结构转化为方法区的运行时数据;
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类加载器分类
- 引导类加载器[BootStrapClassLoader]:负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
链接模块
-
验证(Verify)
-
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
-
主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。
-
-
准备(Prepare)
- 为类变量分配内存并且设置该类变量的默认初始值,即零值;
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 之类不会为实例变量分配初始化,类变量会分配在方法去中,而实例变量是会随着对象一起分配到java堆中。
-
解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程。
初始化模块
- init(构造器初始化)
默认每个类都有
-
cinit(静态变量初始化)
当在代码中有静态值,或者静态代码块之类,会自动创建cinit初始化方法
①.双亲委派机制(重要)
优点:
-
避免了重复的加载
-
保护程序的安全,防止API被串改(如手动写java.lang包String类)
程序计数器
简介
- 程序计数器也称PC寄存器 (PC Register)
- PC寄存器是用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条。
- 它是一个很小的空间,几乎可以忽略不计。也是运行速度最快的存储区域。
举例说明:
声明:简要理解也就是因为在运行时CPU需要不停的切换个线程,这时候就需要PC寄存器来记录当前线程运行到哪个位置了,即将运行哪里给记录下来,等待下次切换回后进行调用。
二、栈
详情结构图
虚拟机栈的特点
- 内存小,跨平台性,可以少量存储一些变量,内存地址,用来管理Java方法的调用。
- 栈也就是方法,一个线程就是一个栈,所以说是线程安全的,是线程私有的—>声明周期与线程同步
- 先进后出(执行完就出)栈帧相当于执行方法,调用完就出
- 因为栈比较小,栈不存在GC问题(OOM)
声明:
一个线程就是一个栈 栈中包含栈帧: 栈帧也就是方法---栈帧中包 含变量表-->变量表包含 变量槽Slot(也就是索引)--->double long因为是8个字节64位占用2个Slot 32位一个,按照加载变量的顺序来分配,索引槽,this(静态是没有this变量槽)默认第一个0,只要不是静态默认都有this 操作数栈:在变量进入变量表时都会经过操作数栈 动态链接:引用调用常量池中的数据 方法的调用:静态链接:早期绑定---在编译时就确定了调用的方法,如static final private 修饰的方法 动态链接:晚期绑定:在运行时才确定了方法 比如接口
栈异常
StackOverflowError:当栈分配的空间不足时,那么会出现—>通常是递归时
设置栈的大小
-Xss:如-Xss128k
本地方法栈(了解)
- 本地方法栈也是私有的。
- 本地方法栈用于管理本地方法的调用。
- 允许被实现成固定或者是可动态扩展的内存大小。
- 本地方法是用C语言实现的
声明:粗略的可以理解为本地方法栈就是使用java可以调用本地方法(C语言编写)简介的操作内存等。
三、堆
简介
- 堆在运行时数据区占用比较大的一部分
- 通常来说new对象都是放在堆中的。
- 堆中又分为新生代、老年代。
- 内存默认比例分配1:2
- 堆中存放字符串常量池(后有详解)
代码简单优化建议
开发中能使用局部变量的,就不要使用全局
①新生代
简介
- 新生代使用了复制算法
- 新生代为gc的重点对象,经官方测试70%对象都生命周期都会在新生代中完结
- 新生代又分为了eden、survivor1、survivor2
- 内存比例分默认为:8:1:1;j8默认开通自适配比例可能有所变化6:2:2
- 新生代收集器:Minor GC/Young GC
eden(新生区)
当初始加载对象时会进入新生区
survivor(幸存区)
-
幸存区又分为from 和 to —谁为空谁为to ,始终都会有一个区域为空。
-
幸存区不会主动进行垃圾回收,只会eden回收时才会附带进行gc
-
当在幸存区中的阈值达到了15后(默认15可修改)会自动进入老年代
当新生区(eden)出现了内存不足时,会进行YGC,那么会将没有指针的对象回收,还有指针引向的对象放入survivor1或者survivor2区域中,eden清空,数据放入一个survivor中。—当第二次进行gc那么会将eden数据放入另一个空的survivor中,并且将当前survivor中有效数据,放入空的survivor中,一次类推。
TLAB(快速分配策略)
由于堆中的空间都是共享的,所以存在线程安全的问题,这时候就出现了TLAB
缓冲区的线程私有的 TLAB ,保证了安全性,是在eden 中只占1%内存可能成功也可能失败,快速分配策略
声明
在一个对象进入内存时 会进入eden,如果满了(YGC进行回收没有引用的,如果还有引用的)会放入s1或者s0这就涉及到to from哪个为空就是to,(下次eden再次满了会将有数据的【举例s1】中的数据放入s0,并且进行迭代版本)以此类推,当某个对象迭代阈值的次数达到默认15此后,(当然也会有特殊的优化:如当survivor区域中相同年龄的内存总和大于survivor的一半内存,会将大于等于平均年龄的对象提前放入老年代)会放入老年代 关于YGC 全程(YoungGC) 也可以为(Minor GC) s1,0是不会有单独的gc回收只会被动的依赖于eden的gc当eden进行gc时会自动回收s1,s0
②老年代
特性
- 较大的对象数据会放入老年代
- 老年代的数据都是相对于持久的不会频繁的gc
- (MajorGC / Old GC) 在进行majorgc时会至少进行一次minorGc ,而且majorgc的效率是比minorGc 慢10倍的
- 老年代收集器:MajorGC / Old GC 要区分与Full GC
Full GC :是进行整堆的回收
③逃逸分析
什么是逃逸?
也就是如果在方法内创建对象,并且return进行传出,或者赋值到外部的变量,那么就进行了逃逸。
-XX:+DoEscapeAnalysis (JDK1.8之后默认开启)
-XX:+DoEscapeAnalysis(关闭)
逃逸分析包括以下
栈上分配
也就是将对象直接分配到栈上,跟随栈的消亡而消亡,减少了gc(栈中没有gc),提高了性能、速度。
同步省略
因为是每个栈独有的,一个栈也就是一个线程所以不存在同步安全的问题。
分离对象或者标量替换
扩充:一个类代表一个:聚合量,标量是无法分析的最小数据,聚合量可以分析为标量,也就是分析属性
也就是当加载一个pojo类时,不会创建对象而是,标量替换进行分析成一个个小的属性,减少了内存,提高了性能。
但是基于hotSpot 虚拟机这项技术并不成熟,因为还需要进行判断是否 属于逃逸,如果没有逃逸,可能会浪费了判断的时间等一些问题。
但是最后标量替换还是引用到了hotSpot虚拟机中
所以问题—所有的对象都是存储在堆空间中么?
回答:是的
四、方法区
简介
- 方法区在Hotspot中又称永久代、元空间(非堆)[这里的永久代、元空间在JVM虚拟机规范中是不等价的]
- 在jdk1.7(包含7)以前称为永久代,并且方法区是在JVM中
- 在jdk1.8以后称为元空间,并将方法区移除JVM的约束
- 使用垃圾收集器:FullGC
- 其中内含了常量池、域(Field)信息、已装载类信息、方法信息、JIT代码缓存
- 方法区是直接内存(也就是直接分配在内存上的)
存储迁移过程
为什么要移除堆空间?
- 顾名思义(永久代)即经常不回被回收的,跟随电脑的内存进行扩展,可以减少gc,提高性能,并减少了内存溢出的风险。
常量池
常量池包含各种字面量和对类型、域和方法的符号引用。
参数设置
初始值 -XX:MetaspaceSize=100m
最大值 -XX:MaxMetaspaceSize=100m
建议不要随意修改设置,因可以跟随本地内存变化而进行扩充变化
- 经过以上学习类装载子系统、栈、堆、方法区、可延伸面试题:
对象在JVM中是怎么存储的?
答:创建对象的步骤有六种
- 判断对象对应的类是否经过类加载子系统加载过
- 为对象分配内存
- 处理并发安全的问题(TLAB)(CAS)
- 初始化分配到空间
- 设置对象的对象头
- 执行init方法进行初始化
对象头里面有什么?
- 类型指针—>指向类源数据,确定对象所属类型
- 运行时元数据
声明:如果是数组,还需要记录数组的长度。
图解流程–以上两问
五、对象访问定位
句柄访问
通俗来说,就是在栈中的动态链表存储着变量对象在堆空间的引用地址,再开辟一个空间作为中间商来链接堆中的地址,那么栈中的引用直接记录中间商的地址就可以了,当堆空间地址变化了,也不需要进行变化处理。
直接指针(Hotspot使用)
也就指栈中的动态链表存储着变量对象在堆空间的引用地址,是直连的方式,当堆中的地址变化,那么栈中也需要进行变化。
六、执行引擎
- java代码被编译成字节码的时候可以称为前端编译器,在进行第二次编译为本地系统所能识别的指令称后端编译器
- 执行引擎也称后端编译器
执行的流程
什么是解释器?
也就是将字节码文件翻译为对应平台的机器指令)(低效)运行流程:直接 翻译,直接运行,也就是一行代码一行代码进行编译
什么是编译器?
将源代码直接编译为对应本地平台的机器指令(JIT编译器—》及时编译器)运行流程:先翻译,翻译后再执行,响应时间慢,但是响应过后非常快。解析热点代码(也就是执行频率高的)后会进行缓存起来,下次调用就直接调用的机器指令不需要再次解析,效率高
为什么java是半编译半解释语言?
因为hotspot虚拟机 既有编译器又有解释器互相结合。效率更高
图解
七、垃圾回收器
①概念。
什么是垃圾?
答:垃圾就是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
什么是引用计数算法?(java没有引用这个算法,python使用)
简单理解来说就是对没一个对象都保存一个整型的计数器,当有人引用时+1 ,不引用了-1,到0时那么就被认定可回收
优点:简单,高效
主要缺点:无法处理循环引用(存在内存泄漏)
吞吐量—延迟性
“程序运行的时间占用总时长的比例” 也就是程序时间/(程序时间+垃圾回收时间) 占用率越高越好 ,一般高吞吐量的那么都是不需要太大交互的(例如)
延迟性也就是回收的时候SWT停顿的时间
两个之间是相对比的,不能同时兼顾极致。
STW
Stop The word :简称STW 指的是在GC垃圾回收事件时,会产生成语的停顿(会让用户线程进行停顿,进行GC快照)
System.gc()
调用gc(使用的是FullGC) 但是不一定能够及时调到 底层调用的是(Runtime.getRuntime.gc());
一般不会调用,只有在调优测试可能会调用
②GC Roots
可达性分析(或 跟搜索算法、追踪性垃圾收集)java使用
关联GC Roots的就是可达性的,对象不应该被回收,不关联的可能会进行回收,看“图文”是否关联
图文理解
哪些对象是关联GC Roots的?
-
虚拟机栈中的引用对象
如:各个线程被调用的方法中使用到的参数、局部变量等
-
本地方法栈内JNI(通常说本地方法)引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
所有被同步锁Synchronized只有的对象
-
java虚拟机内部的引用
finalization方法
- finalization方法是父类Object中的一个方法,可以被重载
- finalization方法一般不会去主动调用,因为也没有意义,默认是空的。
- finalization方法是在gc时会自动调用,
并且只会触及一次
由于方法的存在,可将虚拟机中的对象分为三种
- 可触及的:冲根节点开始,可以到达这个对象
- 可复活的:对象的的引用断开,开始回收,调用了finalization方法又重新引用了。
- 不可触及的:也就是对象的引用断开了,并且已经调用过了finalization方法了,只能被回收了
具体过程
内存泄露
什么是内存泄露?
答:java中对象已经不再使用,但是gc还没办法进行回收
举例
- 单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内.存泄漏的产生。
- 一些提空了close方法关闭资源而没有进行关闭的
数据库连接(dataSourse . getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的。
算法介绍
分带收集
也就是将内存分代进行收集
如:在java堆分成新生代(eden s1 s2[复制算法](不会产生内存的碎片化))老年代(标记-清除算法(会参数内存的碎片化,再次存储是空闲列表方式);标记-整理算法(不会产生碎片化,再次存储时是指针碰撞的方式进行存储))
增量收集算法
一次回收一点,减少延迟性,提高吞吐量
分区算法
就是将堆分为一块一块的小区间region,都相对于是独立的。好处是可以控制一次回收多少个小区间
复制算法
在新生代中使用的就是复制算法。
优点:不会产生内存的碎片化,运行速度快。
缺点:如果在不确保是大量垃圾的情况下,可能会导致时间、资源的浪费。
标记–清除算法
将需要进行回收的垃圾进行标记,当进行gc时进行清除(扩充:这里的清除其实并不是在空间中给remove而是标记到空闲列表等待下次进行存储的话直接覆盖),再次存储是空闲列表方式。
优点:相对比运行速度快
缺点:内存有碎片化,当存储大容量的对象时可能会导致OOM
标记–整理算法
将需要进行回收的垃圾进行标记,当进行gc时进行清除,清除后,会对内存进行整理不会产生碎片化,再次存储时是指针碰撞的方式进行存储。
优点:不会有碎片化
缺点:运行速度较慢
安全点
什么时候都可以GC么?
不是什么时候都可以进行直接GC的 只有在制定的安全点才可以
如何发生GC时,检查所有的线程都跑到了安全点?
抢先式中断:(目前是没有任何商用虚拟机采用)
首先中断用户线程,查看是否到了安全点,如果没有那么继续进行跑(目前是没有任何虚拟机采用)
主动式中断:(大多采用)
设置一个中断的标志,当线程都运行到了主动轮询的标志时,如果中断标志为true,那么进行挂起。
引用—偏门高频面试
引用分为:强引用、软、弱、虚引用分别是依次递减
的
引用:当内存空足够时,则保存在内存空间中,如果垃圾回收后,内存空间不够时,那么就抛弃这些对象
在java中有一个抽象类分别可以实现软、弱、虚
强引用
平常开发99%都是强引用
如:String str = new String()
软引用
使用场景:缓存居多
特点:当内存不够时,那么进行回收,内存够那么继续存在
弱引用
特点:每次gc都会进行直接回收
HotSpot落地GC
Serial—Serial old
单核gc处理,串行,针对于新生代的垃圾回收器,采用了复制算法,串行回收----同和Serial old垃圾回收器(收集老年代的垃圾回收器)采用的标记-压缩算法 优点:在单线程的情况下,简单高效,一般使用在单cpu精简的情况下 主动配置使用SerialGC:-XX:+UseSerialGC
ParNew—CMS || SerialOld
并行回收器,新生代。采用的算法同上。 老年代使用的CMS GC (J14已经移除了)或者SerialOld(J9不再关联) 采用的算法同上;最新版本已经不能使用了 主动配置:(必须要在低版本的如J14以下或者J9以下的版本)-XX:+UseParNewGC
Parallel—Parallel old
并行回收器,新生代, 效率其实和上面差不多,但是比上面的会更好一点,算法同上,吞吐量更有限可控,自适应调解策略(也就是可以调解吞吐量优先还是 暂停时间优先)---组合Parallel old jdk1.8默认使用
CMS
在老年代—是一款并发的垃圾处理器–低延迟,采用的是标记-清除算法
CMS优点:低延迟,并发
缺点:会产出内存碎片,并且在高峰内存的情况下可能会导致内存不够用那么,就直接执行FullGC回降级使用替补的GC SerialOld)(串行)
无法处理浮动的垃圾
对cpu的资源敏感
为什么说这个是低延迟的?
工作原理:用户线程—》初始标记(STW)GC—》继续用户线程并且并发标记—》重新标记(STW)因为在后面用户线程又进行了使用所有需要重新标记下垃圾—并发清理;
图解原理
G1
特点
- 现在主流G1垃圾回收
- 主要应用在服务器端,针对于大内存,多处理器
官方说明:延迟可控的情况下尽可能获得更高的吞吐量,所以才担任了“全功能收集器的”重任
采用的区域(region)分代化(分区算法)
兼具了并行,并发,分区,空间整合,可预测的停顿时间模型
Region之间是复制算法,,但整体上可以看做是标记-整理 - 缺点:内存占用比cms更高
- g1在存储大对象时会专门划分一个区域H区 区别于老年代
- 设置H区域的原因 如果创建了一个大对象并且 生命周期很短,放在老年代中,那么就会造成了内存的泄露
声明:G1垃圾回收是分成一块一块Region进行回收,每次回收并不是全部垃圾都进行回收,而是根据延迟时间,来定制收取占比率较高的region
记忆集RememberdSet
声明:也就是在因为在g1是进行分区的垃圾回收,所以可能会出现一个区域中使用的对象,在另一个区域中也有引用,那么在回收时就需要全部遍历一遍回收,显然效率太低
所以有了记忆集:也就是记录了当前年轻代区中的对象在老年代区中的引用位置(如果有那么在ygc时不会回收掉);
G1垃圾回收过程
G1垃圾回收主要包括三个环节:
年轻代 YGC
老年代并发标记过程(Concurrent Marking)
混合回收 (Mixed GC)
G1回收过程具体
经典垃圾回收器大致流程图
七种垃圾回收器总结
参数总结
测试堆空间常用的jvm参数:
* -XX:+PrintFlagsInitial :查看所有的参数的默认初始算
* -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
* -Xms: 初始堆空间内存 (默认为物理内存的1/64)
* -Xmx: 最大堆空间内存(默认为物理内存的1/4)
* -Xmn: 设置新生代的大小。(初始值及最大值)
* -XX:NewRatio: 配置新生代与老年代在堆结构的占比
* -XX:SurvivorRatio: 设置新生代中Eden和se/S1空间的比例
* -XX:MaxTenuringThreshold: 设置新生代垃圾的最大年龄
* -XX:+PrintGCDetails: 输出详细的GC处理日志
* 打印gc简要信息:⑧-XX:+PrintGC
* -XX:HandlePromotionFailure: 是否设置空间分配担保
//查看默认的垃圾收集器
-XX: +PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令: jinfo -flag 相关垃圾回收器参数 进程ID
(需要配合jps进程号来使用)
//例:jinfo -flag UseParallelGC 17948
//Parallel 参数
-XX:+UseParalle1Gc 手动指定年轻代使用Parallel并行收集器执行内存回收
-XX:+UseParallel0ldGc 手动指定老年代都是使用并行回收收集器。(默认两个互相激活,开启一个就可以)
-XX: ParallelccThreads 设置年轻代并行收集器的线程数。- -般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STw的时间)。单位ms。 ##谨慎使用
-XX:GCTimeRatio垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。
-XX: +UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
//CMS 参数设置
-XX: +UseConcMarkSweepGC 手动指定使用CMS收集器执行内存回收任务。
-XX: +UseCMSCompactAtFullCollection用于指定在执行完FullGC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX: CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理。
-XX: ParallelCMSThreads设置CMS的线程数量。
-XX:CMS1ni tiatingOccupanyFraction 设置堆内存使用率的阈值, 一旦达到该阈值,便开始进行回收。
➢JDK5及以前版本的默认值为68 ,即当老年代的空间使用率达到68号时,会执行一次CMS回收。JDK6及 以上版本默认值为92%
//G1 参数设置
-XX: +UseG1GC 手动指定使用G1收集器执行内存回收任务。
-XX:G1HeapRegionSize设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000.
-XX :MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
-XX: ParallelGCThread 设置STw工作线程数的值。最多设置为8
-XX: ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设 置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
//日志的查看
-XX:+PrintGC 输出Gc日志。类似: -verbose:gc
-XX:+PrintGCDetails 输出Gc的详细日志
-XX:+PrintGCTimeStamps 输出Gc的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013-05-04T21 :53:59.234+0800)
-XX:+PrintHeapAtGC 在进行Gc的前后打印出堆的信息
-Xloggc:.. /logs/gc.log日志文件的输出路径