JVM笔记
# 类加载器的作用是什么?
类加载器(ClassLoader)负责在类加载过程中的字节码获取并加载到内存这一部分。
通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据。
# 类的生命周期
加载-连接(验证-准备-解析)-初始化-使用-卸载
验证:验证内容是否满足《Java虚拟机规范》
准备:给静态变量赋初值
解析:将常量池中的符号引用替换成指向内存的直接引用
以下几种方式会导致类的初始化:
1.访问一个类的静态变量或者静态方法,注意变量是final修饰的并且右边是常量不会触发初始化。
2.调用Class.forName(String className)
3.new一个该类的对象时.4.执行Main方法的当前类。
# 03-初始化 ->会执行静态代码块中的代码,并为静态变量赋值。执行流程与代码流程一致。
几个要点:
1.静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化(除非要执行方法)。
2.直接访问父类的静态变量,不会触发子类的初始化。子类的初始化cinit调用之前,会先调用父类的cinit初始化方法。
添加-XX:+TraceClassLoading 参数可以打印出加载并初始化的类
类的双亲委派机制是什么?
1、当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。
2、应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。
3、双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。
打破双亲委派机制
打破双亲委派机制的第一种方法: 自定义类加载器
自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除
Tomcat通过这种方式实现应用之间类隔离
打破双亲委派机制的第二种方法: 线程上下文类加载器
JDBC案例
1、启动类加载器加载DriverManager.
2、在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
3、SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。
运行时数据区域(JVM管理的内存)
JDK6的方法区(永久代)是存放在堆里面的 字符串常量池在方法区里
JDK7字符串常量池从永久代里面被拆出来 放到了堆上 自己占有一块空间
JDK8永久代不再存在 方法区(元空间)是属于直接内存里边的一块区域 字符串常量池依旧是放在堆里边
垃圾回收(Garbage Collection简称GC)机制。
垃圾回收器主要负责对[堆]上的内存进行回收。
常见的引用类型
强引用,最常见的引用方式,由可达性分析算法来判断
软引用,对象在没有强引用情况下,内存不定时会回收
弱引用,对象在没有强引用情况下,会直接回收
虚引用,通过虚引用知道对象被回收了
终结器引用,对象回收时可以自救,不建议使用
可达性分析算法
GC Root对象:
1线程Thread对象。
2系统类加载器加载的java.lang.Class对象。
3监视器对象,用来保存同步锁synchronized关键字持有的对象。
4本地方法调用时使用的全局对象。
软引用
软引用常用于缓存中
软引用的执行过程如下:
1.将对象使用软引用包装起来, new SoftReference<对象类型>(对象)。
2.内存不足时,虚拟机尝试进行垃圾回收。
3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
4.如果依然内存不足,抛出OutOfMemory异常。
# 软引用中的对象如果在内存不足时回收, SoftReference对象本身也需要被回收。
# 如何知道哪些SoftReference对象需要回收呢?
SoftReference提供了一套队列机制:
1、软引用创建时,通过构造器传入引用队列
2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
3、通过代码遍历引用队列,将SoftReference的强引用删除
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收。
常见的垃圾回收算法
标记-清除算法: 标记之后再清除,容易产生内存碎片
复制算法: 从一块区域复制到另一块区域容易造成只能使用一部分内存
标记-整理算法: 标记之后将存活的对象推到一边对象会移动,效率不高
分代GC: 将内存区域划分成年轻代、幸存者区、老年代进行回收,可以使用多种回收算法
标记清除算法
1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
2.清除阶段,从内存中删除没有被标记也就是非存活对象。
实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
碎片化问题:由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
复制算法
1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。
2.在垃圾回收GC阶段,将From中存活对象复制到To空间。
3.将两块空间的From和To名字互换。
吞吐量高:复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动
不会发生碎片化:复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
内存使用效率低:每次只能让一半的内存空间来为创建对象使用
标记整理算法
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。
1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
内存使用效率高:整个堆内存都可以使用,不会像复制算法只能使用半个堆内存
不会发生碎片化:在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
整理阶段的效率不高:整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过TwoFinger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能
分代GC
分代垃圾回收将整个内存区域划分为年轻代和老年代:
年轻代:存放存活时间比较短的对象 [Eden区-伊甸园 Survivor-幸存区 S0(From) S1(To)]
老年代:存放存活时间比较长的对象
jdk8
-XX:SurvivorRatio 伊甸园区和幸存区的比例,默认为8新生代1g内存,伊甸园区800MB,SO和S1各100MB 比例调整为4的写法:-XX:SurvivorRatio=4
分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC
Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。S0(From) S1(To)
接下来,SO会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。
此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入SO
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1
如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关) ,对象就会被晋升至老年代。
当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC, Full GC会对整个堆进行垃圾回收。
如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
垃圾回收器
垃圾回收器的组合选择如下:
JDK8及之前:ParNew + CMS (关注暂停时间)、Parallel Scavenge + Parallel Old (关注吞吐量)、G1(JDK8之前不建议,较大堆并且关注暂停时间)
JDK9之后:G1(默认)
年轻代-Serial垃圾回收器
Serial是是一种单线程串行回收年轻代的垃圾回收器。
老年代-SerialOld垃圾回收器
SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
年轻代-ParNew垃圾回收器
ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收
老年代-CMS(Concurrent Mark Sweep)垃圾回收器 (特殊情况调用Serial Old) JDK14废除CMS
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
年轻代-Parallel Scavenge垃圾回收器
Parallel Scavenge是JDK8默认的年轻代垃圾回收器多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。
老年代-Parallel Old垃圾回收器
Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。
推荐G1垃圾回收器
JDK9之后默认的垃圾回收器是G1 (Garbage First)垃圾回收器。Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小。CMS关注暂停时间,但是吞吐量方面会下降。
而G1设计目标就是将上述两种垃圾回收器的优点融合:
1.支持巨大的堆空间回收,并有较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。
G1垃圾回收器-内存结构
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。
分为Eden、 Survivor、Old区。 Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M), Region size必须是2的指数幂,取值范围从1M到32M,
G1垃圾回收器-年轻代回收
年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n (默认200) 设置每次垃圾回收时的最大暂停时间毫秒数, G1垃圾回收器会尽可能地保证暂停时间。
参数1: -XX:+UseG1GC打开G1的开关,JDK9之后默认不需要打开
参数2: -XX:MaxGCPauseMillis=毫秒值最大暂停的时间
回收年代和算法: 年轻代+老年代 复制算法
优点: 对比较大的堆如超过6G的堆回收时,延迟可控不会产生内存碎片并发标记的SATB算法效率高
内存泄漏(memory leak) :
在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。
内存溢出
指的是内存的使用量超过了Java虚拟机可以分配的上限,最终产生了内存溢出OutOfMemory的错误。
equals()和hashCode()导致的内存泄漏 解决方案:
1、在定义新实体时,始终重写equals()和hashCode()方法。
2、重写时一定要确定使用了唯一标识去区分不同的对象,比如用户的id等。
3、hashmap使用时尽量使用编号id等数据作为key,不要将整个实体类对象作为key存放。
内部类引用外部类问题
1、非静态的内部类默认会持有外部类,尽管代码上不再使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类。
2、匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者。
解决方案:
1、这个案例中,使用内部类的原因是可以直接获取到外部类中的成员变量值,简化开发。如果不想持有外部类对象,应该使用静态内部类。
2、使用静态方法,可以避免匿名内部类持有调用者对象。
★ ThreadLocal的使用问题
如果仅仅使用手动创建的线程,就算没有调用ThreadLocal的remove方法清理数据,也不会产生内存泄漏。因为当线程被回收时,ThreadLocal也同样被回收。但是如果使用线程池就不一定了。
解决方案:
线程方法执行完,一定要调用ThreadLocal中的remove方法清理对象。
String的intern方法问题
JDK6中字符串常量池位于堆内存中的Perm Gen永久代中,如果不同字符串的intern方法被大量调用,字符串常量池会不停的变大超过永久代内存上限之后就会产生内存溢出问题。
解决方案:
1、注意代码中的逻辑,尽量不要将随机生成的字符串加入字符串常量池
2、增大永久代空间的大小,根据实际的测试/估算结果进行设置-XX:MaxPermSize=256M
★ 通过静态字段保存对象问题
如果大量的数据在静态变量中被长期引用,数据就不会被释放,如果这些数据不再使用,就成为了内存泄漏。
解决方案:
1、尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为null。
2、使用单例模式时,尽量使用懒加载,而不是立即加载。 @Lazy //懒加载
3、Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。
Caffeine.newBuilder()
.expireAfterwrite(Duration.ofMillis(100)) //过期时间
.build();
资源没有正常关闭问题 close
连接和流这些资源会占用内存,如果使用完之后没有关闭,这部分内存不一定会出现内存泄漏,但是会导致close方法不被执行。
解决方案:
1、为了防止出现这类的资源对象泄漏问题,必须在finally块中关闭不再使用的资源。
2、从Java 7 开始,使用try-with-resources语法可以用于自动关闭资源。
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS)){...}
★ 内存溢出 并发请求问题
并发请求问题指的是用户通过发送请求向Java应用获取数据,正常情况下Java应用将数据返回之后,这部分数据就可以在内存中被释放掉。
但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于,内存中,最终超过了内存的上限,导致内存溢出。
这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。
发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\jvm\dump\test1.hprof 指定hprof文件的输出路径。
MAT内存泄漏检测的原理-支配树
MAT提供了称为支配树(Dominator Tree)的对象图。支配树展示的是对象实例间的支配关系。
在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。
MAT内存泄漏检测的原理-深堆和浅堆
支配树中对象本身占用的空间称之为浅堆(Shallow Heap)。
支配树中对象的子树就是所有被该对象支配的内容,这些内容组成了对象的深堆(Retained Heap),也称之为保留集( Retained Set) 。
深堆的大小表示该对象如果可以被回收,能释放多大的内存空间。
分页查询文章接口的内存溢出问题
1、与产品设计人员沟通,限制最大的单次访问条数。
2、分页接口如果只是为了展示文章列表,不需要获取文章内容,可以大大减少对象的大小。
3、在高峰期对微服务进行限流保护。
Mybatis导致的内存溢出
1、限制参数中最大的id个数。
2、将id缓存到redis或者内存缓存中,通过缓存进行校验。
导出大文件内存溢出
Excel文件导出如果使用POI的XSSFWorkbook,在大数据量(几十万)的情况下会占用大量的内存。
解决思路:
1、使用poi的SXSSFWorkbook。
2、hutool提供的BigExcelWriter减少内存开销。
3、使用easy excel,对内存进行了大量的优化。
ThreadLocal使用时占用大量内存
很多微服务会选择在拦截器preHandle方法中去解析请求头中的数据,并放入一些数据到ThreadLocal中方便后续使用。
在拦截器的afterCompletion方法中,必须要将ThreadLocal中的数据清理掉。
文章内容审核接口的内存问题
使用mq消息队列进行处理,由mq来保存文章的数据。发送消息的服务和拉取消息的服务可以是同一个,也可以不是同一个。
在项目中如果要使用异步进行业务处理,或者实现生产者-消费者的模型,如果在Java代码中实现,会占用大量的内存去保存中间数据。
尽量使用Mq消息队列,可以很好地将中间数据单独进行保存,不会占用Java的内存。同时也可以将生产者和消费者拆分成不同的微服务。