一、jvm

目录

一、jvm

1、jvm内存模型

2、说说JVM的双亲委派模型

3、说说JVM调优思路

4、arthas介绍和使用

5、什么是内存溢出

6、什么是内存泄漏?

7、什么是可达性分析算法?

8、垃圾回收器

文章引用


1、jvm内存模型

jvm主要由三部分组成:类加载子系统,运行时数据区,执行引擎;

类加载子系统:通过类加载机制加载类的class文件,如果该类是第一次加载,会执行加载、验证、解析。只负责class文件的加载,至于是否可运行,则由执行引擎决定。

类加载过程是在类加载子系统完成的:加载 --> 链接(验证 --> 准备 --> 解析) --> 初始化

运行时数据区:

在程序运行时,存储程序的内容(例如字节码、对象、参数、返回值等)。运行时数据区包括本地方法栈、虚拟机栈、方法区、堆、程序计数器。

只有方法区和堆是各线程共享的进程内存区域,其他运行区都是每个线程可以独立拥有的。

本地方法栈:存放本地方法调用过程中的栈帧。用于管理本地方法的调用,本地方法是C语言写的。不是所有虚拟机都支持本地方法栈,例如Hotspot虚拟机就是将本地方法栈和虚拟机栈合二为一。栈解决程序的运行问题,即程序如何执行、如何处理数据。
栈帧:栈帧是栈的元素,由三部分组成,即局部变量表(存方法参数和局部变量)、操作数栈(存方法执行过程中的中间结果,或者其他暂存数据)和帧数据区(存方法返回地址、线程引用等附加信息)。


虚拟机栈:存放Java方法调用过程中的栈帧。用于管理Java方法的调用,Java方法是开发时写的Java方法。


方法区:可以看作是一块独立于Java堆的内存空间,方法区是各线程共享的内存区域。所有的类,静态变量,静态方法,成员方法,常量存在这里。


方法区和永久代、元空间的关系:方法区是一个抽象概念,永久代和元空间是方法区的实现方式。
        永久代:属于JVM方法区的内存,用来存储类的元数据,如类名、方法信息、字段信息等一些静态的数据。JDK7及之前方法区也叫永久代。缺点是内存大小固定,容易出现oom问题。可以通过-XX:PermSize设置永久代大小。永久代对象只能通过Major GC(又称Full GC)进行垃圾回收。
        元空间:是Hotspot在JDK8引入的,用于取代永久代。元空间属于本地内存,由操作系统直接管理,不再受JVM管理。同时内存空间可以自动扩容,避免内存溢出。默认情况下元空间可以无限使用本地内存,也可以通过-XX:MetaspaceSize限制内存大小。


常量池:就是一张表,JVM根据这张常量表找到要执行的类信息和方法信息
类常量池:是.class字节码文件中的资源仓库,主要存放字面量(表示字符串值和数值,例如字符串值"abc"、final常量、静态变量)和符号引用(类和接口的全限定名、字段名、方法名)。
运行时常量池:类加载的“加载”阶段会创建运行时常量池,统一存放各个类常量池去重后的符号引用。在类加载的“解析”阶段JVM会把运行时常量池的这些符号引用转为直接引用。类常量池。类常量池在字节码文件中的,运行时常量池在内存中。
字符串常量池:专门针对String类型设计的常量池。是当前应用程序里所有线程共享的,每个jvm只有一个字符串常量池。存储字符串对象的引用。在创建String对象时,JVM会先在字符串常量池寻找是否已存在相同字符串的引用,如果有的话就直接返回引用,没的话就在堆中创建一个对象,然后常量池保存这个引用并返回引用。


堆:存放对象实例、实例变量、数组,包括新生代(伊甸园区、幸存区S0和S1)和老年代。堆是垃圾收集器管理的内存区域。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。堆实际内存空间可以不连续,大小可以选择固定大小或可扩展,堆是各线程共享的内存区域。


程序计数器(PC寄存器):存放下一条字节码指令的地址,由执行引擎读取下一条字节码指令并转为本地机器指令进行执行。是程序控制流(分支、循环、跳转、线程恢复)的指示器,只有它不会抛出OutOfMemoryError。每个线程有自己独立的程序计数器,以便于线程在切换回来时能知道下一条指令是什么。程序计数器生命周期与线程一致。

