JVM学习心得整理一篇帮你应付各大厂面试

以下内容是在看了儒猿技术窝的《从零开始成为JVM实战高手》、周志明 的《深入理解JVM虚拟机》第三版、官网文档《内存管理白皮书》以及其它渠道上对JVM知识的学习整理出来的一些知识点,如有错误或者不足之处,希望大家能够多多指出,共同进步!

内存模型

JDK1.6

线程共享:

堆分为新生代空间和老年代空间

*方法区 **

用于存储被虚拟机加载的类信息,静态变量,即使编译器编译后的代码等数据,也叫永久代

运行时常量池

存储一些常量信息

线程私有:

虚拟机栈:在执行一个方法的时候,会创建一个栈帧,并且把局部变量存储栈帧中,每个栈帧中都拥有:局部变量表。操作数栈,动态链接,方法接口等信息。

局部变量表存放了编译器可知的八大基本数据类型,对象引用类型(refernce)

会发生俩种错误 :

1)StackOverFlowError

2)OutOfMemoryError

不需要发生垃圾回收 ,return 语句或者抛出异常都会导致栈帧被弹出

本地方法栈 :为虚拟机提供Native方法

程序计数器:每个线程都有一个私有得程序计数器,大小约为1M而且不会发生OutOfMemoryError,生命周期随线程的创建而创建,随着线程的结束而消失

1)字节码解释器通过该改变程序计数器来依次读取指令,从而实现代码的流程控制,如顺序执行,选择,循环,异常处理。

2)在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候,就知道上次执行到了哪里,应用于线程的上下文切换

直接内存:

它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

直接内存是基于物理内存和Java虚拟机内存的中间内存

JDK1.8

方法区和运行时常量池变为了元数据空间,并且还不占用JVM得内存

为什么要替换为元数据空间?

因为永久代使用的是JVM空间,有一个设置的固定大小,无法进行调整,而元数据空间使用的是直接内存,减少了内存溢出概率。

垃圾回收可以解决什么问题?

它可以消除开发过程中的极大错误!

1)悬空引用:

可以释放某个对象所使用的空间,而其他对象仍对其有引用

2)内存泄漏

内存已分配且不再引用,但未释放,如果如果发生足够多的泄漏,它们会一直消耗内存,直到耗尽所有可用内存

怎么确定是否是一个垃圾

1)引用计数法

2)对象不可达性分析

对象不可达不一定会回收,而是去执行对象的finalize()方法,看是否把这个实例对象给了某个静态变量

哪些可以作为GcRoots

1)局部变量

2)静态变量

对象被引用就一定不能活嘛

看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候

四大引用类型

1)强引用:绝对不能回收的对象

2)弱引用:垃圾回收之后发现内存不够则会发生垃圾回收

3)软引用:只要发生垃圾回收就会回收

4)虚引用:主要用于finalize()方法的判断

垃圾回收器

1)分配内存

2)确保所有的对象都留在内存中

3)恢复被执行代码中不再从引用中访问的对象所使用的内存

垃圾收集解决了很多内存分配的问题,且需要占用时间和资源,大多数动态内存分配算法的主要问题就是如何必避免碎片且保持分配和回收的效率

理想的垃圾收集器特性

1)安全;不能错误的释放实时数据

2)时间,空间,频率方面衡量

处理空间碎片可以用压实的方法

空间分配的俩种方式

1)指针碰撞

2)空闲列表

多线程下的空间分配:采用叫做Thread-Local Allocation Buffers (TLABs)的技术,提高了多线程分配吞吐量通过给每个线程自己的缓冲区,每个TLAB只能分配一个线程,所以分配可以快速放置利用碰撞指针技术,不需要任何锁定

设计选择

1)串行与并行

2)压缩非压缩与复制

3)性能指标

  • 吞吐量

  • 垃圾收集开销

  • 停顿时间

  • 收集频率

  • 内存占用

  • 及时性

垃圾回收算法

复制算法

将一块空间内的存活对象复制到另一块空间,然后清空这一块空间里的对象,空间利用低

标记-清除算法

分为俩个阶段

1)回收器从根集合(寄存器,线程栈,全局变量)开始遍历对象图,并标记所遇到的每个对象

2)回收器检查堆中的每一个对象,并将所有未标记的对象当作垃圾进行回收

它是属于一种间接回收算法,并非直接检测垃圾本身,而是先确定所有存活对象,然后反过来判断,其他对象都是垃圾,注意的是,该算法的每次调用都需要重新计算对象存活集合。

标记-整理算法

标记要存活的对象,然后整理到一起,再清楚垃圾对象

分代收集算法

分新生代和老年代,选择不同的算法,新生代复制算法,老年代标记整理或者标记清除算法

标记压缩算法

Serial收集器

单线程串行收集,停止线程 。

