JVM

JVM篇

1、说说JDK、JRE、JVM?

JDK,Java标准开发包(Java开发工具包),它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境、以及常用的Java类库等。

JRE,Java运行环境,用于运行Java的字节码文件。JRE中包括了JVM以及JVM工作所需的类库,普通用户只需要安装JRE来运行Java程序,而程序开发者必须安装JDK来编译、调试程序。

JVM,Java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心部分,负责运行字节码文件。

JDK中包含了JRE,JRE中包含了JVM。

2、JVM内存模型?

(1)程序计数器:用于存储当前线程执行的字节码指令的地址。在多线程环境下,每个线程都有自己独立的程序计数器,以保证线程切换后能恢复到正确的执行位置。

  • 程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;

(2)Java虚拟机栈: 线程私有的,每个线程都有一个私有的Java虚拟机栈,用于存储方法调用的局部变量、操作数栈、动态链接、方法出口等信息。每个方法的调用都会创建一个栈帧,方法执行完毕后栈帧会被销毁。如果线程请求的栈深度大于虚拟机允许的深度,将抛出栈溢出异常。

(3)本地方法栈: 线程私有的,与虚拟机栈类似,用于支持Native方法的执行。

  • 本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;

(4)堆:用于存储对象实例和数组

  • 堆:java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作

(5)方法区: 存储类的元数据、常量等信息。运行时常量池是方法区的一部分,用于存放编译时生成的各种字面量和符号引用。在类加载后,常量池会被加载到内存中。

  • 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据,即永久代。在jdk1.8中不存在方法区了,被元空间替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元空间中,运行时常量池保存在堆中;

(6)本地内存: 直接内存不是JVM内部的一部分,但也被广泛使用。它是一种在Java堆之外直接分配内存的方式,通常与NIO相关。直接内存的分配和释放需要通过本地方法库来实现。

3、Java虚拟机中有哪些类加载器?

(1)启动类加载器:这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。

(2)扩展类加载器:这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

(3)应用程序类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

(4)自定义类加载器:用户自定义的类加载器。

4、类加载的过程?

类加载的过程包括:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析统称为连接。

(1)加载:通过一个类的全限定名来获取定义该类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。

(2)验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

(3)准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值“通常情况”下是数据类型的零值。

(4)解析:将常量池内的符号引用替换为直接引用。

(5)初始化:到了初始化阶段,才真正开始执行类中定义的Java初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。

(6)使用:JVM开始从入口方法开始执行用户的程序代码。

(7)卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。

5、JVM有哪些垃圾回收算法?

(1)标记清除算法:

标记阶段:把垃圾内存标记出来;

清除阶段:直接将垃圾内存回收;

这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。

(2)复制算法:为了解决标记清除算法的内存碎片问题,复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。

(3)标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。

6、JVM垃圾回收机制?

在触发GC的时候,会使用垃圾回收机制。

对那些JVM认为已经“死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。

7、GC Roots有哪些?

可作为GC Roots的对象包括下面几种:

(1)虚拟机栈中引用的对象。

(2)方法区中类静态属性引用的对象。

(3)方法区中常量引用的对象。

(4)本地方法栈中JNI(即一般说的Native方法)引用的对象。

8、什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

9、使用双亲委派模型的好处?

使用双亲委派模型来组织类加载器之间的关系,好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

10、怎么判定对象已经“死去”?

常见的判定方法有两种:引用计数法和可达性分析算法,HotSpot中采用的是可达性分析算法。

(1)引用计数法:

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

客观地说,引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,但是主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

(2)可达性分析算法: 就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

11、HotSpot为什么要分为新生代和老年代?

HotSpot根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

其中新生代又分为1个Eden区和2个Survivor区,通常称为From Survivor和To Survivor区。

img

12、HotSpot GC的分类?

(1)Partial GC:并不收集整个GC堆的模式,具体如下:

1)Young GC/Minor GC:只收集新生代的GC。

2)Old GC:只收集老年代的GC。只有CMS的concurrent collection是这个模式。

3)Mixed GC:收集整个新生代以及部分老年代的GC,只有G1有这个模式。

(2)Full GC/Major GC:收集整个GC堆的模式,包括新生代、老年代、永久代等所有部分的模式。