2、说说JVM的双亲委派模型

JVM三个默认类加载器:

启动类加载器BootStrapClassLoader(最顶端):
加载内容:负责加载java的核心类库,包括java.lang包中的类等。底层使用C++实现(不会继承ClassLoader),是虚拟机自身的一部分。
不能被直接引用:因为是C++实现的,所以无法被Java程序直接引用,只能加载委派过来的请求。这些类库存放在 JAVA_HOME\lib(具体解释看下文) 目录下,或者被 -Xbootclasspath 参数指定的路径中。(启动类加载器主要加载java的核心类库,即加载lib目录下的所有class)


扩展类加载器ExtClassLoader:
加载内容:负责加载JAVA_HOME\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有类库。
可以被直接引用:它可以直接用来加载类,也可以通过委派加载类。Ext是Extract缩写,译为扩展、提取。


应用程序类加载器AppClassLoader(最低端):
加载内容:负责加载类路径的所有类库,在大多数情况下,我们编写的 Java 程序都是由这个类加载器加载的。
可以被直接引用:可以直接在代码中使用这个类加载器。
 

实际流程:

JVM在加载一个类时,会调用应用程序类加载器的loadClass()方法来加载这个类,不过在这方法中,会先使用扩展类加载器的loadClass()方法来加载类,同样扩展类加载器的loadClass()方法中会先使用启动类加载器来加载类;

如果启动类加载器加载到了就直接成功,如果启动类加载器没有加载到,那扩展类加载器就会自己尝试加载该类,如果没有加载到,那么则会由应用程序类加载器来加载这个类。

双亲委派模型的作用:

  • 避免类的重复加载:无论哪一个类加载器要加载某类,最终都是委派最顶端的启动类加载器。
  • 防止核心API被篡改:如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

jvm主要由三部分组成,类加载子系统,运行时数据区,执行引擎。类加载子系统定义了三个类加载器bootstrapClassLoader,extClassLoader,appClassLoader,使用双亲委派机制加载类(即加载类时先由app加载器提请ext加载器,ext加载器提请bootstrap加载器,如果启动类加载器加载到了就直接成功,启动类加载器没有加载到,那扩展类加载器就会自己尝试加载该类,如果没有加载到,那么则会由应用程序类加载器来加载这个类。),加载过程主要有加载-链接(验证,准备,解析)-初始化;第二个重要部分是运行时数据区。包括方法区,堆,本地方法栈,虚拟机栈,程序计数器等,其中方法区和堆各个线程共享,不是线程安全的;所有的类,静态变量,静态方法,常量,成员方法存在方法区中;所有的对象和他们的实例变量,数组存在堆中;所有局部变量,对象的引用(动态链接),方法出口等在栈中;执行引擎执行代码,gc收集线程也在这里。

3、说说JVM调优思路


JVM调优三步骤、性能监控、性能分析、性能调优


监控发现问题:看服务器有没有以下情况,有的话需要调优:

GC频繁
CPU负载过高
OOM
内存泄露
死锁
程序响应时间较长


工具分析问题:使用分析工具定位oom、内存泄漏等问题

  • 调优依据:吞吐量提高的代价是停顿时间拉长。如果应用程序跟用户基本不交互,就优先提升吞吐量。如果应用程序和用户频繁交互,就优先缩短停顿时间。
  • GC日志:使用GCViewer、VisualVM、GCeasy等日志分析工具打印GC日志;
  • JDK自带的命令行调优工具:
    • jps:查看正在运行的 Java 进程。jps -v查看进程启动时的JVM参数;
    • jstat:查看指定进程的 JVM 统计信息。jstat -gc查看堆各分区大小、YGC,FGC次数和时长。如果服务器没有 GUI 图形界面,只提供了纯文本控制台环境,它是运行期定位虚拟机性能问题的首选工具。
    • jinfo:实时查看和修改指定进程的 JVM 配置参数。jinfo -flag查看和修改具体参数。
    • jstack:打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。
      • 线程快照:该进程内每条线程正在执行的方法堆栈的集合。

  • JDK自带的可视化监控工具:例如jconsole、Visual VM。Visual VM可以监视应用程序的 CPU、GC、堆、方法区、线程快照,查看JVM进程、JVM 参数、系统属性。
  • MAT:解析Heap Dump(堆转储)文件dump.hprof,查看GC Roots、引用链、对象信息、类信息、线程信息。可以快速生成内存泄漏报表。
  • eclipse使用MAT下载地址(JDK8对应1.10.0版本)(idea可以使用profiler):Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
  • 生成dump文件方式:
    • jmap:
      • jmap -dump:live,format=b,file=heap_dump.hprof <你的PID>
    • JVM参数:OOM后生成、FGC前生成
    • Visual VM
    • MAT直接从Java进程导出dump文件
