JVM面试题

类生命周期

 一个类完整的生命周期,会经历五个阶段,分别为:加载、连接、初始化、使用、和卸载。其中的连接又分为验证、准备和解析三个步骤。
 加载:先找到字节码文件(.class文件),加载到JVM内存中的方法区里。然后在内存中的体现就是一个Class对象;
 验证:验证加载到内存里的.class文件是否被篡改过,确认没有安全问题,以及符合JVM规范;
 准备:为类中的一些变量分配内存空间,并且设置一下默认值;
 解析:将常量池内的符号引用转为直接引用;
  符号引用:符号引用是一种泛指,com/mashibing/A-findAllOvoid;
  直接引用:直接指向的内库的具体位置,直接就是内存偏移量。后面调用会更快。
 初始化:对所有静态变量复制,执行静态代码块,初始化当前类的父类。
 前面走完,到这,这个.class就可以在Java程序中使用了,new一个对象,类名.静态方法即可。
 使用完后,没有被引用,会被垃圾回收器回收掉(卸载),卸载后如果再次使用,又会初始化。


类加载器,JVM类加载机制

 类加载器:将.class加载到内存中,一个类加载过程中,JDK8版本会由启动类加载器、扩展类加载器、应用程序加载器加载,分三个加载器加载主要是为了单一职责。


  引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  自定义加载器:负责加载用户自定义路径下的类包
 JVM类加载机制:例如一个自定义加载器进行加载时,先委派给父加载器加载,一直委派到启动类加载器,加载到返回,如果启动类加载器没有加载到,向子加载器进行加载,直到加载完成,如果加载完成也没有加载到,报类加载器没有找到的异常。
双亲委派机制的作用保证类的唯一性与安全性,保证类在加载过程只被加载一次,比如核心类库的String类与用户自定义的String类,使用此机制,在启动类加载器就加载了核心类库的String类,不用担心用户篡改
双亲委派机制的打破场景:例如Tomcat要加载不同web应用,web应用可能有一个相同命名的user类,如果不打破双亲委派,第一个web应用的user类被加载后,由于只会加载一次,另外的就报类加载器没有找到的异常,Tomcat为了保证每个应用要有自己独立的类就需要打破。
JDK9版本引入了模块化,把包划分到不同的模块下,把扩展类加载器改为平台类加载器,模块指定了具体的类加载器,平台类加载器读取到类后,会根据模块化指定的加载器交给具体类加载器加载,如果是应用类加载器,在平台类加载器就不会再向上加载了,直接到应用类加载器,改善了类加载器的性能。


JVM的内存区域

 线程共享:方法区、堆;
 线程私有:虚拟机栈、程序计数器、本地方法栈。

 类结构、常量静态变量存储在方法区;
 运行到main方法时往栈压入栈帧,栈帧中存局部变量方法出口等;
 调用别的方法,往栈顶压入栈帧,方法实现里的局部变量会在新栈帧新的一块存储,局部变量赋值的字面量(比如int a = 1的1)存在方法区中,局部对象就在栈帧中引入指向在堆中的地址;
 native当修饰符的方法存在本地方法栈中。


对象创建的过程了解吗

 new指令new一个对象;
 类加载检查,常量池尝试获取对象引用,判断是否加载过类;
 不能拿到,加载类然后分配内存;能拿到,分配内存(Serial,ParNew,G1使用指针碰撞,CMS使用空闲列表);
 将对象数据存储在堆中的Eden,如果是CMS会先尝试在TLAB上分配;
 零值初始化;
 设置对象头;
 执行<init>方法,对对象级别的变量或非静态代码块进行初始化的(clinit对静态变量或者静态代码块来初始化)