新生代采用复制算法,老年代采用标记整理算法

优点: 简单高效,没有线程交互的开支,比较适合单核模式下的虚拟机

Serial Old收集器

1)老年代使用收集器,也有stw机制,使用标记-压缩算法

2)运行在client模式下默认的老年代垃圾回收器

3)在server端有两个用途

  • 与新生代Parallel Scavenge配合使用

  • 作为老年代CMS收集器的后备方案

ParNew收集器

serial的多线程版本

并行:多个人做一条路

并发:多个多条路,一个人一条路

Paraller Scavenge收集器

1)采用复制算法、并行回收,stw机制

2)吞吐量优先

3)具有自适应调节策略(ParNew是没有的)

适合在后台运算,不需要太多交互的任务

ParallerOld收集器

1)jdk1.6引入的老年代收集器

2)采用标记压缩,并行和stw机制

3)jdk8默认的垃圾收集器(Parallel Scavenge+Parallel Old)

CMS收集器

1)采用标记-清除算法和stw机制,会出现内存碎片,内存分配使用空闲,当堆内存使用达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作中有足够内存运行,如果cms运行期间,预留的内存无法满足程序运行,会抛出“Concurrent Mode Failure”,这时虚拟机采用后备方案,启用Serial Old收集器,这样停顿时间会变长。

2)为什么不用标记压缩算法

因为并发清除时,用压缩整理内存的话,原来的线程使用的内存有可能被修改了,所以压缩整理算法更适合在stw环境下

2)垃圾收集分为四个阶段

  • 初始标记

标记出GC Roots能直接关联到的对象,一旦标记完成之后,恢复之前暂停的所有应用线程

  • 并发标记

从GC Roots的直接对象开始遍历整个对象图的过程,这个过程较耗时,但是不需要停止用户线程。

  • 重新标记

在并发标记阶段,用户线程和回收线程同时运行,为了修正并发阶段,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。该阶段比并发标记时间短

  • 并发清除

清除掉标记阶段中被判定死亡的对象,释放内存空间。

优点:并发收集,低停顿

缺点:

  • 对CPU资源敏感

  • 无法处理浮动垃圾

  • 有空间碎片

G1收集器

1)适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量

2)把内存划分为多个(约2048个)Region(物理上不连续,小大相同)新生代空和老年代空间动态转化

3)G1有计划的避免在整个堆中进行全区域的垃圾收集,维护了一个优先列表,根据允许收集的时间,优先选择回收垃圾最多的Regin

** 优点:**

  • 停顿时间短且可控

  • 老年代,新生代都可以回收

  • Region是复制算法,整体上是标记压缩算法,没有内存碎片

  • 在GC慢时,采用应用线程承担GC工作

    缺点:

    在小内存上的应用CMS的表现优于G1,G1适合在大内存上发挥优势在垃圾回收时产生的内存占用,执行负载比CMS要大的多

    回收过程

    (1)年轻代GC

    扫描根:根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。跟引用连同RSet记录的外部引用作为扫描存活对象的入口。

    更新Rset:处理dirty card queue(见备注)中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。

    处理RSet: 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。 复制对象: 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阈值会被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。

    处理引用: 处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

    (2)并发标记过程

    初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。 根区域扫描:G1 GC扫描survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成。 并发标记:在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收,同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。 再次标记: 由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照法: snapshot-at-the-beginning(SATB)。 独占清理:计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的。 并发清理阶段:识别并清理完全空闲的区域。

    (3)混合回收

    当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意: 是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。

    并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收。

    混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。

    由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高,越会被先回收。并且有一个阈值会决定内存分段是否被回收。-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

    混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

    G1的初衷就是要避免Full GC的出现。按时如果上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

    要避免Full GC的发生,一旦发生需要进行调整。什么时候会发生Full GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到full gc,这种情况可以通过增大内存解决。

    导致G1Full GC的原因可能有两个:

    1. Evacuation的时候没有足够的to-space来存放晋升的对象;

    2. 并发处理过程完成之前空间耗尽。

为什么要分新生代和老年代

因为在程序运行过程中,很多对象的存活时间比较短暂,而有些对象存活的时间较长,为了更好的进行垃圾回收,提高资源的利用,把对象分为新生代和老年代,选择适合的算法进行垃圾回收,为了避免大对象长期存活占用对象空间,可以设置一定大小的对象之间进入老年代 XX:PretenureSizeThreshold来配置

新生代如何进入老年代

1)对象刚创建的适合为新生代,除非是大对象直接进入老年代。

2)新生代要想变成老年要么经过了一定的垃圾回收次数,JVM为15次也可以自行设置 XX:MaxTenuringThreshold

3)动态年龄判定机制,新生代中存活的对象放在s区中,将新生代s区空间中年龄1+年龄2+年龄3+年龄N的对象加起来的空间大于S区空间的一半,年龄大于或者等于该年龄的对象就可以进入老年代