// 开启在出现 OOM 错误时生成堆转储文件
-Xmx1024m
-XX:+HeapDumpOnOutOfMemoryError
// 将生成的堆转储文件保存到 /tmp 目录下,并以进程 ID 和时间戳作为文件名
-XX:HeapDumpPath=/tmp/java_%p_%t.hprof
 
// 在进行 Full GC 前生成堆转储文件
// 注:如果没有开启自动 GC,则此参数无效。JDK 9 之后该参数已被删除。
-XX:+HeapDumpBeforeFullGC    


性能调优:

  • 排查大对象和内存泄漏:使用MAT分析堆转储日志中的大对象,看是否合理。大对象会直接进入老年代,导致Full GC频繁。具体排查步骤看下面OOM。
    • 调整JVM参数:主要关注停顿时间和吞吐量,两者不可兼得,提高吞吐量会拉长停顿时间。
    • 减少停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。 可以通过-XX:MaxGCPauseMillis参数进行设置,以毫秒为单位,至少大于1
    • 提高吞吐量:吞吐量=运行时长/(运行时长+GC时长)。通过-XX:GCTimeRatio=n参数进行设置,99的话代表吞吐量为99%, 一般吞吐量不能低于95%。吞吐量太高会拉长停顿时间,造成用户体验下降。
    • 调整堆内存大小:根据程序运行时老年代存活对象大小(记为x)进行调整,整个堆内存大小设置为X的3~4倍。年轻代占堆内存的3/8。
      • -Xms:初始堆内存大小。默认:物理内存小于192MB时,默认为物理内存的1/2;物理内存大192MB且小于128GB时,默认为物理内存的1/4;物理内存大于等于128GB时,都为32GB。
      • -Xmx:最大堆内存大小,建议保持和初始堆内存大小一样。因为从初始堆到最大堆的过程会有一定的性能开销,而且现在内存不是稀缺资源。
      • -Xmn:年轻代大小。JDK官方建议年轻代占整个堆大小空间的3/8左右。
    • 调整堆内存比例:调整伊甸园区和幸存区比例、新生代和老年代比例。Young GC频繁时,我们提高新生代比例和伊甸园区比例。默认情况,伊甸园区:S0:S1=8:1:1,新生代:老年代=1:2。
    • 调整升老年代年龄:JDK8时Young GC默认把15岁的对象移动到老年代。JDK9默认值改为7。当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率。JDK8默认Young GC时将15岁的对象移动到老年代。
    • 调整大对象阈值:Young GC时大对象会不顾年龄直接移动到老年代。当Full GC频繁时,我们关闭或提高大对象阈值,让老年代更迟填满。默认是0,即大对象不会直接在YGC时移到老年代。
    • 调整GC的触发条件:
      • CMS调整老年代触发回收比例:CMS的并发标记和并发清除阶段是用户线程和回收线程并发执行,如果老年代满了再回收会导致用户线程被强制暂停。所以我们修改回收条件为老年代的60%,保证回收时预留足够空间放新对象。CMS默认是老年代68%时触发回收机制。
      • G1调整存活阈值:超过存活阈值的Region,其内对象会被混合回收到老年代。G1回收时也要预留空间给新对象。存活阈值默认85%,即当一个内存区块中存活对象所占比例超过 85% 时,这些对象就会通过 Mixed GC 内存整理并晋升至老年代内存区域。
  • 选择合适的垃圾回收器:最有效的方式是升级,根据CPU核数,升级当前版本支持的最新回收器。
  • CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
  • CPU多核,关注吞吐量 ,那么选择Parallel Scavenge+Parallel Old组合(JDK8默认)。
  • CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择ParNew+CMS,吞吐量降低但是低停顿。
  • CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
  • 优化业务代码:绝大部分问题都出自代码。要尽量减少非必要对象的创建,防止死循环创建对象,防止内存泄漏,有些情景下需要以时间换空间,控制内存使用
  • 增加机器:增加机器,分散节点压力
  • 调整线程池参数:合理设置线程池线程数量
  • 缓存、MQ等中间件优化:使用中间件提高程序效率,比如缓存、消息队列等