对象内存分配方式

 指针碰撞:对于规则的堆内存,所有用过的内存放在一边,空闲的内存放在一边,两者中间用一个指针作为分界点的指示器(分界指针)(回收要不断整理压缩内存让它变得规整,垃圾回收慢点)。
 空闲列表(CMS为了减少STW时间使用):对于不规则的堆内存,集合维护一个空闲内存空间,分配内存时在列表寻找。(一个列表存空闲的内存块,存在内存使用不充分的问题,3M的放到5M内存块就浪费2M,但存在一个优点,回收后直接放回空闲列表即可)

JVM里new对象时,堆会发生抢占吗?JVM是怎么设计来保证线程安全的?/JVM对象分配内存如何保证线程安全

 两个线程同时new一个对象,线程1和线程2接受命令后会在堆内存分配空间,指针碰撞与空闲列表都可能指向同一个位置,就会发生堆抢占。
 两种解决方式:CAS与TLAB(可以一起使用)。
 TLAB:每个线程在Java堆中预先分配一小块内存,也就是本地线程分配缓冲
 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,我们可以通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。如果通过TLAB分配失败的时候,则会回到Eden区通过CAS方式进行分配


对象的内存布局


在Java虚拟机(HotSpot)中,对象在Java内存中的存储布局可分为三块:
 1.对象头存储区域
 2.实例数据存储区域
 3.对齐填充存储区域


对象头.png

 Object Header(对象头):
  1. MarkWord标记字段(32位占4字节,64位占8字节自身运行时数据:哈希值,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间戳);
  2. KlassPointer类型指针(开启压缩占4字节,关闭压缩占8字节):类的元数据的指针(D),用于堆对象连接类元信息,方便寻找类;
  3. 数组长度(4字节,只有数组对象才有)。
 实例数据:存储对象实际字段值的核心区域,包含了对象的所有非静态成员变量。
 对齐填充:保证对象8个字节对齐,对象完整大小是8的倍数,不是8的整数倍会加4个字节填充。


JVM内存泄露的原因

静态集合类引起内存泄漏:
  static List list = new Arraylist();
  使用后需要手动回收。
 单例模式:和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在JVM的整个生命周期中存在如果单例对象持有外部的引用,那么这个外部对象将不能被GC回收,导致内存泄漏。
 数据连接、IO、Socket等连接:创建的连接不再使用时,需要调用close方法关闭连接,只有连接被关闭后,GC才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被GC 回收。
 变量不合理的作用域:一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为null,很可能导致内存泄漏的发生。
 hash值发生变化:对象Hash值改变,使用HashMap、HashSet等容器中时候,由于对象修改之后的Hash值和存储进容器时的Hash值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么String类型被设置成了不可变类型。
 ThreadLocal使用不当:ThreadLocal的弱引用导致内存泄漏,使用完ThreadLocal一定要记得使用remove方法来进行清除。


如何判断对象仍然存活?

 引用计数法:对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。
  只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
  标准引用计数法无法解决循环引用,不能解决循环引用情况原因:引用计数器的值为0才不可用,但循环引用因为互相引用至少默认1。
 可达性分析算法(JDK普遍使用):JVM会去维护一个GC Roots集合(可达性根节点),从根节点寻找它们的引用,引用链上的都会被视为正在引用的对象,不在引用链上的都视为垃圾对象。
GC Roots包括:
 虚拟机栈(栈帧中的本地变量表)中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等(局部变量表);
 方法区中类静态属性引用的对象,比如:Java类的引l用类型静态变量;
 方法区中常量引用的对象,比如:字符串常量池(String Table)里的引用;
 所有被同步锁synchronized持有的对象


垃圾收集算法了解吗?

 复制算法:将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
 缺点:浪费内存,只能用一半内存空间,年轻代使用。


垃圾收集算法之标记清除算法.png

 标记清除算法:分为"标记"和"清除"阶段:标记存活的对象,统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
 缺点:标记对象太多,效率不高;标记清除后会产生大量不连续碎片


垃圾收集算法之标记整理算法.png

 标记整理算法:根据老年代的特点特出的一种标记算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

