文章目录
- JVM
- 一、JVM内存区域
- 1、运行时数据区域
- 1、程序计数器
- 2、java虚拟机栈
- Q:栈可能出现的两种错误
- 3、本地方法栈
- 4、堆
- Q:堆中会出现的错误
- Q:堆中对象的分配过程
- 5、方法区
- Q:堆和栈的区别?
- 2、为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
- 1、方法区的常用参数有?
- 2、运行时常量池
- 3、字符串常量池
- 4、jdk1.7 为什么将字符串常量池移到堆中?
- 5、直接内存
- 3、对象创建过程
- Q:JVM创建对象,如何保证线程安全性?
- 4、Java内存分配方式
- 5、Java对象内存布局
- Q:对象怎么访问定位?
- 6、JVM中对象及常量、局部变量、全局变量的存储位置
- 7、Java虚拟机的生命周期
- 二、JVM 垃圾回收
- 1、对象死亡的判断方法
- 2、哪些对象可以作为GC Roots 呢?
- 3、常见的引用类型(强软弱虚)
- Q:如何判断一个常量是废弃变量?如何判断一个类是无用的类?
- 4、垃圾回收算法
- 5、垃圾回收器
- CMS收集器
- G1
- Q:有了CMS,为什么还要引入G1?
- Q:垃圾回收器负责的3个任务
- 6、Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC 都是什么意思?
- Q:Minor GC的触发条件?
- Q:什么时候会触发Full GC ?
- Q:对象什么时候进入老年代?
- 三、类加载过程
- 1、类的生命周期
- 2、类加载过程
- 1、加载
- 2、验证
- 3、准备
- 4、解析
- 5、初始化
- Q:初始化开始的时机
- 6、卸载
- 3、Java中的类加载器
- Q:为什么要自定义类加载器?
- Q:如何实现自定义类加载器?
- 4、双亲委派模型
- Q:双亲委派模型实现步骤?
- Q:双亲委派模型的好处?
- Q:如何判断两个Class对象是否是一个类?
- Q:不想使用双亲委派模型怎么解决?
- Q:3次打破双亲委派机制的案例?
- Q:如何实现热部署?
- 5、Tomcat的类加载机制?
- 四、JVM调优参数
- 五、JVM调优
- 1、常用的命令行性能监控和故障处理工具?
- 2、JVM可视化的性能监控和故障处理工具有?
- 3、线上服务CPU占用过高怎么排查?
- 4、内存飙升怎么排查?
- 5、频繁的年轻代垃圾回收怎么办?
- 6、频繁的整堆垃圾回收(full GC)怎么办?
- 7、内存泄漏和内存溢出如何排查?
JVM
一、JVM内存区域
1、运行时数据区域
线程私有: 程序计数器、虚拟机栈、本地方法栈
线程共享: 堆、方法区、直接内存(非运行时数据区的一部分)
JDK 1.8同JDK 1.7比,最大的差别就是:元数据区取代了永久代。元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
1、程序计数器
2、java虚拟机栈
Q:栈可能出现的两种错误
3、本地方法栈
4、堆
Q:堆中会出现的错误
Q:堆中对象的分配过程
- new 的对象先放在 Eden 区,大小有限制。
- 如果创建新对象时,Eden 空间填满了,就会触发 Minor GC,将 Eden 不再被其他对象引用的对象进行销毁,再加载新的对象放到 Eden 区,特别注意的是 Survivor 区满了是不会触发 Minor GC 的,而是 Eden 空间填满了,Minor GC 才顺便清理 Survivor 区。
- 将 Eden 中剩余的对象移到 Survivor0 区。
- 再次触发垃圾回收,此时上次 Survivor 下来的,放在 Survivor0 区的,如果没有回收,就会放到 Survivor1 区。
- 再次经历垃圾回收,又会将幸存者重新放回 Survivor0 区,依次类推。
- 默认是 15 次的循环,超过 15 次,则会将幸存者区幸存下来的转去老年区,jvm 参数设置次数 : -XX:MaxTenuringThreshold=N 进行设置。
- 频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集。
5、方法区
Q:堆和栈的区别?
2、为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
1、方法区的常用参数有?
2、运行时常量池
3、字符串常量池
4、jdk1.7 为什么将字符串常量池移到堆中?
5、直接内存
3、对象创建过程
①类加载(判断类是否被加载、解析、初始化过) --> ②分配内存(分配内存的方式) --> ③初始化零值 --> ④设置对象头 --> ⑤执行init方法
Q:JVM创建对象,如何保证线程安全性?
- 采用 CAS分配重试 的方式来保证更新操作的原子性。
- 每个线程 在Java堆中预先分配一小块内存 ,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),要分配内存的线程,先在本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
4、Java内存分配方式
5、Java对象内存布局
Q:对象怎么访问定位?
HotSpot虚拟机主要使用 直接指针 来进行对象访问。
6、JVM中对象及常量、局部变量、全局变量的存储位置
-
局部变量
基本数据类型:变量名和变量值存储在 方法栈 中。
引用数据类型:变量值存储在 方法栈 中(存储的是堆中对象的内存地址),所指向的对象是存储在堆内存中(如new出来的对象)。 -
全局变量
基本数据类型:变量名和变量值存储在 堆 内存中。
引用数据类型:变量名存储的是所引用对象的内存地址,变量名和变量值存储在 堆 内存中。
7、Java虚拟机的生命周期
Java虚拟机:执行Java程序。程序开始执行时它才运行,程序结束时它就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、只接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包或main()方法的类名。 Main()方法是程序的起点,它被执行的线程初始化为程序的初始线程。程序中其他的线程都由它来启动。Java中的线程分为两种:守护线程(daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集GC的线程就是一个守护线程。当然,你也可以把自己的程序用setDeamon设置为守护线程。包含Main()方法的初始线程不是守护线程。只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。
二、JVM 垃圾回收
1、对象死亡的判断方法
2、哪些对象可以作为GC Roots 呢?
3、常见的引用类型(强软弱虚)
Q:如何判断一个常量是废弃变量?如何判断一个类是无用的类?
4、垃圾回收算法
5、垃圾回收器
注意:
- 注重吞吐量以及CPU资源情况下,优先选用Parallel Scavenge和Parallel Old收集器。
- CMS全称Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器。
- G1(garbage frist)优先回收垃圾存放最多的分区,垃圾回收暂停时间短,同时也能维持较好的吞吐量,还解决了CMS的浮动垃圾问题、内存碎片问题。
- 吞吐量:CPU用于运行代码的时间与CPU总消耗时间的比值。
CMS收集器
G1
Q:有了CMS,为什么还要引入G1?
优点: CMS最主要的优点在名字上已经体现出来——并发收集、低停顿。
缺点: CMS同样有三个明显的缺点。
- 标记算法会导致内存碎片比较多。
- CMS的并发能力比较依赖于CPU资源,并发回收时垃圾收集线程可能会抢占用户线程的资源,导致用户程序性能下降。
- 并发清除阶段,用户线程依然在运行,会产生所谓的“浮动垃圾”(Floating Garbage),本次垃圾收集无法处理浮动垃圾,必须到下一次垃圾收集才能处理。如果浮动垃圾太多,会触发新的垃圾回收,导致性能降低。
G1主要解决了内存碎片过多的问题。
Q:垃圾回收器负责的3个任务
①分配内存;②确保被引用对象的内存不被错误的回收;③回收不在被引用的对象的内存空间。
6、Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC 都是什么意思?
- 部分收集 (Partial GC): 指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
1、新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。当Eden区没有足够的空间时,就会触发Young GC来清理新生代。
2、老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
3、混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
- 整堆收集(Full GC): 收集整个Java堆和方法区的垃圾收集。
Q:Minor GC的触发条件?
① eden区满的时候,会触发Minor GC。
② 新创建的对象大小大于Eden区所剩下的空间大小时,会触发Minor GC。
Q:什么时候会触发Full GC ?
Q:对象什么时候进入老年代?
- 长期存活的对象将进入老年代
在对象的对象头信息中存储着对象的迭代年龄,迭代年龄会在每次YoungGC之后对象的移区操作中增加,每一次移区年龄加一。当这个年龄达到15(默认)后,这个对象将会被移入老年代。
可以通过这个参数设置这个年龄值。-XX:MaxTenuringThreshold - 大对象直接进入老年代
有一些占用大量连续内存空间的对象在被加载就会直接进入老年代。这样的大对象一般是一些数组,长字符串之类的。
HotSpot虚拟机提供了这个参数来设置。-XX:PretenureSizeThreshold - 动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在 Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。 - 空间分配担保
假如在Young GC之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。
三、类加载过程
1、类的生命周期
2、类加载过程
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去自定义类加载器去控制字节流的获取方式(重写一个类加载器的loadclass()方法)。数组类型不通过类加载器创建,它由Java虚拟机直接创建。
所有的类都由类加载器加载,加载的作用就是将.class文件加载到内存。
1、加载
2、验证
3、准备
4、解析
5、初始化
Q:初始化开始的时机
6、卸载
注意: 接口加载过程与类加载过程稍有不同。当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
3、Java中的类加载器
JVM类加载器分为两类:JVM自带的类加载器和自定义类加载器。
Q:为什么要自定义类加载器?
① 隔离加载类;② 修改类加载的方式;③ 扩展加载源;④ 防止源码泄漏。
Q:如何实现自定义类加载器?
继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器。
4、双亲委派模型
每一个类都有一个对应它的类加载器。系统中的ClassLoader在协同工作的时候会默认使用双亲委派模型。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的loadClass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器BootstrapClassLoader中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器BootstrapClassLoader作为父类加载器。
Q:双亲委派模型实现步骤?
①首先判断该类是否已经被加载;
②该类未被加载,同时父类不为空,交给父类加载;
③如果父类为空,交给启动类加载器Bootstrap ClassLoader加载;
④如果类还是无法被加载到,则触发findclass,抛出ClassNotFoundException(findclass这个方法当前只有一个语句,就是抛出ClassNotFoundException),如果想自己实现类加载器的话,可以继承ClassLoader后重写findclass方法,加载对应的类)。
Q:双亲委派模型的好处?
双亲委派模型 保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类★★★),也保证了Java的核心API不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的Object类。
Q:如何判断两个Class对象是否是一个类?
在JVM中表示两个Class对象是否为同一个类存在两个必要条件:
- 类的 完整类名必须一致 ,包括包名。
- 加载这个类的类加载器 ClassLoader (指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
Q:不想使用双亲委派模型怎么解决?
自定义加载器的话,需要继承ClassLoader。如果我们 不想打破双亲委派模型 ,就重写ClassLoader类中的findclass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果 想打破双亲委派模型 则需要重写loadClass()方法。
Q:3次打破双亲委派机制的案例?
- 第一次破坏
第一次被破坏发生在双亲委派模型出现之前——即JDK 1.2之前。 - 第二次破坏
第二次被破坏是由这个模型自身的缺陷导致的,如果有基础类型又要调用回用户的代码,那该怎么办呢?如JDBC。 - 第三次破坏
第三次被破坏是由于用户对程序动态性的追求而导致的,例如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
Q:如何实现热部署?
想要实现热部署可以分以下三个步骤:
- 销毁原来的自定义ClassLoader
- 更新class类文件
- 创建新的ClassLoader去加载更新后的class类文件。
到此,一个热部署的功能就这样实现了。
5、Tomcat的类加载机制?
Tomcat破坏了双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。每一个WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
四、JVM调优参数
五、JVM调优
1、常用的命令行性能监控和故障处理工具?
- 操作系统工具
top:显示系统整体资源使用情况。
vmstat:监控内存和 CPU。
iostat:监控 IO 使用。
netstat:监控网络使用。 - JDK性能监控工具
jps:虚拟机进程查看。
jstat:虚拟机运行时信息查看。
jinfo:虚拟机配置查看。
jmap:内存映像(导出)。
jhat:堆转储快照分析。
jstack:Java 堆栈跟踪。
jcmd:实现上面除了jstat外所有命令的功能。
2、JVM可视化的性能监控和故障处理工具有?
JConsole、VisualVM、Java Mission Control(前3个为JDK自带)、MAT(Java堆内存分析工具)、GChisto(GC日志分析工具)、GCViewer(GC日志分析工具)、JProfiler(性能分析工具)、arthas(阿里开源诊断工具)
3、线上服务CPU占用过高怎么排查?
问题分析:CPU高一定是某个程序长期占用了CPU资源。
- 先找出是哪个进程占用CPU高:
top
列出系统各个进程的资源占用情况。 - 然后根据找到对应进程里哪个线程占用CPU高:
top -Hp 进程id
列出对应进程里面的线程占用资源情况。 - 找到对应线程id后,再打印出对应线程的堆栈信息:
printf "%x\n" PID
把线程id转换为16进制。
jstack PID
打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程id对应的线程信息。 - 最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。
查看是否有线程长时间的watting或blocked,如果线程长期处于watting状态下,关注watting on xxxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。
4、内存飙升怎么排查?
分析:内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。
- 先观察垃圾回收的情况
jstat -gc PID 1000
查看GC次数、时间等信息,每隔一秒打印一次。
jmap -histo PID | head -20
查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。
如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。 - 导出堆内存文件快照
jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump
堆内存信息到文件。 - 使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。
5、频繁的年轻代垃圾回收怎么办?
优化Minor GC频繁问题:通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过 增大新生代空间-Xmn来降低 Minor GC的频率 。
6、频繁的整堆垃圾回收(full GC)怎么办?
Full GC的排查思路大概如下:
1. 清楚从程序角度,有哪些原因导致full GC?
· 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
· 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发full GC,最后导致OOM.
· 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发full GC。
· 程序BUG
· 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
· JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。
2. 清楚排查问题时能使用哪些工具?
JDK 的自带工具,包括jmap、jstat等常用命令:
# 查看堆内存各区域的使用率以及GC情况
jstat -gcutil -h20 pid 1000
# 查看堆内存中的存活对象,并按空间排序
jmap -histo pid | head -n20
# dump堆内存文件
jmap -dump:format=b,file=heap pid
可视化的堆内存分析工具:JVisualVM、MAT等
3. 排查指南
· 查看监控,以了解出现问题的时间点以及当前full GC的频率(可对比正常情况看频率是否正常)
· 了解该时间点之前有没有程序上线、基础组件升级等情况。
· 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
· 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
· 针对大对象或者长生命周期对象导致的full GC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
· 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
7、内存泄漏和内存溢出如何排查?
内存泄漏是内在病源,外在病症表现可能有:(内存泄漏可能导致内存溢出,排查步骤和内存泄漏类似)
- 应用程序长时间连续运行时性能严重下降。
- CPU使用率飙升,甚至到100% 。
- 频繁Full GC,各种报警,例如接口超时报警等。
- 应用程序抛出OutOfMemoryError错误。
- 应用程序偶尔会耗尽连接对象。
严重内存泄漏往往伴随频繁的Full GC,所以分析排查内存泄漏问题首先还得从查看Full GC入手。主要有以下操作步骤:
- 使用
jps
查看运行的 Java 进程 ID - 使用
top -p [pid]
查看进程使用 CPU 和 MEM 的情况 - 使用
top -Hp [pid]
查看进程下的所有线程占 CPU 和 MEM 的情况 - 将线程 ID 转换为 16 进制:
printf "%x\n" [pid]
,输出的值就是线程栈信息中的 nid。 - 抓取线程栈:
jstack 29452 > 29452.txt
,可以多抓几次做个对比。 - 使用
jstat -gcutil [pid] 5000 10
每隔 5 秒输出 GC 信息,输出 10 次,查看 YGC 和 Full GC 次数。通常会出现 YGC 不增加或增加缓慢,而 Full GC 增加很快。或使用 jstat -gccause [pid] 5000 ,同样是输出 GC 摘要信息。或使用 jmap -heap [pid] 查看堆的摘要信息,关注老年代内存使用是否达到阀值,若达到阀值就会执行 Full GC。 - 如果发现 Full GC 次数太多,就很大概率存在内存泄漏了
- 使用
jmap -histo:live [pid]
输出每个类的对象数量,内存大小(字节单位)及全限定类名。 - 生成 dump 文件,借助工具分析哪个对象非常多,基本就能定位到问题在那了。
- dump 文件分析
可以使用 jhat 命令分析:jhat -port 8000 29471.dump
,浏览器访问 jhat 服务,端口是 8000。或使用第三方式具分析的,如 JProfiler 也是个图形化工具,GCViewer 工具。Eclipse 或以使用 MAT 工具查看。或使用在线分析平台 GCEasy。 - 在 dump 文析结果中查找存在大量的对象,再查对其的引用。