JVM参数: 

//调整内存大小
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
-Xms1024m(堆最大大小)
-Xmx1024m(堆默认大小)
-Xmn256m(新生代大小)
-Xss256k(栈最大深度大小)
 
//调整内存比例
 //伊甸园:幸存区
-XX:SurvivorRatio=8(伊甸园:幸存区=8:2)
 //新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
 
//修改垃圾回收器
//设置Serial垃圾收集器(新生代)
-XX:+UseSerialGC
 //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
-XX:+UseParallelOldGC
 //CMS垃圾收集器(老年代)
-XX:+UseConcMarkSweepGC
 //设置G1垃圾收集器
-XX:+UseG1GC
 
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
 -XX:MaxGCPauseMillis
 
 //进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,JDK8默认值15,JDK9默认值7
 -XX:InitialTenuringThreshold=7
 //新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
  -XX:PretenureSizeThreshold=1000000
 
 //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
 -XX:CMSInitiatingOccupancyFraction 
 //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65
 
 //Heap Dump(堆转储)文件
 //当发生OutOfMemoryError错误时,自动生成堆转储文件。
-XX:+HeapDumpOnOutOfMemoryError 
 //错误输出地址
-XX:HeapDumpPath=/Users/a123/IdeaProjects/java-test/logs/dump.hprof
 
 //GC日志
-XX:+PrintGCDetails(打印详细GC日志)
-XX:+PrintGCTimeStamps:打印GC时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps:打印GC时间戳(以日期格式)
-Xlog:gc:(打印gc日志地址)

4、arthas介绍和使用

命令列表 | arthas

//启动arthas
java -jar arthas-boot.jar
//选择应用进程如果进程是[2]: 71560 math-game.jar
2//回车即可
//使用dashboard命令查看当前进程
dashboard
//通过 thread 命令来获取到math-game进程的 Main Class
thread 1
//通过 jad 来反编译 Main Class
jad demo.MathGame

使用dashboard查看线程情况

使用thead-?来查看线程情况

5、什么是内存溢出

内存溢出: 申请的内存大于系统能提供的内存。

溢出原因:

本地直接内存溢出:本地直接内存设的太小导致溢出。设置直接内存最大值-XX:MaxDirectMemorySize,若不指定则默认与Java堆最大值一致。
虚拟机栈和本地方法栈溢出:如果虚拟机的栈内存允许动态扩展,并且方法递归层数太深时,导致扩展栈容量时无法申请到足够内存。
方法区溢出:运行时生成大量动态类时会内存溢出。
CGlib动态代理:CGlib动态代理产生大量类填满了整个方法区(方法区存常量池、类信息、方法信息),直到溢出。CGlib动态代理是在内存中构建子类对象实现对目标对象功能扩展,如果enhancer.setUseCache(false);,即关闭用户缓存,那么每次创建代理对象都是一个新的实例,创建过多就会导致方法区溢出。注意JDK动态代理不会导致方法区溢出。
JSP:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。
堆溢出:

  • 死循环创建过多对象;
  • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  • 内存中加载的数据量过于庞大,如一次从数据库取出的数据集太大、第三方接口接口传输的大对象、接收的MQ消息太大;
  • Tomcat参数设置不当导致OOM:Tomcat会给每个线程创建两个默认4M大小的缓冲区,高并发情况下会导致缓冲区创建过多,导致OOM。
  • 程序计数器不会内存溢出。
  • 使用JDK自带的命令行调优工具 ,判断是否有OOM:

排查方法:

  1. 使用jps命令查看当前Java进程;
  2. 使用jstat命令多次统计GC,比较GC时长占运行时长的比例;
  3. 如果比例超过20%,就代表堆压力已经很大了;
  4. 如果比例超过98%,说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。
  5. MAT定位导致OOM:示例代码:写死循环创建对象,不断添加到list里,导致堆内存溢出;
  6. 导出dump文件。
  7. 方法一:OOM时自动导出:JVM参数设置,内存溢出后生成dump文件,设置路径;-XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath。
  8. 方法二:mat选择线程后手动导出:
  9. 方法三:jmap命令导出:
  10. # 1.查看进程号
  11. jps
  12. # 2.根据进程号导出
  13. jmap -dump:format=b,file=D:\heapdump.hprof <pid>
  14. MAT解析dump文件;
  15. 定位大对象:点击直方图图标(Histogram),对象会按内存大小排序,查看内存占用最大的对象;右键 “List Objects” -> “with outgoing references”,找到特定实例,选择 “Path to GC Roots” -> “Exclude all phantom/weak/soft etc. references”。这将显示从垃圾收集根(GC Roots)到该对象的引用路径。找到路径上的类,分析源代码
  16. 这个对象被谁引用:点击支配树(dominator tree),看大对象被哪个线程调用。这里可以看到是被主线程调用。
  17. 定位具体代码:点击概述图标(thread_overview),看线程的方法调用链和堆栈信息,查看大对象所属类和第几行,定位到具体代码,解决问题。
  18. 解决方案:
    1. 通过jinfo命令查看并修改JVM参数,直接增加内存。如-Xmx256m
    2. 检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
    3. 对代码进行走查和分析,找出可能发生内存溢出的位置。
    4. 使用内存查看工具动态查看内存使用情况。

标准回答

内存溢出,简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。

引起内存溢出的原因有很多种,常见的有以下几种:

1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

3. 代码中存在死循环或循环产生过多重复的对象实体;

4. 使用的第三方软件中的BUG;

5. 启动参数内存值设定的过小。

加分回答

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能。

1. Java堆溢出

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

2. 虚拟机栈和本地方法栈溢出

HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

3. 方法区和运行时常量池溢出

方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这类场景常见的包括:程序使用了CGLib字节码增强和动态语言、大量JSP或动态产生JSP文件的应用、基于OSGi的应用等。 在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,即常量池是方法区的一部分,所以上述问题在常量池中也同样会出现。而HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代,所以上述问题在JDK 8中会得到避免。

4. 本地直接内存溢出

直接内存的容量大小可通过`-XX:MaxDirectMemorySize`参数来指定,如果不去指定,则默认与Java堆最大值一致。如果直接通过反射获取Unsafe实例进行内存分配,并超出了上述的限制时,将会引发OOM异常。

6、什么是内存泄漏?

内存泄漏: 不再使用的对象仍然被引用,导致GC无法回收;

内存泄露的9种情况:

  1. 静态容器里的对象:静态集合类的生命周期与 JVM 程序一致,容器里的对象引用也将一直被引用得不到GC;Java里不准静态方法引用非静态方法也是防止内存泄漏。
  2. 单例对象引用的外部对象:单例模式里,如果单例对象如果持有外部对象的引用,因为单例对象不会被回收,那么这个外部对象也不会被回收
  3. 外部类跟随内部类被引用:内部类持有外部类,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
  4. 数据库、网络、IO等连接忘记关闭:在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。如果对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
  5. 变量作用域不合理:例如一个变量只会在某个方法中使用,却声明为成员变量,并且被使用后没有被赋值为null,将会导致这个变量明明已经没用了,生命周期却还跟对象一致。
  6. HashSet中对象改变哈希值:当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则对象哈希值改变,找不到对应的value。
  7. 缓存引用忘删除:一旦你把对象引用放入到缓存中,他就很容易遗忘,缓存忘了删除,将导致引用一直存在。
  8. 逻辑删除而不是真实删除:监听器和其他回调:如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 软WeakHashMap 中的键。例如出栈只是移动了指针,而没有将出栈的位置赋值null,导致已出栈的位置还存在引用。
  9. 线程池时,ThreadLocal忘记remove():使用线程池的时候,ThreadLocal 需要在使用完线程中的线程变量手动 remove(),否则会内存泄漏。因为线程执行完后没有销毁而是被线程池回收,导致ThreadLocal中的对象不能被自动垃圾回收。 