三色标记算法了解吗?三色标记优点?标记过程?产生的问题?

优点:
 1.用于垃圾回收器升级,将STW变为并发标记。STW就是在标记垃圾的时候,必须暂停程序,而使用并发标记,就是程序一边运行,一边标记垃圾。
 2.避免重复扫描对象,提升标记阶段的效率

 把gc roots可达性分析遍历对象过程中遇到的对象,按照"是否访问过"这个条件标记成以下三种颜色:
 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
 例:gc扫描过程中,初始扫描的直接引用标为黑色,间接引用的关联有两个对象,其中一个已经扫描并且没有关联对象标为黑色,另一个还没扫描到,标为白色,这个间接引用因此标为灰色(没分析完),默认都是白色。

 多标(并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,由于不会再对黑色标记过的对象重新扫描,所以不会被发现,那么这个对象不是白色的但是不会被清除)处理方式:当作浮动垃圾(为了应对浮动垃圾,老年代90%左右就要进行回收)处理。

 漏标(并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题):

   A a= new A();
   //开始做并发标记
   D d = a.b.d;
   a.b.d = null;
   a.d=d;

漏标两种处理方式:
 增量更新:所有对象在赋值时,会把赋值新增的引用弄一个集合存储,会把新增引用的对象标为灰色对象,重新标记去扫描;
  记录集:年轻代的某个对象被老年代引用,JVM开辟一段内存空间维护老年代对年轻代的所有引用;
  卡表:记录集的实现,老年代划分很多小区域(页内存,卡页),其中有一个引用到年轻代,就把卡页标记为dirty,GC时,这些变脏的元素会加入GC Roots(底层写屏障实现)。
 原始快照(SATB):先"拍个快照"记录所有存活对象的引用关系。之后如果某些引用被删除了,不会立即更新标记状态,而是假装这些引用仍然存在,即把这些对象全部标记为黑色,当作浮动垃圾去处理,下一轮回收。


说一下CMS收集器的垃圾收集过程

 初始标记:暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。(通过可达性算法寻找直接引用的)(STW)
 并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。 (通过可达性算法寻找所有引用的)(占时约80%)
 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。(STW)
 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
 并发重置:重置本次GC过程中的标记数据。


G1垃圾收集器了解吗

 G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间(本身算是没有新生代的Eden空间、Survivor空间,或者老年代空间了)。收集器能够对扮演不同角色的Region采用不同的策略去处理。
 本身采用指针碰撞方式分配内存(虽然要整理几个Region,但每个Region不大,CMS是需要整理压缩整个堆,还可以进行优先级处理)。
 优点:更精细的控制、可预测的停顿时间、内存碎片的控制、优先级处理

垃圾收集器-G1.png

 初始标记(initialmark,STW):暂停所有的其他线程,并记录下gcroots直接能引用的对象,速度很快;
 并发标记(Concurrent Marking):同CMS的并发标记
 最终标记(Remark,STW):同MS的重新标记
 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可用JVM参数-XX:MaxGCPauseMillis指定)来制定回收计划
  并不会回收整个堆的垃圾,按设置的STW时间(最大停顿时间)进行回收,多余的下一次回收时回收,这是为了用户体验(同时还会参照优先回收回收效益比更高的);
  回收算法用的复制算法,对比CMS很少有内存碎片,看着像标记整理算法;
  GC停顿STW时间设置过短,每次收集的垃圾过少,积累多了会导致Fullgc,最终GC停顿STW时间失效,同时,对G1优化,核心还是此参数。


有了CMS,为什么还要引入G1?

 先进的服务器能应对更大内存的服务器,发挥更大的性能。
