类加载流程
加载->验证->准备->解析—>初始化。解析不是固定顺序,其他步骤都是固定的顺序。
- 加载:获取类的字节流,将类信息存储到方法区,在堆中生成该类的class对象指向类信息
- 验证:
类似编译期的验证
。验证类文件格式、类的版本信息(防止低版本虚拟机加载高版本类字节流)、语义分析比如重载是否合法、是否继承final定义的类等等类似编译期的验证,防止加载了跳过编译器生成的错误的字节码文件。 - 准备:为成员变量赋
初始值
(比如对象引用初始化为空,基本数据类型int初始化为0)、final修饰的变量赋值。 - 解析:解析即将
符号引用
转为直接引用
,包含类引用的转换、类字段引用解析、类方法引用解析、接口、接口方法
,从引用对象的自下往上(本身类找不到则往继承的类或实现的接口找
)查找对应的字段、方法是否存在,且会校验访问权限。解析阶段不是固定的顺序,jvm规范并不要求类在被加载过程就一定要将符号引用
转为直接引用
,只要保证在操作符号引用之前解析就可以(比如引用的类是写在成员变量并new,那么创建当前类时就加载引用类并解析,但是如果是在方法里引用,就可以在调用时再去加载并解析,当然如果类已经被加载过就不会重复加载了,而是可以直接获取到引用类的class对象进行特定的操作了)。需要注意的是解析因为要转为直接引用,因此可能会触发引用类的加载。
比如字段解析,成String a=“11”,会把a指向常量池中的11。 - 初始化:为
成员变量
真正赋值,执行静态代码块,父类先初始化,再初始化子类。(非静态的成员变量创建类时赋值)
类加载过程详细信息
符号引用和直接引用
编译期间引用的类尚未分配内存,因此通过全限定名(包名+类型)
表示引用的类,称为符号引用。在加载类的时候会根据符号引用找到对应的类并分配对象的内存空间,将引用指向对象的内存地址。
类加载器
- 启动类加载器(Bootstrap): c++实现的类加载器,负责加载
jre/lib
目录下的jar(不包含文件夹内的jar),比如rt.jar(lang包、util包等等)。即加载jdk提供的java类。 - 扩展类加载器(Extension):java实现的类加载器(sun.misc.Launcher$
ExtClassLoader
),负责加载jre/lib/ext
目录下的jar包,jdk包中有些默认的扩展的jar,比如sun提供的处理日期的localedata.jar,处理zip的zipfs.jar等等。 - 应用程序类加载器(Application):java实现的类加载器(sun.misc.Launcher$
AppClassLoader
),加载classPath下的类,用户定义的类默认都是通过这个加载器加载的。 - 自定义类加载器
继承ClassLoader,重写findClass方法,父加载器通过loadClass方法加载类,加载不了则会调用子类的findClass加载。 - 线程上下文加载器
java的spi
(Service Provider Interface)模块,提供了很多服务提供者接口,允许由第三方来实现这些接口,比如jdbc(由数据库方提供操作jar,比如mysql的com.mysql.jdbc.Driver)。但是这也导致了Bootstrap加载器需要去加载classPath下的类,因此定义了线程上下文加载器来加载classPath下的类
。线程的上下文加载器可以通过Thread类的setContextClassLoader
方法设置,通过getContextClassLoader获取。如果没有设置这个加载器则从父线程中继承,如果父线程也没有则默认为Application加载器AppClassLoader
。
jvm不会一次加载所有的类,而是需要用到的时候去加载。
双亲委派机制
- 为了避免应用程序编写了和jdk包提供的java类同步同名,当应用程序编写了一个同包同名的类,加载类时会使用双亲委派机制,先由顶层父类处理,无法加载再由子类尝试加载。这样就
避免用户自定义了一个和jdk同包名同类名的类
时,出现加载了不合法的类。一个类的加载首先会由Bootstrap classLoad加载、无法加载的再由Extension classLoad加载,再由Application classLoad加载。 - 类A引用类B的时候,当加载类A时因为需要加载类B,这时会用加载类A的加载器来加载类B,且从父加载器开始尝试加载,
避免一个类被不同的加载器加载,生成重复的字节码
。类加载器加载过一个类后会存在内存中,如果已经加载过不会重复加载.
3.破坏双亲委派机制:
1自定义加载器重写loadClass
方法,使用defineClass方法加载。将需要特殊对待的类自己先处理(比如将特定路径的class读出对象字节流加载到内存),非处理范围的类调用super方法即可。破坏双亲委派机制只是破坏加载顺序,但是preDefineClass方法是final,会做安全性检查,无法定义java开头的包名,还是无法定义jdk自带的类.
Class、classLoad、对象
- java每个类都对应一个class对象,创建某个类的实例时,jvm会先检查内存中是否存在该类的class对象,如果不存在则先通过classLoad加载类,
生成类的class对象,再通过该class对象创建实例
。通过class对象可以获得该类的加载器,而类加载器也会通过一个集合存储加载的类的引用。 - 因此关联关系为:对象->class对象-><-classLoader
- class对象也是存在堆中的。
静态变量,静态方法jdk8之后都存储在class对象中,即存在堆中
。class对象作为类信息的访问入口。
类卸载
一个类的卸载与该类的class对象什么时候被释放相关,而class对象除了与对象实例关联还与类加载器关联,三种类加载器在jvm运行期间是不会卸载的,如果不是通过自定义的类加载器加载的类,是不会被卸载的。只有类加载器没有被引用、该类没有对象实例被引用、该类的class对象没有被引用
,类才会卸载。
静态代码块和构造代码块
- 执行顺序:静态代码块>构造代码块>构造函数,静态代码块只有第一次加载类时才执行。
- 如果类包含父类且都包含静态代码块、构造代码块
执行顺序为:父类静态代码块>子类静态代码块>父类构造代码块>父类构造函数>子类构造代码块>子类构造函数
Java内存划分
堆
- 新生代、老年代,默认比例为
新生代:老年代=1:2
。线程共享
- 新生代分为
eden
和survivor
,survivor分为from和to
,默认比例为eden:from:to=8:1:1
。 - 堆存储对象本身,成员变量本身、数组。
大对象直接进老年代
,通过-XX:PretenureSizeThreshold控制直接进入老年代的对象大小(ParNew、Serial收集器才有效),默认为0
即都直接现在新生代分配,除非对象大小超过eden区域大小
也会直接分配到老年代。class对象存储在堆。
- 默认初始大小物理内存的64分之一,最大大小物理内存4分之一。
栈
- 执行一个方法会创建一个栈帧,包含方法返回地址,局部变量表,引用,操作数栈(运算通过这个栈进行运算)。
线程私有
- 默认大小1M.局部变量表、操作数栈占用越多空间,栈的深度就越小。没有任何参数的空方法默认深度在1W左右。
元空间
- jdk8之后方法区被移除,方法区存储
静态变量、常量池、类元数据
,而元空间只存储类元数据
。静态变量、常量池
存在堆中。之所以这么干是因为方法区本身就只是要存储一些静态数据
的,但是因为静态变量(随着运行不断加载新的类,新的类包含静态变量
)和常量(string intern方法
)是运行期间可以变的,迁移到堆中就可以避免方法区溢出
了。 - 元空间是独立的空间,默认系统根据机器内存情况调整,即只受机器内存限制,设定MaxMetaspaceSize参数可以限制最大大小,-1表示无限制。初始大小固定为21807104字节,约20.8m。由于元空间只存储类信息,所以就避免了运行期报永久代oom错误。
既然只是存类信息,为什么限制元空间大小?因为字节码增强技术的存在,比如spring使用cglib增强类,不断生成新的类,就要加载更多的类的类型信息到元空间,所以如果不限制,可能导致本地内存爆炸。
- 元空间的回收,只有类卸载了才会回收这个类的元数据。
线程共享
方法区存在堆中,元空间是堆外内存。
永久代是hotspot对方法区的实现。
类元数据
包含了类的全限定名、类的直接超类的全限定名、该类是类还是接口、该类型的访问修饰符、直接超接口的全限定名的有序列表。字段信息、方法信息。
程序计数器
记录线程执行到程序的哪个位置.线程私有
本地方法栈
本地方法使用的栈空间,与栈功能类似。不同虚拟机不同存放方式,可能与栈是同个空间.线程私有
常量池
常量池存储的内容
常量池中包含字面量
(基本数据类型的值,字符串值,final修饰的常量(基本数据类型),符号引用
(全限定名标识类,方法名称和描述符、字段名称和描述符)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ozNrJg8m-1585712501305)(evernotecid://EA56B0F8-13B5-4B11-8191-1E827D9FC2A6/appyinxiangcom/22897236/ENResource/p37)]
常量池分为分为静态常量池和运行时常量池。
- 静态常量池。指的是class文件中包含一个常量池,即定义的一个区域存储字面量和符号引用。
- 运行时常量池。即jvm加载类之后,把class文件中的常量池加载到内存中。
存储的区域
在jdk8以前常量池存在方法区中,jdk8后常量池存在堆中。之所以这么干是因为方法区本身就只是要存储一些静态数据
的,但是因为静态变量(随着运行不断加载新的类,新的类包含静态变量
)和常量(string intern方法
)是运行期间可以变的,迁移到堆中就可以避免永久代溢出
了。
垃圾回收
如果某个字符串常量没有被任意一个存活的对象引用,jvm在判断有必要回收的情况下,也会回收。jvm规范并没有要求jvm实现方一定要回收这部分内存。
字符串常量池stringpool
前面说常量池包含字面量,字面量中的字符串在jvm中有个单独存储字符串的缓存池。
编译时就确定的字符串,会在编译后加入到静态常量池
中,而运行时通过变量或外部输入
的字符串(即编译期无法确定的)则不会经过常量池。如果希望可以获取到常量池中的常量,可以通过intern()
方法拿到常量池中的对象,常量池中没有则将当前常量加到常量池中。如下例子。
String s1 = "sss";
Scanner scanner = new Scanner(System.in);
String s2 = scanner.next();
System.out.println(s1 == s2); System.out.println(s2.intern()== s1);
// 分别输出 false ture
字符串池大小可以通过-XX:StringTableSize
参数指定。
字符串的常量池是jvm层面的实现,在java类中没有相关代码。
Integer常量池
Integer在类中有一个IntegerCache
类,用static代码块初始化了-128 ~ 127
范围Integer
对象,在将int类型装箱时会判断是否在这个范围类,如果有则会从该类层面的缓存池中拿对应的对象。
缓存池最大值可以通过jvm参数-XX:AutoBoxCacheMax
指定。
Long类型常量池
与Integer类 类似,有个LongCache
类,也是缓存了-128 ~ 127
的类缓存。
垃圾回收机制
确定对象可回收的方式
- 引用计数法,通过记录对象引用的个数,当对象没有被引用时即为可回收。
无法解决两个垃圾对象相互引用的问题
。(比如在一个方法中创建两个对象,两个对象之间是相互引用的,方法执行结束后,两个对象应该被清除的) - 可达性分析,jvm采用这个方式进行确认是否可以回收。根引用集合GC roots,判断是否可达会从某个gc root为起点,往下查找。通过分析对象是否有被GC roots包含的引用所引用进行判断对象是否存活。通过这个可达性分析
解决了两个没有被gc root引用的两个垃圾对象相互引用的问题,因为从gc root作为起点是无法到达这两个垃圾对象的
。
可以作为gc root
的对象如下
- 在
虚拟机栈
(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。 - 在方法区中
类静态属性
引用的对象,譬如Java类的引用类型静态变量。 - 在方法区中
常量引用
的对象,譬如字符串常量池
(String Table)里的引用。 - 在
本地方法栈
中JNI(即通常所说的Native方法)引用的对象。 - Java虚拟机内部的引用,如
基本数据类型对应的Class对象
,一些常驻的异常对象
(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器classLoader。 - 所有被
同步锁
(synchronized关键字)持有的对象。 - 反映
Java虚拟机内部
情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 - 被标记一次后在队列中等待执行finalize的对象
- 有finalize方法但是没在队列中的对象
finalize
- 确认不可达后,对象不是就立即可回收,对象被标记之后会判断对象是否实现了finalize方法,如果没有实现则该对象可以直接回收。如果实现了finalize方法,则该对象会被放到一个队列中,由一个
低优先级的线程
执行对象的finalize
方法,可以通过重写finalize让对象在被回收时重新被引用。垃圾回收线程在重标记时,如果该对象没有被finalize拯救,则会变成可回收对象。 - finalize只会执行一次,第二次回收该对象不会重复执行。
- finalize是
不可靠的
,因为是由一个低优先级线程去执行的,与垃圾回收线程是并行执行的,可能还没执行就被二次标记回收了。
守护线程
执行gc的线程便是属于守护线程。Thread.setDaemon()可以将一个线程设置为守护线程.
java程序执行结束后,会触发jvm退出。会等待进行非守护线程
执行完再退出jvm,不会等待守护线程。所以守护线程执行方法会有没执行完就被中断
的风险,finally方法也不会执行。
System.gc()
不能保证一定会执行gc,只是建议虚拟机进行一次gc,如果出发gc会触发fullgc。运行时指定-XX:+DisableExplicitGC可以让System.gc()失效。
Gc算法
标记清除 Mark-Sweep
。标记存活的对象,标记结束后,把未存活的对象都清除掉。会产生内存碎片
。标记复制 Copying
。标记存活的对象,复制对象别的空间,将原空间的对象清除,不会存在内存碎片
,缺点时要浪费一半的空间用于复制,但是效率高。新生代的垃圾回收采用这个算法
,因为新生代大部分的对象都会被回收,只有少部分对象回存活,只需要复制少量的对象。空间换时间。标记整理 Mark-Compact
。将存活的对象标记后移到内存的一端,将未标记的对象清除,避免了内存碎片。老年代采用这个算法进行垃圾回收,老年代每次回收的对象少,如果采用复制算法会浪费很大的空间。
堆中不同位置的gc
minor gc
: 即young gc,触发新生代的垃圾回收。
minor gc触发时机:eden区域满
的时候触发。
Full gc
: 触发新生代和老年代的垃圾回收。
full gc触发时机:
- 当要进行minor gc前,判断
之前gc晋升对象平均大小超过老年代剩余的空间
,如果超过就出发full gc而不会执行minor gc,因为full gc已经包含了年轻代的gc。 - 如果存在永久代,
永久代空间不足
时会触发。 System.gc()
会触发。大对象超过eden区大小,直接进入老年代
,老年代空间不足时。
major gc
: 有两种说法,一种是触发老年代的垃圾回收,一种是与full gc等价。
为什么要分代回收
有些常驻对象可能一直回存活,如果不分代那所有的对象每次都要扫描一次,包括常驻对象,降低了效率。并且gc时回触发gc停顿,分代回收提升gc效率也是降低停顿时间。
90%的对象熬不过第一次垃圾回收,而老的对象(经历了好几次垃圾回收的对象)则有98%的概率会一直活下来。
gc流程
对象先在eden区域
分配,进过一次minor gc
,会将eden区域存活的对象复制survivor的to区域
,同时也会把survivor的from区域存活的对象复制到to区域
,复制后to区域会变成from区域
,继续等待下次gc。同时对象每经过一次gc没有被回收,则年龄会加1。当对象年龄超过15则会进入老年代
。或者当Survivor区域年龄1+2+…+n的对象总和超过Survivor区域大小的一半,则n岁以上的会进入老年代。或者对象超过了eden区域的大小也会直接进入老年代。或者当minor gc后,存活的对象过多超过了survivor to区域的大小,也会进入老年代。
Gc停顿Stop-The-World stw
gc标记之前要等待jvm所有的线程挂起后才能执行,这个是时间点也叫安全点safepoint
。在这个时间点上所有线程都会暂停执行,这时gc才能够安全
的标记对象。jvm实现的原理是通过在方法返回前、抛出异常的位置、循环的末尾等位置插入标记,线程在运行到这些标记的位置时,会检查当前时候需要停顿,如果需要则线程停顿。
执行gc的线程
VM Thread,该线程维护了一个线程队列,记录了jvm所有线程和线程状态,当所有线程都处于停顿点时,开始gc,gc结束后再唤醒线程。
Gc收集器
- Serial是
单线程
进行垃圾回收,效率低,其他收集器都多线程回收
垃圾。 - 每个收集器都需要stop the world,但是
cms可以跟用户线程并行执行,分多阶段收集,停顿时间是最低的
。 - 除了cms收集器,其他都遵循新生代用复制算法、老年代用整理算法。
cms
收集器采用标记清除算法
。 - 串行收集器适合小数据量、cpu是单核的情况
- 并行收集器指的是多线程进行垃圾回收,但是用户线程会处于停顿暂停状态。并发收集器是用户线程和垃圾回收的多线程同时执行,不会暂停。
吞吐量优先则采用并行收集器(对象多、多线程垃圾回收,停顿时间偏长)、响应时间优先则采用并发收集器(对象相对少、gc停顿低)
Serial 新生代收集器。
串行的单线程垃圾回收器,只适合单cpu单核的机器使用,避免线程上下文切换。
ParNew 新生代收集器。
Serial的多线程版本。
Parallel Scavenge
jdk8默认
,新生代收集器,同样是多线程收集。与ParNew的区别是Parallel Scavenge是更加关注吞吐量(用户代码执行时间/用户代码执行时间+gc停顿时间)
,可以通过-XX:MaxGCPauseMillis
控制最大停顿时间,通过-XX:GCTimeRatio
控制吞吐量,该值为1-99,默认为99。停顿时间不是越低越好
,调低则会降低新生代的空间,即通过减少空间降低收集时间
,但是却导致收集次数变多。-XX:+UseAdaptiveSizePolicy
通过此参数该收集器会自动调节
新生代大小、晋升老年代年龄等参数以保证达到设置的吞吐量。
cms(Concurrent Mark & Sweep)
老年代
收集器,多线程收集,优点是停顿时间短
,并发标记和清除阶段没有stw。
分4阶段:
初始标记阶段:GC roots直接关联的对象(存在stw
)。
并发标记阶段:与用户线程同时执行(占用了部分线程,实际上也会影响用户线程的执行效率),通过标记的gc root标记引用的链。可能会出现没有引用的对象又被引用,所以会监控这些引用修改,已经标记不会取消标记
。
最终确认阶段:合并标记结果,由于标记的对象可能会已经在程序运行中被引用,所以要把标记取消。存在stw
。
并发清除数据阶段(与用户线程并行)。
优点:并行、停顿时间长。
缺点:
- cms之所以使用
清除算法
,就是为了可以实现与用户线程并行,其他算法需要移动对象内存位置,只有清除算法可以不影响
,但是同时也造成了内存碎片
。内存碎片多,会导致内存还有空间,但是就不够分配而触发gc.可以通过一个开关开启,会进行内存碎片压缩,标记整理,但是也会造成stw时间变长。 - 存在浮动垃圾。(第一阶段标记后,新产生的垃圾不会被回收,要等下一次gc)
- 由于清除和用户线程并行,因而也需要更大的内存空间,因为在清除的过程中会有新的对象在进来。可以通过设置jvm参数-XX:CMSInitiatingOccupancyFraction控制堆利用率达到多少就触发gc(0-100)。
- 占用cpu较高。
Serial old
老年代收集器。Serial单线程处理
Parallel old
jdk8默认
,老年代收集器。Parallel Scavenge的老年代收集器,多线程处理。
g1 Garbage First(优先处理垃圾多的内存块)
jdk7后出现,jdk9默认
。收集流程与cms类似,初始标记、并发标记、最终标记、清除(选择一些区域清除
)。不同是g1只有并发标记没有stw.
特点
stw时间短,区域分块、可以比较有效的避免内存碎片(因为每次操作的区域都是一小块,进行内存压缩代价低)。
分代回收内存分为eden、survivor、old,g1将这三种类型的类型分为一个个块,内存块粒度变小,更好的并行。g1几乎整个收集全程都是并行的。
收集新生代和老年代范围,将每个区域划分成多个region区域,且在该收集器下新生代和老年代不再是固定的区域,而是分布在整个区域中的各个region。
新生代采用标记复制
需要完全stw。老年代也是分阶段清理,优点类似cms,但是因为分区域处理,还涉及到计算区域存活率。与cms的区别是并发标记阶段效率更高
.
清除阶段因为使用的是标记整理
,会存在stw。
参数设置
使用g1收集器通过增加参数
-XX:+UseG1GC,设置最大堆内存-Xmx32g
,
-XX:MaxGCPauseMillis=200,设置最大停顿时间(g1只能尽量保证,但是无法确保一定在这个时间内,
g1为什么分区域收集?为什么g1可以指定停顿时间?
分区域可以更好控制回收多久。根据用户配置的stw时间控制每次只回收某些块,而不是像其他收集器是整个堆进行回收。其实也是空间换时间。
缺点
比较适合内存大的应用,如果内存小,每次仅仅回收部分内存还是不够用,还是要进行整个堆的回收。而且本身算法比较复杂,可能效率还会更差。因此如果内存比较小还是建议使用其他回收器。
总结
分治法,分而治之。G1把几块大内存块的回收问题变成了几百块小内存的回收问题,使得回收算法可以高度并行化,同时也因为分成很多小块,使得垃圾回收的单位变成了小块内存,而不是整代内存,使得用户可能对回收时间进行配置,垃圾回收变得可以预期了。一般延迟在0.5-1s
如果停顿时间要求比较严格,当前也没有很长时间的stw,没必要换g1.
内存泄露
内存泄露指的是一个不再被程序使用的对象或变量一直被占据在内存中。出现内存泄露的情况:1. 在方法中创建成员变量引用的对象
,导致方法出栈后,引用的对象不会随着方法结束而回收。解决方法是方法内创建对象,引用用使用局部变量。2. 把对象加入到集合中
,将对象引用置空也无法回收对象,因为会被集合占用,要把集合的引用也去除。
为什么可达性分析还会存在内存泄漏?
因为主观上这些对象是可达的,只是从客观角度,这些对象可能是不会再去访问的,造成没必要的内存占用。
Jvm参数
-Xms 最小堆容量
-Xmx 最大堆容量
-Xss 设置线程的栈大小,默认是1m,在同等物理内存大小下,将这个值改小可以创建更多线程。
-Xmn 新生代的大小。年轻代大小+老年代=堆大小。
-XX:NewRatio。设置年轻代(包括Eden和两个Survivor区)与年老代的比值,官方建议配置比例新生代:年老代=3:5,默认的比例是1:2
-XX:MaxMetaspaceSize 设定元空间最大大小
-XX:MetaspaceSize 设置元空间扩容时,触发fullgc的乏值
-XX:SurvivorRatio eden和survivor的比例
-XX:PretenureSizeThreshold 调节对象直接进入老年代的对象大小阈值
-XX:MaxTenuringThreshold 调节对象从年轻代晋升老年代的年龄阈值。老年代对象较多的程序可以设置小些。
-XX:+HeapDumpOnOutOfMemoryError 内存溢出时打出堆栈信息(dump文件可以通过Visual VM、IBM HeapAnalyzer、JDK 自带的Hprof工具、Mat查看)
除了xms,xmx外其他参数配置要谨慎配置,如果影响了收集器动态扩展,反而导致更多问题。
收集器
-XX:+UseParallelGC 设置年轻代的垃圾收集器为并行收集器.
-XX:+UseParallelOldGC 设置年老代的垃圾收集器为并行收集器
-XX:ParallelGCThreads 设置并行收集器收集时使用的CPU数.并行收集线程数.
-XX:MaxGCPauseMillis 设置并行收集最大暂停时间
-XX:CMSFullGCsBeforeCompaction 并发收集器不压缩、整理内存空间,会有内存碎片,可以设置多少次gc后压缩整理内存空间。
-XX:+UseCMSCompactAtFullCollection 打开年老代压缩
-XX:+UseG1GC 使用G1收集器
-XX:G1HeapRegionSize=n 设置g1 Region大小,最小1m,最大32m。超过Region百分之50大小的对象会导致内存碎片、导致性能问题。可以适量调大。
jvm调优
堆大小设置
xmx微服务一般测试环境设置128m、256m,线上设置512m、1g。具体可以通过观察老年代的内存使用情况,如果长期超过50%或者达到80% 90%,那么久设置少了,可以调高。观察gc次数、如果不到1分钟就gc一次,就属于不正常了。
xms最好与xmx设置相同大小,避免每次垃圾回收后重新分配内存。
大小设置,要考虑如果设置太大会导致每次gc可能会回收的对象多,gc停顿长。设置太小会导致频繁gc,甚至内存溢出。
年轻代
响应时间优先
什么时候需要调优
cpu占用高、gc频繁、gc时间长
调优思路
观察gc情况,让gc回收尽可能多的对象,避免频繁gc,避免gc时间过长,减少gc停顿时间。
年轻代:如果频繁gc,但是每次回收、回收的对象并不多,则可以适当调大年轻代的大小。
年老代:full gc频繁,如果每次full gc年老代内存占用率并没有下降,则要考虑是否有内存泄露,如果full gc后内存占用下来了,则考虑调大年老代。
性能指标:吞吐量(单位时间内响应的请求数)、响应时间(单词请求的响应速度)
jvm监控命令
jstat
参考链接
jstat -gc pid 1000 10
看gc情况 1000表示1秒展示一次,10表示最多展示10次
其他参考深入理解java虚拟机图:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-doFCglhY-1585712501308)(evernotecid://EA56B0F8-13B5-4B11-8191-1E827D9FC2A6/appyinxiangcom/22897236/ENResource/p26)]
jps
jps -l 看启动的java项目pid和启动类名
jps -v 看jvm参数
jps -m 看传给主类的参数
jmap
内存映射工具
jmap -heap pid 看整体堆信息
jmap -histo pid 看所有对象所占内存情况和对象数量
jmap -dump:live,format=b,file=a.log pid 导出内存使用详细情况到a.log文件
jhat(不推荐)
jhat a.log 看dump文件,自带Http服务器,运行后可在浏览器上查看,不是很清晰
jstack
堆栈跟踪,查看线程快照
jstack pid
-l 附加关于锁的信息
-m 查看navtive方法堆栈
-F正常请求无响应时,使用这个强制输出
参考链接
排查死锁
排查死锁,通过jstack命令,如果有死锁会
显示dead lock,或者通过jconsole监控查看
java visualVM
jdk bin目录下有个java visualVM 可以看dump文件和监控jvm运行情况,包含cpu、内存、线程情况等
引用强度
- 强引用,只要引用存在,对象就不会回收。普通创建对象的方式就是强引用。
- 软引用。如下通过SoftReference包含引用,
当垃圾回收后依然内存不足时会被回收,即oom前的一次解救方式,释放这些引用的对象
,可以用于做本地缓存之类的场景,空间足够当做缓存用,空间不够就被清掉,下次用到再获取。
SoftReference<String> softRef=new SoftReference<String>(new String("c"));
- 弱引用WeakReference,每次垃圾回收都会回收该应引用,不管是否内存充足。
- 虚引用PhantomReference,必须与引用队列ReferenceQueue联合使用,对象被回收后引用会加入到引用队列。通过引用队列可以得知对象已被回收。虚引用任何时刻都可能被回收,主要可以用于追踪垃圾回收情况的。
扩展图、来源网络
jdk8堆内存划分
jdk8内存划分
垃圾回收器
g1收集器
扩展引用、来源网络
阿里垃圾回收讲解
https://zhuanlan.zhihu.com/p/73628158
gc分类
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
Partial GC:并不收集整个GC堆的模式
- Young GC:只收集young gen的GC
- Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
- Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
jvm调优
https://yq.aliyun.com/articles/637890
https://yq.aliyun.com/articles/737508?spm=a2c4e.11153940.0.0.7cba70b8rkY6Hl