XX:TargetSurvivorRatio

老年代空间担保机制

新生代要想进入老年代,为了避免内存溢出,首先得判断一下老年代空间是否大于新生代空间,如果不大于就通过一个参数设置来判断 根据以前的存活对象来判断是否可以进去。

什么时候会发生垃圾回收

minor GC

当对象进入新生代空间中,不断存活,最终导致E区满了之后,就会发生垃圾回收,采用复制算法和ParNew垃圾回收器,并将存活的对象复制进入老年代中 ,如果存活对象大于s区空间则会直接进入老年代中,所以合理的设置新生代e区和s区的大小

Old GC

当老年代空间满了之后会发生清理,采用CMS垃圾回收器,标记清除算法。如果清除之后还是放不下来自新生代的对象

会触发full GC 也就是对新生代做一次垃圾回收,因此,full GC 一般伴随着一次minor GC 顺带着会对元数据空间也进行清理

元数据空间中的类在什么情况下才会发生回收

1)该类的所有实例对象都已经被回收

2)该类的CLass对象没有任何引用

3)这个类的类加载器已经被回收

所有的对象都是分配在堆空间上嘛

JIT编译器可以在编译期间根据逃逸分析的结果,来决定是否可以将对象的内存分配从堆转化为栈

堆和栈有什么区别

1)申请方式

stack是系统自动分配且操作系统自动释放

heap是由程序员自己申请

2)申请后系统的响应

stack只要栈空间大于所申请空间就行

系统有个记录地址的空闲链表,会去寻找一段连续的大于申请空间的内存

3)栈空间在内存上是连续的,堆空间在内存上不连续

4)存储的内容不同

内存溢出

栈内存溢出

栈是线程私有的,每个方法在创建的时候就会创建一个栈帧,

如果存在递归调用,不断的往栈中压入栈或者栈帧的所占用的空间太大,可能就导致了栈满,这个时候会栈会继续深度申请,如果无法申请就会抛出StackOverFlowError异常,栈空间为最大空间时,还继续压入栈就会发生栈空间溢出

堆空间溢出

同理,垃圾回收之后还无法存放对象

方法区溢出

遇到动态生成大量的类,jsp等

常量池溢出

jdk1.8字符串常量池还是放在堆中 ,运行时常量池(存放各种类信息)存放在元数据空间

怎么排查OOM问题

1)增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;

2)同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;

3)使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。

如何处理OOM问题

1)第一步是检查完整的错误消息

2)Jconsole管理工具可用于监视挂起的对象数量终结。

类加载的流程

类的加载流程主要分为 加载 连接 初始化

加载:将java文件解析为可以被jvm识别的class字节码文件

连接主要分为以下三个过程

验证:验证该class文件是否符合规范

准备:为类中的局部变量,静态变量,以及对象等分配空间

静态变量有自己的赋值方式,不是初始化阶段赋值,准备阶段只是申请了空间

解析:将符号引用转化会直接引用

初始化:对之前申请了空间的对象和局部变量进行赋值。

类加载器

启动类加载器(BootStrapClassLoader):用来加载java核心类库,也就是lib包

扩展类加载器(Extension ClassLoader):用来加载lib/ext下的类

系统类加载器(AppClassLoader):更具java的类路径来记载类

自定义类加载器:几次自ClassLoader

加载器加载器规则

双亲委派原则 :当一个类收到请求时,不会自己去加载这个类,而是去请求父类,是否加载过这个类,直到最顶级的父类,然后开始加载这个请求,如果没有找到,就去子类加载

作用:为了避免重复加载某个类,且保证类的唯一性

用户定义java.lang.String类,会去加载自定义的类嘛

显然是不会的,自定义加载器,也会从启动类加载器开始加载,而不会加载自定义的类

双亲委派原则之打破tomcat

应用类加载器优先自行加载应用目录下的class,而不是委派给父类加载器

为什么tomcat要打破双亲委派原则

1)为了适应不同的webapp 其中的class和lib需要相互隔离,对于某些应用,又需要有共享的lib提高资源利用率

2)安全问题,使用单独的classloader去加载tomcat自身的类库避免被恶意或者无意的破坏

如何判断俩个类是否相等

判断俩个类是否相等,首先要看他们是不是同一个类加载器,只有同一个类加载器上加载的俩个相同的类才是相等的

三大拷贝深拷贝,浅拷贝,零拷贝

深拷贝:对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响

浅拷贝:拷贝对象共用一份实体,仅仅是引用的变量不同,对其中任何一个对象的改动都会影响另外一个对象

零拷贝:用户进程和驱动共享同一块物理内存,使得它们之间不需要数据拷贝,这就是零拷贝策略

调优工具

*jmap*

*jstat*

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

毛毛怪不奇怪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值