优点:并发收集,低停顿
缺点:
 对CPU资源敏感(会和服务抢资源);
 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
 它使用的回收算法-"标记-清除"算法会导致收集结束时会有大量空间碎片产生(G1解决了,通过指针碰撞,可以每一次进行垃圾回收之后,进行内存的压缩整理,并且不会对垃圾回收有太大影响,只需要把需要回收的Region进行压缩整理),当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理;
 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收。
  浮动垃圾:并发清理过程中,new新对象,标记为黑色或灰色,这个对象如果一会儿又变成垃圾了就是浮动垃圾,下一次清理即可(默认直接当为黑色);
  "concurrent mode failure":老年代空间不足,并发过程中仍然向老年代扔垃圾,它会停止用户线程,专心垃圾回收,此时效率极其低(并发收集阶段再次触发Full gc的处理)。


你们项目用的什么垃圾收集器?为什么用它

常见的垃圾回收器:
 新生代收集器(高吞吐量):Serial、ParNew、Parallel Scavenge
 老年代收集器(SWT停顿时间):Serial Old、CMS、Parallel Old
 新生代和老年代收集器:G1、ZGC、Shenandoah

SerialNew(复制算法。单线程,不能利用多核)+SerialOld(标记整理。单线程)(Serial系列
是单线程,GC时stop the world)JDK 5版本之前;
JDK8:
 ParNew(复制算法。并行。单核情况下不如Serial)+CMS(标记清除,并发)
  适合类型:适用于需要低停顿时间的应用,如Web服务器、应用服务器。
  示例应用:电商网站、在线游戏、高并发服务器(ToC)。
  4-8G可以用ParNew+CMS
 Parallel Scavenge(复制算法。并行,吞吐量优先收集器)+ParallelOld(标记整理,并行)
  适合类型:适用于多核处理器的高吞吐量应用。
  示例应用:科学计算、数据分析、大规模数据处理(ToB)。
  4G以下可以用parallel
 G1(年轻代:复制老年代:标记-整理)JDK9默认的收集器要求尽可能可控GC停顿时间;内存占用较大的应用。
  适合类型:适用于需要可预测停顿时间的应用,尤其是大堆内存的应用。
  示例应用:企业级应用、中大规模Web服务、应用响应时间要求高的系统。
  8G以上可以用G1
 zgc:适用于需要极低停顿时间(毫秒级别)的大内存应用
  适合类型:适用于需要极低停顿时间(毫秒级别)的大内存应用。
  示例应用:内存密集型数据库、金融交易系统、云服务。
  几百G以上用ZGC
 JDK8选前三个,一般视项目性质选垃圾回收器,并且结合服务器性能,通常ToB系统比较复杂,使用Parallel、ParallelOld,如果是中型互联网项目,对并发量要求比较高,使用ParNew、CMS,如果项目是中大型,并且对应用响应时间要求高使用G1。


对象一定分配在堆中吗?,有没有了解逃逸分析技术?

 寄存器分配(Registers Allocation):在某些情况下,JIT编译器甚至可能将某些对象的内容存放在CPU寄存器中,以提高访问速度。
 对象逃逸分析:分析对象动态作用域,一个对象在方法中被定义后,它可能被外部方法所引用(如public User test1()作为调用参数传递到其他地方中,public void test()没逃逸)。
 除此之外,如果对象还有可能被外部线程访问到,例如赋值给可以在其它线程中访问的实例变量,这种就被称为线程逃逸。
 栈上分配(Stack Allocation):如果对象可以被确定为不会逃逸出其方法,则JVM可以在栈上为该对象分配内存。这减少了垃圾回收的压力,因为栈上的内存在方法执行结束后自动释放(特别大的对象还是堆)。
 优点:不用GC回收时回收,在方法出栈就销毁了,减轻垃圾回收器压力;线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉
 标量替换(Scalar Replacement):如果对象的所有属性都可以独立处理,JVM可能会对对象进行标量替换,将对象分解为其基本类型的成员变量进行优化。这种情况下,原始的对象概念被消除,更谈不上在堆或栈上分配。


