JVM基础(后续补充完善)
2021-08-05 16:30:43 0 举报
AI智能生成
加油学习,冲
作者其他创作
大纲/内容
什么是jvm内存结构
jvm将虚拟机分为五大区域:
程序计数器
线程私有的,是jvm的一块很小的区域,作为当前线程的行号指示器。用于记录当前虚拟机正在执行的行号地址
虚拟机栈
线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表,动态链接,操作数和方法返回等信息。当线程请求超过虚拟机允许的最大深度的时候,就会报StackOverFlowError
本地方法栈
线程私有的,保存的是Native方法的信息,当jvm线程调用native方法的时候,不会在虚拟机栈中创建,而是简单的动态链接直接调用改方法
堆
java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收操作
方法区
存放已经被加载的类的信息、常量、静态变量。即是编译器编译后的代码数据。即永久代,在jdk1.8后改为元数据区,原方法区分为俩部分,1、加载的类的信息,被存放在元数据区中;2、运行时常量池,保存在堆中
什么是jvm内存模型
java内存模型简称JMM。就是在底层处理器的内存模型基础上,定义自己的多线程语义。它明确的指定了一组排序规则,来表示线程之间的可见性
jmm规定:想要保证B操作能够看到A操作的结果,无论他们是否在一个线程内,那么AB之间必须满足Happens-Before(发生之前)关系
单线程规则:一个线程中的每个动作都happens-before该线程中后续的每个动作
监视器锁定规则:监听器的解锁动作happens-before后续对这个监听器的锁定动作
volatile变量规则:线程start() 方法执行happens-before一个启动线程内的任意动作
线程join规则:一个线程内的所有动作happens-before任意其他线程在该线程join()成功返回之前
传递性:如果A happens-before B, B happens-before C, 则 A happens-before C
heap和stack有什么区别(堆和栈)
申请方式
栈:自动分配。例如,声明一个局部变量 int b系统自动在栈中为b开辟空间
堆:需要程序员手动申请,并指明大小。new object()
申请后系统的相应
栈:只要栈剩余空间大于所申请空间,那么就会为该程序提供内存,否则报错提示栈溢出
堆:操作系统中有个记录空闲内存地址的链表,当系统收到程序的申请的时候,会遍历该链表,寻找到第一个空间大于所申请空间的堆结点,然后将该结点从空闲列表中删除,并分配给程序。由于堆的空间不一定正好等于申请空间,那么就会将多余的自动放回空闲列表中
申请大小的限制
栈:栈是向低地址扩展的数据结构,是一块连续的内存区域。也就是说栈的空间大小是预先设计好的,申请超过预定大小,就会报错,oveflow
堆:是向高地址扩展的,是不连续的内存区域。这是由于系统是用链表来存储空闲内存地址的。堆的大小受计算机的有效虚拟内存。
申请效率
栈:由系统自动分配,速度很快,程序员无法控制
堆:由new分配内存,一般速度较慢,但是比较灵活,用起来方便,但是会产生内存碎片
堆和栈的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令的地址(函数调用语句的下一条可执行语句),然后是函数的各个参数。静态变量是不入栈的
堆:一般是在堆的头部用一个字节存放堆的大小。
详细的说一下CMS的回收过程,CMS问题是什么
CMS(并发标记清除)收集器,是以获取最短停顿时间为目标的收集器,他在收集垃圾的时候,可以让用户线程和GC线程并发执行,因此在回收过程中也不会感觉到卡顿:回收过程分为四步
1、初始标记
主要是标记GCRoots下级(只有下一级)的对象,所以比较少,过程中会STW,但也是很快的
2、并发标记
根据上一级的结果,继续向下标记所有关联对象,直到这条链上没有对象,这个过程是并发执行的,虽然对象很多,但是并不会阻塞。没有STW
3、重写标记
重新标记一次,因为是并发执行的,所以在第二步的时候,也可能产生垃圾
4、并发清除
清除阶段是清理标记阶段判断死亡的对象,由于不需要移动对象,所以可以并发执行
CMS的问题
1、并发回收导致CPU资源紧张
在并发阶段,他虽然不用用户线程停顿,但是他占用了一部分线程,会导致程序变慢,降低总的吞吐量。
2、无法清理浮动垃圾
在并发清除的时候,程序在一直进行,就会伴随的垃圾一直产生,但这部分垃圾只能等下次清理,所以叫做浮动垃圾
3、并发失败
由于并发的时候,需要预留足够的内存空间,默认情况下,老年代使用了92%的时候就会出发CMS,假如留的空间不够了,就会出现一次并发失败,这时候,虚拟机就会启动后备方案,STW,临时启用Serial Old来进行老年代回收,这样停顿的时间更长了
4、内存碎片问题
CMS是一款基于“标记清除”算法实现的回收器,这就意味这又碎片空间产生,碎片空间太多时,会给大对象分配带来麻烦,往往会出现,还有很多空间,但不得不进行fullGC的情况
详细说一下G1的回收过程
G1回收器采用的是面向局部收集的思路和基于region(局部)的内存布局模式,是一款面向服务端的应用垃圾回收器。G1在jdk9成为服务端默认的垃圾回收器。G1在整体是标记整理算法实现,但从局部来看,又是基于标记复制算法实现。
回收过程分为四个步骤:
初始标记:(会STW)仅仅是标记初始的GCRoots能直接关联的对象,并且修改TAMS指针,让下一阶段用户线程并发运行的时候,能正确的在可用的区域分配对象。这个过程很短,一般在利用minorGC的时间,所以并没有额外的停顿时间
并发标记:从GCRoots对象开始在整个堆中进行可达性分析,递归扫描整个堆中的对象,找出要处理的对象,这个阶段耗时可能会长,但是可以和用户线程并发执行。当扫描完之后,还要对在扫描过程中出现变动的对象进行处理
最终标记:(会STW)对用户进行短暂的暂停,处理并发阶段结束后仍有变动的对象
清理阶段:更新区域内的数据,对各个区域的回收价值和成本进行排序,根据用户所希望停顿的时间,来进行回收。可以自己选择多个区域构成回收集,然后把决定回收那部分区域的存活的对象复制到空的区域中,再清理掉整个旧的区域空间。这里涉及到活对象的移动,所以必须停顿用户线程
JVM中一次完整的GC过程是什么样子的
java堆内存划分:
在java堆中,被划分成俩个部分。新生代和老年代,新生代占比1/3,老年代2/3。
在新生代中,有划分了ende,to survivor和fromsurvivor,他们默认占比8:1:1。
新生代中存活对象比较少,采用复制算法,只需少量的复制成本就好,(MinorGC)
老年代存活对象较多,采用标记整理或者标记清理,(Major GC)
转换流程
对象优先在Eden分配,当eden没有空间的时候,虚拟机将发起一个minor GC
在Eden区执行了一次GC之后,存活的对象会进入到一个Survivor分区
Eden区再次GC,这时会采用复制算法,将Eden和from区一起清理,存活对象被复制到to区
移动一次,对象年龄就会加一,对象年龄大于一定的阈值之后,就会移动到老年代,默认是15,可以通过-XX:MaxTenuringThreshold
动态对年龄判定:survivor区相同年龄所有对象大小的总和>(Survivor 区内存大小 * 这个目标使用率)时,大于等于该年龄的对象直接进入老年代。大于等于该年龄的对象直接进入老年代。其中这个使用率通过-XX:TargetSurvivorRatio指定,默认为50%;
Survivor区内存不足会发生担保分配,超过指定大小的对象就可以直接进入老年代
大对象直接进入老年代,大对象就是需要大量连续内存空间的对象。为了避免大对象分配内存是由于分配担保机制带来的复制而降低效率
老年代满了额无法容纳更多的对象,MinorGC之后通常就会进行FullGC,FullGC清理整个内存堆-包括年轻代和老年代
Minor GC 和 Full GC 有什么不同吗
MiniorGC只发生在新生代,FullGC收集整个堆,包括新生代,老年代,等所有。
MiniorGC触发条件:当Eden区满的时候,触发
FullGC触发条件:
通过MiniorGC后进入老年代的平均大小大于老年代可用内存。如果发现统计数据说之前的MiniorGC的平均晋升大小比目前oldgen剩余的空间大,则不会触发MiniorGC而是转化为FullGC
老年代空间不够分配新的内存
由于Eden区和from区向to区转移数据的时候,to区不够,同事老年代也不够的时候
调用Syste.gc,系统建议执行FullGC,但也不一定
介绍一下空间分配担保原则
jvm有个老年代空间分配担保机制来保证对象能够进入到老年代
在执行youngGC之前,jvm会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,新生代所有对象都存活了,只能进入到老年代。这是时候,如果老年代的内存大小是大于新生代所有对象的,就可以放心YoungGC。如果是小于,那就有可能不够放,这时候jvm就会检查-XX:HandlePromotionFailure参数是否允许担保失败,如果允许就会判断老年代最大可连用空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽管这次是有风险的。如果小于,或者参数不允许,就就行一次FullGC
通过图解来看
什么是类加载,类加载的过程
虚拟机把描述类的数据加载到内存里面,并对数据进行校验,解析,初始化,最终变成可以被虚拟机直接使用的对象
类的整个生命周期包括:
加载,验证,准备,解析,初始化,使用和卸载。其中验证,准备,解析三个部分可以称为连接
加载,验证,准备,初始化,卸载这五个步骤是确定的,类的加载过程必须按照这种顺序进行,而解析不一定,他可以在初始化之前或者之后,这就为了支持java语音的运行时绑定(也称为动态绑定)
类的加载过程:
加载,分为三步:1、通过全限定类名获取该类的二进制流;2、将二进制流的静态内存结构转为运行时数据结构;3、在堆中为该类生产一个class对象
验证:验证该类是否符合虚拟机的要求,会不会威胁到jvm的安全
准备:为class对象的静态变量分配内存,初始化其初始值
解析:该阶段主要是将符号引用转换为直接引用
初始化:到了初始化阶段,才执行java类中的代码,初始化阶段是调用构造器的过程
什么是类加载器,常见的类加载器有哪些
什么是类加载器:通过一个类的全限定类名获取该类的二进制字节流叫做类加载器
类加载器分为四种:
启动类加载器:用来加载java核心类库,java程序员无法调用
扩展类加载器:用来加载java的扩展类库,java虚拟机会提供一个扩展类目录,该类加载器在扩展类中找并加载
应用程序类加载器:它根据java的类路径来加载类,一般俩说,java应用是通过它加载的;
自定义类加载器:由java语言实现,继承ClassLoader
什么是双亲委派模型,为什么需要双亲委派模型
当一个类加载器收到一个类加载的请求,他首先不会尝试自己加载,而是将这个请求派给父类去加载,只有父类加载器在自己的搜索范围内找不到类时,子加载器才会去加载
为了防止内存中出现多个相同的字节码;因为没有双亲委派的话,无法保证类的唯一性
怎么打破双亲委派:自定义ClassLoader类,重写LoadClass和findClass方法
什么情况下会发生栈内存溢出
栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候,都会创建一个栈帧,它包括局部变量表,操作数栈,动态链接和方法出口等信息,局部变量又包括基本数据类型和引用数据类型
当线程请求栈的最大深度超过虚拟机允许的最大深度时,就会抛出StackOverFlowError异常,方法的递归可能引起该问题
调整参数-xss去调整jvm栈的大小
谈谈对OOM的认知,如何排查OOM的问题
除了程序计数器,其他内存区域都有oom的风险
栈一般会发生stackoverflow,无限创建线程就会导致栈溢出,例如一直递归
java8常量池移到堆中,溢出会包java.lang.outofmemoryerror, 设置最大元空间参数没用
堆内存溢出,报错同上。gc之后,无法创建新的对象,就会报错
排查的话,首先可以通过jstat查看jvm的内存和gc的情况,先观察大概在那个问题
然后可以通过工具进入dump文件,分析大对象的占用情况。比如hashmap做缓冲未清理,时间长了就会溢出。这个可以通过将其设为弱引用解决
谈谈JVM的常量池
jvm的常量池一般分为class文件常量池,运行时常量池,全局字符串常量池以及基本类型包装类型常量池
class文件常量池
class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被翻译成 .class文件格式的二进制数据流存放在磁盘中,其中就包括class文件常量池
运行时常量池
运行时常量池相对于class文件常量池有一大特点就是具有动态性,java规范并不要求常量只能在java运行时产生,也就是说运行时常量池的内容并不完全来自于class文件常量池,在运行过程中可以通过代码生成常量并将其放入到常量池中,这种特性被用的最多的就是string.intern()
全局字符串常量池
字符串常量池是jvm维护的一个字符串实例的引用列表,在字符串常量池中维护的是字符串实例的引用。底层c++实现的就是hashtable。
基本类型包装类对象常量池
java中的基本类型的包装类型大部分都实现了常量池的技术,比如对应值小于127时才可以使用常量池,也既对象不负责创建和管理大于127这些类的对象
如何判断一个对象是否存活
引用计数法
给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,引用计数器+1,引用失效时,引用计数器-1;当引用计数器等于0的时候,就说明这个对象没有被引用,也就是垃圾对象,等的被回收
缺点:无法解决循环引用的问题,比如a引用b,b引用a,此时a、b对象引用都不为零,此时也就无法垃圾回收
可达性分析算法
从一个被称为GC Roots的对象向下搜索,如果一个对象到GCRoots之间没有直接或者间接的引用链接的时候,说明此对象不可用
可以做GCRoots对象的有这几种:
虚拟机栈中引用变量
方法区类静态属性引用的变量
方法区常量池引用的对象
本地方法栈引用的对象
但一个对象满足上述条件的时候,并不会马上被回收,还需要标记俩次; 第一次标记:判断当前对象是否有finalize()方法,并且该方法没有被执行过,若不存在则标记为垃圾对象,等待回收;若有的话,则进行二次标记;
第二次标记:将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚拟机不保证该方法一定会被执行,这是因为如果线程执行缓慢或者进入了死锁,会导致回收系统的崩溃。如果执行了finalize方法之后,仍然没有与GC Roots有直接或者间接联系,则该对象被回收
强引用,弱引用,软引用,虚引用是什么,有什么区别
强引用:就是普通关系的引用,如:String s = new String("string")
弱引用:用于维护一些可有可无的对象。只有在内存不足的时候,才会被回收,当回收之后,仍然没有足够的内存,那么抛出内存溢出异常。SoftReference实现
软引用:想对于弱引用,更加无用一些,只要jvm进行垃圾回收的时候,就会将其回收,无论内存是否存足。WeakReference实现
序引用:是一种形同虚设的引用,在现实常场景中用的不是很多,他主要是用来跟踪对象被垃圾回收的活动。PhantomReference实现
被引用的对象就一定可以存活吗
不一定,看引用类型,弱引用在GC的时候会被回收,软引用在内存不足的时候,既OOM前被回收,如果没有在Reference Chain 中的对象就一定会被回收
java中的垃圾回收算法有哪些
标记清除算法:
第一步:利用可达性去遍历,把存活对象和垃圾对象进行标记;
第二步:在遍历一遍,将所有标记的对象进行回收
特点:效率不高,容易产生不连续的空间碎片,导致之后可能无法找到大的空间创建大对象而不得不触发一次GC
标记整理算法:
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:将所有存活的对象向一端移动,将端边界以外的对象都回收
特点:适用于存活对象多,垃圾少的情况,整理过程,没有空间碎片产生
复制算法:
将内存按照容量大小分为相等的俩块。每次只是用一块,当一块用完了,就将还存活的对象移到另一块上,然后再吧使用过得空间移除
特点:不会产生空间碎片,但是内存利用率很低
分代收集算法:
根据内存对象的存活周期不同, 将内存分为几块,java虚拟机将其分为老年代和新生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法。老年代中因为对象存活率高,没有额外空间进行分配,所以使用标记清除或者标记整理算法进行回收
有哪几种垃圾回收器,各自的优缺点是什么呢
垃圾回收器主要分为这几种:Serial、ParNew、Parallel Savenge、Serial old、Parallel Old、CMS、G1
Serial:单线程的收集器,在回收的时候,必须停止所有线程,采用的是复制算法,对于一些实时性要求不高的应用还是可以接受的
ParNew:Serial的多线程版本,也是复制算法,也需要stw,
Parallel Svaenge:新生代收集器。和ParNew最大的区别就是,可以自定义参数,实现回收时间停顿和吞吐量的最优解
Serial old:Serial的老年代版本,标记整理算法那,单线程
Paralled old:新生代收集器老年版使用多线程,标记整理算法
CMS:为了实现最少停顿时间的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集完之后会产生大量碎片空间
G1:标记整理算法实现,运作流程:初始标记,并发标记,最终标记,筛选回收。不会产生碎片空间,精准的控制。G1将空间分为很多个相同的区域,G1跟踪每个区域的垃圾大小,在后台维护一个优先队列表,没吃在允许的时间内,回收价值最大的区域,在有效地时间内获取尽可能大的价值
0 条评论
下一页