解决方法:

  1. 性能分析工具判断是否有内存泄漏:
  • JDK自带的命令行调优工具:

每隔一段较长的时间通过jstat命令采样多组 OU(老年代内存量) 的最小值;
如果这些最小值在上涨,说明无法回收对象在不断增加,可能是内存泄漏导致的。

  • MAT监视诊断内存泄漏:(idea可以使用profiler)
  1. 生成堆转储文件:MAT直接从Java进程导出dump文件
  2. 可疑点:查看泄漏怀疑(Leak Suspects),找到内存泄漏可疑点
  3. 可疑线程:可疑点查看详情(Details),找到可疑线程
  4. 定位代码:查看线程调用栈(See stacktrace),找到问题代码的具体位置。
  5. GC详细日志:启动参数开启GC详细日志,设置日志地址;-XX:+PrintGCDetails;
  6. 编译器警告:查看Eclipse等编译器的内存泄漏警告;
  7. Java基准测试工具:分析代码性能; 

牢记内存泄漏的场景,当一个对象不会被使用时,给它的所有引用赋值null,堤防静态容器,记得关闭连接、别用逻辑删除,只要用到了引用,变量的作用域要合理。
使用java.lang.ref包的弱引用WeakReference,下次垃圾收集器工作时被回收。
检查代码;
内存泄漏,是指不再使用的对象仍然被引用,导致垃圾收集器无法回收它们的内存。由于不再使用的对象仍然无法清理,甚至这种情况可能会越积越多,最终导致致命的OutOfMemoryError。

可以按照如下的思路来分析和解决内存泄漏问题:

1. 启用分析器

Java分析器是通过应用程序监视和诊断内存泄漏的工具,它可以分析我们的应用程序内部发生的事情,例如如何分配内存。使用分析器,我们可以比较不同的方法并找到可以最佳利用资源的方式。

2. 启用详细垃圾收集日志

通过启用详细垃圾收集日志,我们可以跟踪GC的详细进度。要启用该功能,我们需要将以下内容添加到JVM的配置当中:`-verbose:gc`。通过这个参数,我们可以看到GC内部发生的细节。

3. 使用引用对象

我们还可以借助java.lang.ref包内置的Java引用对象来规避问题,使用java.lang.ref包,而不是直接引用对象,即使用对象的特殊引用,使得它们可以轻松地被垃圾收集。

4. Eclipse内存泄漏警告

对于JDK1.5以及更高的版本中,Eclipse会在遇到明显的内存泄漏情况时显示警告和错误。因此,在Eclipse中开发时,我们可以定期地访问“问题”选项卡,并更加警惕内存泄漏警告。

5. 基准测试

我们可以通过执行基准测试来衡量和分析Java代码的性能。通过这种方式,我们可以比较执行相同任务的替代方法的性能。这可以帮助我们选择更好的方法,并可以帮助我们节约内存。

6. 代码审查

最后,我们总是采用经典的老方式来进行简单的代码演练。在某些情况下,即使这种看似微不足道的方法也有助于消除一些常见的内存泄漏问题。

加分回答-没有一刀切的解决方案,具体问题具体分析

通俗地说,我们可以将内存泄漏视为一种疾病,它通过阻塞重要的内存资源来降低应用程序的性能。和所有其他疾病一样,如果不治愈,随着时间的推移,它可能导致致命的应用程序崩溃。

内存泄漏很难解决,找到它们需要对Java语言有很深的理解并掌握复杂的命令。在处理内存泄漏时,没有一刀切的解决方案,因为泄漏可能通过各种不同的事件发生。 但是,如果我们采用最佳实践并定期执行严格的代码演练和分析,那么我们就可以将应用程序中内存泄漏的风险降到最低。
 

7、什么是可达性分析算法?

可达性分析算法:

以根对象集合(GC Roots)的每个跟对象为起始点,根据引用关系向下搜索,将所有与GC Roots直接或间接有引用关系的对象在对象头的Mark Word里标记为可达对象,即不需要回收的有引用关系对象。搜索过程所走过的路径称为“引用链” 。