了解哪些JVM监控和故障处理工具?

 Jconsole:一个内置Java性能分析器,是基于JavaManagement Extensions(JMX)的实时图形化监测工具,这个工具利用了内建到JVM里面的JMX指令来对Java进程提供实时的性能和资源的监控。其监控内容包括:内存、线程、类、CPU使用(Java进程的内存使用,线程的状态,类的使用)等。通过监控信息,可以很清晰的了解到当前程序是否运行正常,如内存泄露、死锁、类加载异常等
  本身可以选择本地或远程Java应用,选择远程配置:
   java -jar xxx.jar
    -Dcom.sun.management.jmxremote远程开启开关
    -Dcom.sun.management.jmxremote.port=1808 jmx远程调用端口
    -Dcom.sun.management.jmxremote.authenticate=false 不开启验证
     -Dcom.sun.management.jmxremote.ssl=false 不为ssl连接
    -Djava.rmi.server.hostname=34.126.141.21服务器所在ip或者域名
  提供了内存、线程、类、CPU使用(Java进程的内存使用,线程的状态,类的使用);然后可以在内存看各个部分使用量,同时可以点击执行垃圾回收;可以看线程加载数量,线程调用栈情况,同时可以检查死锁;类加载数量;Java运行时的指标参数。

jvisualvm(分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和CPU分析,其中Visual GC查看GC情况)
 jvisualvm:可以查看对象在新生代老年代的过程、装堆的快照信息等,通过jvisualvm进入。

jvisualvm.png

 它的右上方文件处可以装入eureka.hprof文件。
文件装入.png

 导入后如下:
导入后信息.png

 类指导入那一时刻关于类、类的实例、实例占比等信息。
 idea中可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来),之后导入分析
 JVM options中设置:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./(路径)
 之后内存引出问题可以看前几个分析情况。
 远程连接jvisualvm:
  java ‐Dcom.sun.management.jmxremote.port=远程机器端口号 ‐Djava.rmi.server.hostname=远程机器ip地址 ‐Dcom.sun.management.jmxremot
e.ssl=false ‐Dcom.sun.management.jmxremote.authenticate=false ‐jar microservice‐eureka‐server.jar

 jps:查看Java进程。
 jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
  jstat [-命令选项] [vmid][间隔时间(毫秒)][查询次数]
   jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况
   jstat -gccapacity pid 堆内存
   jstat -gcnew pid 新生代垃圾回收统计
   jstat -gcnewcapacity pid 新生代内存统计
   jstat -gcold pid 老年代垃圾回收统计
   jstat -gcoldcapacity pid 老年代内存统计
   jstat -gcmetacapacity pid 元数据空间统计
   jstat -gcutilcapacity pid 幸存区
 jmap用于生成堆转储快照(一般称为 heapdump或dump文件)。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
 jhat dump 文件名:分析dump文件
 jstack用于生成虚拟机当前时刻的线程快照(分析死锁)。


JVM的常见参数配置知道哪些?

堆配置:
 -Xms:初始堆大小
 -Xms:最大堆大小
 -XX:NewSize=n:设置年轻代大小
 -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
 -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。Eden:8Survivor:2,一个Survivor区占整个年轻代的1/5
 -XX:MaxPermSize=n:设置持久代大小

gc设置:
 -XX:+UseSerialGC:设置串行收集器
 -XX:+UseParallelGC:设置并行收集器
 -XX:+UseParalledlOldGC:设置并行年老代收集器
 -XX:+UseConcMarkSweepGC:设置并发收集器
 -XX:+UseG1GC

