-
JVM的组成部分
-
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎),两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)
-
Class loader(类装载):装载class文件到运行时数据区中的方法区
-
Execution engine(执行引擎):执行字节码文件中的指令
-
Native Interface(本地接口):是java和其它编程语言交互的接口
-
带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库,会进入本地方法栈,调用本地方法本地接口
-
-
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存
-
-
介绍一下JVM运行时数据区?
-
方法区:存储类信息、常量、静态变量
-
Java 堆:是被所有线程共享的,所有的对象实例都在这里分配内存
-
本地方法栈:为虚拟机调用 Native 方法服务的
-
虚拟机栈:用于存储局部变量表、操作数栈、动态链接、方法出口等信息
-
程序计数器:指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成
-
线程私有的:程序计数器、虚拟机栈、本地⽅法栈
-
线程共享的:堆、方法区、(直接内存,元空间)
-
-
对象从加载到JVM,再到被GC清除,经历了什么过程
-
首先把字节码文件内容加载到方法区
-
然后再根据类信息在堆区创建对象
-
对象首先会分配在新生代的Eden区,经过一次Minor GC后,对象如果存活,就会进入Suvivor区。在后续的每次Minor GC中,如果对象一直存活,就会在Suvivor区来回拷贝,每移动一次,年龄加1
-
当年龄超过15后,对象依然存活,对象就会进入老年代
-
如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉
-
-
类加载的几个过程
-
加载、验证、准备、解析、初始化。然后是使用和卸载
-
通过全限定名来加载生成class对象到内存中,然后进行验证这个 class 文件,包括文件格式校验、元数据验证,字节码校验等。准备是对这个对象分配内存。解析是将符号引用转化为直接引用(指针引用),初始化就是开始执行构造器的代码
-
-
JVM怎么进行内存的分配和回收?
-
堆空间的基本结构:堆里面分为新生代和老生代(java8 取消了永久代,采用了 Metaspace),新生代包含 Eden+Survivor区,survivor 区里面分为 from 和 to 区,内存回收时,如果用的是复制算法,从 from 复制到 to,当经过一次或者多次 GC 之后,存活下来的对象会被移动到老年区,当 JVM 内存不够用的时候,会触发 FullGC,清理 JVM老年区
-
-
堆内存中对象的分配策略?
-
基本策略:对象优先在eden区分配,大对象直接进入老年代,长期存活的对象将进入老年代
-
对象优先在 eden 区分配:
-
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起⼀次 Minor GC
-
-
大对象直接进入老年代:
-
大对象就是需要大量连续内存空间的对象(比如:字符串、数组),目的为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率
-
-
长期存活的对象将进入老年代:
-
虚拟机给每个对象⼀个对象年龄(Age)计数器。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置
-
-
-
介绍一下四种引用的区别?
-
强引用:内存不足也不会回收
-
我们使用的大部分引用实都是强引用,是使用最普遍的引用。一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,JVM宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
-
-
软引用:内存不足时会回收
-
一个对象只具有软引用,内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存
-
软引用可以和引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中
-
-
弱引用:无论内存是否足够都会回收
-
⼀旦发现了只具有弱引用的对象,无论内存是否足够都会回收
-
弱引用可以和引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中
-
-
虚引用:形同虚设,任何时候都会被回收
-
如果一个对象仅持有虚引用,它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
-
主要用来跟踪对象被垃圾回收器回收的活动
-
与弱引用和软引用的区别是:必须配合引用队列(ReferenceQueue)联合使用。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收
-
-
一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出OOM等问题的产生
-
-
几种类加载器?
-
根加载器Bootstrap ClassLoader:负责加载JAVA_HOME/jre/lib目录下的类,加载核心类库
-
扩展类加载器Extension ClassLoader:负责加载JAVA_HOME/jre/lib/ext目录下的类,加载扩展类
-
应用程序类加载器Application ClassLoader:加载用户类路径(Class Path)上所有类库,以及第三方jar
-
-
双亲委派机制?
-
什么是双亲委派?
-
双亲委派就是优先委派上级类加载器进行加载,如果上级类加载器能找到这个类,由上级加载,加载后该类也对下级加载器可见。如果找不到这个类,则下级类加载器才有资格执行加载
-
-
双亲委派机制的目的(好处)?
-
保证安全:让类的加载有优先次序,保证核心类优先加载,避免核心类篡改
-
避免了类的重复加载:相同的 class文件被不同的ClassLoader加载就是不同的两个类
-
让上级类加载器中的类对下级共享(反之不行),即让你的类能依赖到jdk提供的核心类
-
-
能自己写一个类叫java.lang.System(不可以)吗?
-
假设自己的类加载器用双亲委派,那么优先由启动类加载真正的System,自然不会加载假冒的
-
假设自己的类加载器不用双亲委派,那么类加载器加载假冒的System时,它需要先加载父类Object,而你没用双亲委派,找不到Object类所以加载失败
-
实际操作中jvm发现自定义类加载器加载以java.打头的类时,会抛出安全异常,在jdk9以上版本这些特殊包名都与模块进行了绑定,连编译都过不了
-
-
-
如何判断一个类是无用的类?
-
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
-
加载该类的类加载器(ClassLoader)已经被回收
-
该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
-
-
如何判断对象是否死亡?(如何确定垃圾)
-
引用计数法:给堆内存中的每个对象中添加⼀个引用计数器来记录引用个数。每当有⼀个地方引用它,计数器就加1,当引用失效就减1,引用个数为0就认为是垃圾。JDK早期使用的方法,引用记数无法解决循环依赖的问题
-
可达性分析法(引用链法):将GC Roots,也就是根对象作为起点,从根对象向下一直找引用。如果一个对象到根对象没有任何引用链相连,证明此对象不可用
-
-
三种垃圾回收算法?
-
标记清除算法:
-
标记阶段:标记出所有不需要回收的对象
-
清除阶段:统⼀回收掉所有没有被标记的对象
-
会产生大量内存碎片
-
-
复制算法:
-
将内存分为大小相同的两块,每次使用其中的一块。垃圾回收的时候,就将还存活的对象复制到另一块去,然后再把这一半内存直接清除
-
没有内存碎片,但造成了空间的浪费(永远有一块内存空着)。效率跟存活对象的个数有关
-
-
标记压缩算法:
-
在标记阶段与标记清除算法一样,但在完成标记后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除
-
-
分代收集算法:
-
堆分为新生代和老年代,我们就可以根据各个年代的特点选择合适的垃圾收集算法
-
新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
-
老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择标记清除或标记压缩算法进行垃圾收集
-
-
-
介绍一下几种垃圾回收器?
-
串行垃圾回收器
-
只会使⽤⼀条GC线程去完成垃圾收集⼯作,它在进⾏GC⼯作的时候必须暂停其他所有的⼯作线程(STW),直到它收集结束。新⽣代采⽤复制算法,⽼年代采⽤标记压缩算法
-
-
并行垃圾回收器
-
ParNew:Serial收集器的多线程版本,默认开启的收集线程数和CPU数量一样,数量可以通过修改ParallelGCThreads设定。用于新生代收集,复制算法
-
Parallel Scavenge: 关注吞吐量,吞吐量优先。新⽣代采⽤复制算法,⽼年代采⽤标记压缩算法
-
Parllel Old:Parallel Scavenge的老年代版本,使⽤多线程和标记压缩算法,在注重吞吐量以及CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器
-
-
CMS收集器
-
CMS 收集器是通过标记清除算法实现的,是一种以获得最短回收停顿时间为目标的收集器,适合注重响应时间的程序.垃圾回收线程可以和用户线程同时运行,也就是说它是并发的。但是也会造成大量内存碎片
-
清除过程分为以下4个阶段:
-
初始标记:标记一下GC Roots能直接关联到的对象,会暂停所有用户线程(STW)
-
并发标记:从 GC Roots 开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要暂停用户线程
-
重新标记:在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会暂停所有用户线程(STW)
-
并发清除:清除标记对象,这个阶段也是可以与用户线程同时并发的
-
-
-
G1垃圾收集器
-
JDK 9开始默认的垃圾回收器。通过“-XX:+UseG1GC”启动参数即可指定使用G1 GC
-
响应时间与吞吐量兼顾
-
堆内存划分多个区域,每个区域都可以充当eden,survivor,old,humongous(专门用来存储大对象,大对象的复制成本高,G1主要是用标记复制算法,因此必须对大对象进行特殊处理 )
-
工作主要分为3个阶段:
-
新生代回收:当eden内存不足,会采用标记复制算法进行一次清理(STW)
-
并发标记(老年代占堆内存达到45%以上时会触发):在老年代里去找那些存活对象,然后给它们加上标记,这个过程是并发执行的(不会暂停用户线程),重新标记时需要STW
-
混合收集:并发标记完成后,开始混合收集,这一阶段会收集部分价值较高的老年代连同eden和幸存区做一次垃圾回收,复制时会STW
-
-
-
-
什么是守护线程?
-
守护线程是为所有非守护线程提供服务的线程,用户线程结束了,程序就会终止,守护线程也会被中断
-
任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用
-
守护线程不能去访问固有资源,比如读写操作或者计算逻辑,因为它随时会中断,IO,File等重要操作逻辑不可以分配给守护线程
-
-
怎么解决OOM问题?
-
什么情况下会遇到OOM?
-
误用线程池导致内存溢出,解决方法就是不要用Executor工具类的newFixedThreadPool,LinkedBlockingQueue是一个无界队列。建议使用ThreadPoolExecutor自己设置有大小限制的工作队列,防止溢出
-
查询数据量太大导致内存溢出
-
动态生成类导致的内存溢出:开发中经常用到一些第三方的脚本语言,动态生成类过多导致元空间溢出
-
递归没有设置终止条件会导致StackOverflow
-
-
你是怎么解决?
-
会先尝试分配一个更大的堆内存(-Xms8m),看是否能解决问题
-
如果解决不了可以生成Dump文件,用Jprofiler工具去查看问题
-
根据dump文件定位到异常的实例对象,和异常的线程(占CPU高),定位到具体的代码
-
-
-
项目如何排查JVM问题
-
可以使用jmap来查看JVM中各个区域的使用情况
-
可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
-
可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
-
通过各个命令的结果,或者jvisualvm等工具来进行分析
-
首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在minor gc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
-
同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
-
JVM常见知识点
最新推荐文章于 2024-09-12 11:18:03 发布