GC Roots:即GC根节点集合,是一组必须活跃的引用。可作为GC Roots的对象:

  • 栈引用的对象:Java方法栈、本地方法栈中的参数引用、局部变量引用、临时变量引用等。临时变量是方法里的中间操作结果。
  • 方法区中常量、静态变量引用的对象;
  • 所有被同步锁持有的对象;
  • 所有线程对象;
  • 所有跨代引用对象;
  • JVM内部的引用:如基本数据类型对应的Class对象,常驻的异常对象,以及应用程序类类加载器; 


加分回答-宣告对象死亡要经历两次标记
真正宣告一个对象死亡,至少要经历两次标记过程:
1. 第一次标记
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。反之,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
2. 第二次标记
稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
    finalize()方法是对象逃脱死亡命运的最后一次机会,需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。

8、垃圾回收器

串行收集器Serial

Serial收集器是最基础、历史最悠久的收集器,使用复制算法,曾经是HotSpot虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。也就是说它在进行垃圾收集时,会发生“Stop The World”

  • 算法:标记复制算法
  • 回收区域:新生代
  • 优点:简单、比其他单线程收集器效率高:单线程,不用线程切换,可以专心进行垃圾回收。
  • 应用场景:适用于内存小的桌面应用,可以在较短时间完成收集。Serial GC是最基础、历史最悠久的收集器,曾是HotSpot虚拟机新生代收集器的唯一选择。
  • 命令:指定新生代用Serial GC,老年代用Serial Old GC:-XX:+UseSerialGC
     

老年代串行收集器Serial Old
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

  • 算法:标记整理算法

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

  • 算法:标记复制算法
  • 回收区域:新生代
  • 优点:多CPU场景下性能高,吞吐量大
  • 缺点:单CPU场景下性能差,不如串行收集器
  • 应用场景:多CPU场景下。

Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

  • 介绍:可控制高吞吐量,多线程并行回收新生代,回收时会STW。比ParNew优秀,可以动态调整内存分配情况。
  • 算法:标记复制算法
  • 回收区域:新生代
  • 应用场景:后台运算量大而不需要太多交互的任务。JDK8默认回收器是Parallel+Parallel Old

Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

  • 介绍:Parallel Scavenge收集器的老年代版本。可控制高吞吐量,多线程并行回收老生代,回收时会STW。
  • 算法:标记整理算法
  • 回收区域:老年代

CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,从名字上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:初始标记、并发标记、重新标记、并发清除。不使用标记整理算法是为了保证清除时不影响用户线程中的工作线程,如果使用标记整理算法的话工作线程引用指向的对象地址就都变了。

  • 回收区域:老年代
  • 步骤:
  • 初始标记:标记GC Roots直接关联的对象。单线程且停顿用户线程,速度很快。
  • 并发标记:从直接关联对象并发遍历整个图,标记可达对象。并发不停顿。
  • 重新标记:修正上一步用户线程变动的标记。并发停顿。速度远比并发标记阶段快。注意只能修正原有对象不能修正新增对象,即只能修正原有对象非可达变可达、可达变非可达。
  • 并发清除:并发线性遍历并清理未被标记的对象。并发不停顿。
  • 优点:
    • 并发速度快;
    • 低停顿:用户线程和垃圾回收器同时执行,仅初始标记和重新标记阶段需要停顿,这两个阶段运行速度很快。
  • 缺点:
    • 并发占线程
    • 有内存碎片:内存不规整,需要维护空闲列表。
    • 无法处理浮动垃圾:并发标记阶段会产生新对象,重新标记阶段又只能修正不能新增,所以会出现浮动垃圾。
    • 回收时要确保用户线程有足够内存:不能等老年代满了再回收,而是内存到达某个阈值后回收,防止用户线程在并发执行过程中新创建对象导致内存不够,导致虚拟机补偿使用Serial Old收集器进行回收并处理内存碎片,从而浪费更多时间。CMS默认是老年代68%时触发回收机制。-XX:CMSInitiatingOccupancyFraction
  • 应用场景:因为底层是标记清除算法,所以有内存碎片,适合小应用。
     