并行收集器设置:
 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数
 -XX:MaxGCPauseMillis=n:设置并行收集最大的暂停时间(如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
 -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
 -XX:ParallelGCThreads=n:设置并发收集器年轻代手机方式为并行收集时,使用的CPU数。并行收集线程数

打印GC回收的过程日志信息
 -XX:+PrintGC
 -XX:+PrintGCDetails
 -XX:+PrintGCTimeStamps
 -Xloggc:filename


线上服务CPU占用过高怎么排查

top找到CPU耗用最厉害的进程PID,按m进行排序;
top -H -p 进程PID找到耗用厉害的线程;
 通过printf '0x%x\n' 线程ID将线程ID转为16进制;
jstack 进程PID|grep 16进制线程PID -A 显示行数得到线程状态等信息定位代码。


内存飙高问题怎么排查?

 内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。
  先观察日志的情况
  top,按m进行排序;
  jstat-gcPID1000查看GC次数,时间等信息,每隔一秒打印一次,然后观察OC(老年代容量),OU(老年代已经使用大小),如果一直OU接近OC,说明无法回收对象。
  jmap-histoPID|head-20查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存,根据第一个进行分析。
 如果业务比较复杂,把dump文件(应用运行的快照信息)拿出来,jmap -dump:live,file=导出到的位置.hprof 进程ID导出文件,之后装入VisualVM,按大小排序,之后双击进入分析。


频繁minor gc怎么办?(不是很急,可放到老年代)

 验证minor gc频繁:jstat -gc 进程号 多久打印一次,YGC(minor gc总耗时),YGCT(minor gc回收次数);
 minor gc频繁先结合堆内存,堆内存经过Full gc能回收大量内存,就有可能是年轻代设置得太小,把一些短命对象流入老年代;
 可以通过增大新生代空间-Xmn来降低MinorGC的频率。


频繁Full GC怎么办?(急急急)

jstat -gc 进程号 多久打印一次,然后观察OC(老年代容量),OU(老年代已经使用大小),GC,FGCT(老年代回收次数),回收不掉,就可能有问题;
 可能原因:
  大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
  内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
  程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC
  程序BUG
  代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
  JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等
 查看堆内存各区域的使用率以及GC情况:jstat -gcutil -h20 pid 1000
 查看堆内存中的存活对象,并按空间排序:jmap -histo pid | head -n20
 dump堆内存文件:jmap -dump:format=b,file=heap pid
 可视化的堆内存分析工具:JVisualVM、MAT等分析
 排查指南:
  先观察日志的情况
  top,按m进行排序;
  jstat-gcPID1000查看GC次数,时间等信息,每隔一秒打印一次,然后观察OC(老年代容量),OU(老年代已经使用大小),如果一直OU接近OC,说明无法回收对象。
  jmap-histoPID|head-20查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存,根据第一个进行分析。
 如果业务比较复杂,把dump文件(应用运行的快照信息)拿出来,jmap -dump:live,file=导出到的位置.hprof 进程ID导出文件,之后装入VisualVM,按大小排序,之后双击进入分析。


有没有处理过内存溢出(OOM)问题?是如何定位的?

 OOM:内存溢出,频繁发生Full gc,老年代爆满,无法进行回收。
 OOM造成原因:
  1.本身分配得堆资源不够:jmap-heap 查看堆信息
   jmap -heap 进程ID:打印堆使用得内存,新生代使用多少,空闲多少,老年代使用多少,空闲多少。
  2.一次性申请的太多:更改申请对象数量
  3.内存资源耗尽未释放:找到未释放的对象进行释放
   系统已经OOM挂了定位:提前设置(记录磁盘空间,可能很大)-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=;然后载入VisualVM,找最占内存实例,双击进入找gc Root定位。
   系统运行中还未OOM定位:导出dump文件:jmap-dump:format=b,file=文件名.hprof 14660(会STW)或Arthas;然后载入VisualVM,找最占内存实例,双击进入找gc Root定位。


OOM一定会导致JVM退出吗

1.主线程中未处理的OOM:
 如果在主线程中发生OOM且没有被捕获,JVM通常会终止程序并退出。这是因为JVM中没有其他存活的非守护线程来保持程序运行。
2.子线程中未处理的OOM:
 在非主线程中,如果OOM发生且未被捕获,该线程会停止执行。但如果其他非守护线程仍在运行,JVM不会退出。
3.捕获并处理OOM:
 如果在代码中捕获并正确处理了OOM错误,JVM则可以继续执行其余的程序代码。合适的错误处理可能包括释放内存资源或提示用户进行适当的操作。


内存泄漏与内存溢出区别

 内存泄漏指的是程序未能释放已不再使用的对象或者资源,从而导致内存的浪费。在Java中,内存泄漏通常是由于对象的引用没有被正确清除,使得垃圾回收无法回收这些对象所占用的内存。
特点:
 不会立即导致程序崩溃,但会缓慢地消耗内存,最终可能导致内存不足。
 在Java应用中,常常由于集合类中保留了不再使用的对象引用而产生。

 内存溢出是指程序在申请内存时,没有足够的内存可用,导致无法正常分配,从而抛出OutOfMemoryError。这是由于程序已使用的内存超过了JVM为其分配的最大内存限制。
特点:
 通常会导致程序崩溃或终止。
 因为未能限制内存使用而直接超过JVM配置的内存上限。


堆一定是线程共享的吗

 TLAB(ThreadLocalAllocationBuffer):尽管堆是共享的,TLAB为每个线程分配了一个独立的内存块,用于快速分配对象。这减少了线程间在堆内存分配时的竞争,被分配到这得就不是线程共享的,其它情况线程共享。


Class常量池与运行时常量池区别

Class 常量池(Class Constant Pool)

 Class常量池是Java类文件的一部分,它由编译器在编译Java源文件时生成,存储在.class文件中。它包含了类或接口的字面量(如字符串、整数常量等)以及符号引用(如类和接口的名字、字段和方法的名字及描述符)。
特点:
 位于每个类文件的开头,是描述类或接口在Java平台上运行所需信息的重要部分。
 它是静态的,包含类文件中的所有依赖项,以便于类加载器在运行时解析。

运行时常量池(RuntimeConstant Pool)

 运行时常量池是Class常量池在类加载到JVM后的一种表现形式。它是类加载过程的一部分,在类或接口被载入JVM时,Class常量池的信息被载入运行时常量池。
 它在类加载时被创建,是方法区的一部分(在Java8后部分实现为元空间的一部分)。
特点:
 动态的,可以在运行时扩展,因为它不仅包含Class常量池的映射数据,还允许在运行时添加新的常量,例如通过字符串interning。
 用于解析类、方法、字段的引用,以及存储JVM执行所需的其他信息。

关系

来源与转换:
 Class常量池是从Java编译器生成的静态数据结构,是.class文件的一部分。
 运行时常量池是JVM执行环境的一部分,是Class常量池在类加载时被解析、验证后存储的方法区中的数据结构。
作用域与用途:
 Class常量池是在磁盘上文件级别的数据结构,定义了类的编译时依赖和信息。
 运行时常量池存在于内存中,在类加载期间被VM转化和使用,维护符号引用的解析,动态链接和跨越生命周期的优化。
使用与管理:
 编译器生成Class常量池,它是只读的。
 运行时常量池在运行期间可以被动态更新,允许JVM对类执行管理和优化。


运行时常量池与字符串常量池区别

运行时常量池(Runtime Constant Pool)
  1. 关系:
     运行时常量池是每个类或接口的常量池表的一部分,包含了字符串字面量以及其他的编译期常量。
     字符串常量池可以被视为是运行时常量池的一个特殊部分,专门用于字符串字面量的存储和重用。
  2. 存储位置:
    方法区(Method Area):
     运行时常量池属于方法区的一部分。
     在Java8之前,方法区是永久代(PermGen)的一部分;在Java 8及以后,方法区被转移到元空间(Metaspace)。
字符串常量池(StringConstant Pool)
  1. 关系:
     字符串常量池是用于存储字符串字面量的内存区域。当一个新的字符串字面量被声明时,如果字符串常量池中已经存在这个字符串,它将不会创建新的字符串对象,
    而是返回已经存在的引用。
     它是运行时常量池中专门用于字符串管理的部分。
  2. 存储位置:
    堆内存(Heap Memory):
     从Java7开始,字符串常量池移到了堆内存(主要是字符串使用、创建、销毁过于频繁)中。这种转移有助于减少方法区的内存压力,也更好地支持垃圾回收。
区别

 运行时常量池:用于存储类常量、方法和字段的引用,以及字符串字面量等。
 字符串常量池:专注于优化字符串使用,通过在堆中存储唯一的字符串实例来减少内存消耗。
位置差别:
 运行时常量池位于方法区/元空间。
 字符串常量池位于堆内存中。


JVM退出情况分析

Java虚拟机(JVM)退出通常是由以下几个原因导致的:
 1. 正常程序终止:
  当程序执行完main方法,包括所有非守护线程都终止时,JVM将正常退出。
 2. 调用System.exit(int status):
  显式调用System.exit()方法,以指定的状态码终止当前运行的Java虚拟机。
 3. 未捕获的异常或错误:
  如果某个线程抛出的异常没有被捕获,并且此异常传播到了主线程,JVM可能会终止。
 4. Runtime.halt(int)或崩溃:
  直接调用Runtime.halt()会立即停止Java进程,类似于突然终止程序而不调用任何钩子。
  JVM的致命错误(如内存访问违规)也可能导致崩溃并退出。
 5. 外部命令强制关闭:
  例如通过操作系统的任务管理器或者控制台命令,如kill命令。


JVM内存分代原因

  1. 提高垃圾回收效率:
     年轻代的对象创建和销毁频繁,通过将这些对象集中在一起,GC可以更高效地处理。年轻代的垃圾回收(MinorGC)通常采用复制算法,只扫描和处理一小部分存活对象。
  2. 减少GC停顿时间:
     短生命周期的对象大多集中在年轻代,通过更频繁但快速的垃圾回收可以快速释放内存,从而减少每次GC停顿时间。老年代的GC(MajorGC或FullGC)频率更低,因此对应用程序的停顿影响较小。
  3. 优化内存分配:
     分代允许JVM优化不同生命周期对象的处理机制。将短生命周期的对象与长生命周期对象分开,能够减少老年代的碎片化,避免其过于频繁的垃圾回收操作。
  4. 简化内存管理:
     简化了垃圾回收的过程,使得GC算法可以针对不同对象的生命周期优化内存的利用。年轻代使用ScavengeGC,老年代使用CMS或G1等更加复杂的算法,这些算法分别适合处理不同性质的内存块。
  5. 适应实际应用场景:
     大多数应用程序的对象生命周期符合分代假设,通过这种内存管理方式,能够适配绝大多数应用的行为特征,从而在各种应用中提供优秀的性能。
  6. 分代算法的灵活性:
     根据对象的不同生命周期特点,JVM可以更灵活地选择垃圾回收策略。例如,年轻代可以频繁进行快速的复制回收,而老年代可以使用更多时间进行标记-清除-压缩等操作以减少内存碎片。

GC是任意时候都能进行的吗

 GC垃圾收集只能在安全点才能进行。在Java虚拟机(JVM)中,安全点(SafePoint)是程序执行的某些特定位置。JVM只能在安全点安全地暂停执行,从而进行垃圾回收(GC)等操作。安全点的设定确保了当线程暂停时,程序的状态是可知和一致的。

安全点一般插入在以下几种情况:
 1.方法调用:每次方法调用都是一个潜在安全点。
 2.循环回跳:长时间循环中间会插入安全点检查。
 3.异常处理:处理异常时,也会检查是否到达安全点。

检查所有线程都到安全点才会GC。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容