13、HotSpot GC的触发条件?

(1)触发Young GC:当新生代中的Eden区没有足够空间进行分配时会触发Young GC。

(2)触发Full GC:

1)当准备要触发一次Young GC时,如果发现统计数据说之前Young GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Young GC而是转为触发Full GC。 ​ 2)如果有永久代的话,在永久代需要分配空间但已经没有足够空间时,也要触发一次Full GC。 ​ 3)System.gc()默认也是触发Full GC。 ​ 4)heap dump带GC默认也是触发Full GC。

5)CMS GC时出现Concurrent Mode Failure会导致一次Full GC的产生。

14、Full GC后老年代的空间反而变小?

HotSpot的Full GC实现中,默认新生代里所有活的对象都要晋升到老年代,实在晋升不了才会留在新生代。假如做Full GC的时候,老年代里的对象几乎没有死掉的,而新生代又要晋升活对象上来,那么Full GC结束后老年代的使用量自然就上升了。

15、什么情况下新生代对象会晋升到老年代?

(1)如果新生代的垃圾收集器为Serial和ParNew,并且设置了-XX:PretenureSizeThreshold参数,当对象大于这个参数值时,会被认为是大对象,直接进入老年代。

(2)Young GC后,如果对象太大无法进入Survivor区,则会通过分配担保机制进入老年代。

(3)对象每在Survivor区中“熬过”一次Young GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,可以通过-XX:MaxTenuringThreshold设置),就将会被晋升到老年代中。

(4)如果在Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

16、发生Young GC的时候需要扫描老年代的对象吗?

在分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,如果回收新生代时,不得不同时扫描老年代的话,那么Young GC的效率可能下降不少。

17、垃圾收集器有哪些?

(1)Serial。Serial是一个单线程的收集器,它不但只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。适合用于客户端垃圾收集器。

(2)Parnew。ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。

(3)parallel Scavenge。Parallel Scavenge收集器关注点是吞吐量。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间;高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。

(4)Serial old。Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。主要有两个用途: 1)在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。 2)作为年老代中使用CMS收集器的后备垃圾收集方案。

(5)JDK8-CMS:(关注最短垃圾回收停顿时间)。CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。初始标记、并发标记、重新标记、并发清除。

(6)JDK9-G1:(精准控制停顿时间,避免垃圾碎片)。是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以及高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。相比与CMS收集器,G1收集器两个最突出的改进是: 1)基于标记-整理算法,不产生内存碎片。 2)可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

18、如何配置垃圾收集器?

(1)首先是内存大小问题,基本上每一个内存区域都会设置一个上限,来避免溢出问题,比如元空间。

(2)通常,堆空间会设置成操作系统的2/3,超过8GB的堆,优先选用G1。

(3) 然后采用JVM进行初步优化,比如根据老年代的对象提升速度,来调整年轻代和老年代之间的比例。

(4)依据系统容量、访问延迟、吞吐量等进行专项优化,服务是高并发的,对STW的时间敏感。

(5)会通过记录详细的GC日志,来找到这个瓶颈点,借用GCeasy这样的日志分析工具,定位问题。

19、JVM性能调优?

常用命令:jps、jinfo、jstat、jstack、jmap。

 

20、内存溢出问题?OOM

(1)当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生内存溢出问题。

(2)假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。

(3)内存申请过大:程序试图申请超过可用内存的空间,超出了操作系统或者硬件的控制。

(4)内存泄漏:程序中存在未被释放的内存块,随着程序的执行,这些未释放的内存会逐渐累积,最终导致内存耗尽。

(5)同时运行多个大内存消耗程序:如果同时运行了多个需要大量内存的程序,可能会导致系统内存不足。

(6)内存碎片问题:即使总内存足够,但是由于内存碎片的存在,导致没有足够的连续内存块来分配给程序。

解决方法:

(1)优化程序设计,减少内存消耗。

(2)检查是否存在内存泄漏,并修复。

(3)使用合适的数据结构和算法,避免不必要的内存占用。

(4)合理配置虚拟内存。

(5)确保及时释放不再使用的内存块。

(6)分析系统资源使用情况,避免过度占用资源。

posted @   守漠待花开  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示