G1

  • Garbage First(简称G1)以延迟可控并保证高吞吐量为目标,为了适应内存大小和处理器数量不断扩大而在JDK7推出的垃圾回收器。开创了收集器面向局部收集的设计思路和基于Region(区域)的内存布局形式。JDK8支持并发类卸载后被Oracle官方称为“全功能的垃圾收集器”。并行低停顿,除了并发标记外需要stw,但耗时很短(初始标记和最终标记是真短,筛选回收是有指定STW)。
  • 实现机制:不再把堆划分为连续的分代,而是将堆内存分割成2048个大小相等的Region,各Region根据需要扮演伊甸园区、幸存区、老年代区、巨大区。垃圾优先收集器跟踪各Region里垃圾的回收价值(回收空间大小和预计回收时长),在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,回收优先级最高的那些Region,以达到垃圾优先的效果。
  • 设置最大停顿时间:-XX:MaxGCPauseMillis=默认0.2s
  • Humongous Region(巨大区):存储大小超过Region一半空间的大对象,如果大对象的内存大小超过了Region大小,将会被存在几个连续的巨大区里。G1的大多数行为把巨大区看作老年代的一部分。
  • 算法:分区收集算法(整体是标记整理算法、Region之间标记复制算法)
  • 回收区域:整堆。整堆里哪个Region垃圾最多,回收收益最大。
  • 步骤:
    • 初始标记:标记GC Roots直接关联的可达对象。单线程且停顿用户线程,速度很快。
    • 并发标记:从直接关联对象并发遍历整个图,标记可达对象。并发不停顿。
    • 最终标记:重新标记所有存活的对象和上个阶段用户线程新产生的可达对象。并发停顿。采用SATB算法,效率比CMS重新标记高。并发停顿。
    • 筛选回收:根据优先级列表,回收价值高的一些Region,将存活对象通过标记复制算法复制到同类型的空闲Region。根据指定的最大停顿时间回收,因此可能来不及回收所有垃圾对象,但能保证回收到最高回收价值的垃圾。并发停顿。
  • 记忆集:是一个抽象概念。每个Region都维护一个记忆集Rset,用来记录其他Region对象对本Region对象的引用。本Region在回收后对象地址会改变,用记忆集就能直接知道直接找到对应引用修改指向的地址,从而不用全局扫描。
  • 卡表(CardTable):是记忆集的一种实现方式。卡表是一个字节数组,每个元素对应一个内存块,每个内存块大小都是2^n字节(Hotspot是2^9=512字节)。
  • 写屏障:当前对象被其他Region对象通过引用关系赋值时,赋值前后会插入写前屏障和写后屏障中断当前Region垃圾回收。
  • CMS的记忆集和写屏障:其他回收器也用到了记忆集和写后屏障,用来防止回收导致位置改变时,不用为了更正引用地址而扫描整个堆。例如CMS记忆集记录老年代指向年轻代的引用。但只有G1用到了写前屏障。
  • 优点:
    • 无内存碎片:因为整体和局部是整理和复制,都不会产生内存碎片。
    • 无浮动垃圾:最终标记阶段不但会修正,也会标记新增对象。
  • 缺点:
    • 比CMS更耗费内存和负载。
    • 可能来不及回收所有垃圾:根据指定的STW时间(默认0.2s)回收,因此可能来不及回收所有垃圾对象,但能保证回收到最高回收价值的垃圾。
    • 比CMS更耗费内存和负载:因为使用写前屏障和写后屏障维护记忆集,而cms只用写后屏障。
  • 应用场景:适合多核CPU且内存大的大应用,小应用不及其他回收器,但未来会越来越适合。

用到的指令

//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65
//指定老年代使用CMS GC:
-XX:+UseConcMarkSweepGC

加分回答
通常,Serial收集器搭配Serial Old使用,ParNew收集器搭配CMS使用,Parallel Scavenge收集器搭配Parallel Old使用。此外,G1是整堆收集器,它无需搭配其他的垃圾收集器。
zgc  目前应用较少可参考新一代垃圾收集器:ZGC深度剖析,到底什么时候用?-CSDN博客

文章引用


————————————————
版权声明:本文部分内容为CSDN博主「程序员小海绵【vincewm】」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40991313/article/details/130232389

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