一、JVM类加载机制
1、类加载过程
当我们用java命令加载某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
具体步骤:1、调用底层的jvm.dll创建java虚拟机(C++)
2、创建一个引导类加载器(C++)
3、C++调用java代码创建JVM启动器实例com.misc.Lanucher(由引导类加载器加载)
4、获取自己的类加载器并加载
5、加载完成时会执行主类的main方法入口
6、程序运行结束时销毁JVM
2、类加载过程的具体步骤
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main方法,new对象等,在加载阶段会在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存并赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据内存的指针或句柄(指针的指针),这个过程称之为静态链接过程,动态链接是在程序运行期间将符号引用替换为直接引用
- 初始化:对类的静态变量初始化值,执行静态代码块
注意:主类在运行过程中如果引用到其他类会逐步加载,jar、war包里的类不是一次性全部加载的,是使用到时才会加载。
3、JAVA类加载器
- 引导类加载器:负责加载lib目录下的核心类库,如rt.jar、charset.jar
- 扩展类加载器:负责加载lib目录下ext扩展目录中的jar包
- 应用程序类加载器:classPath路径下的,就是加载你自己写的那些类
- 自定义加载器:负责加载自定义路径下的类
4、双亲委派机制
HOW?
- 首先,检查指定类是否已经加载过,如果已经加载过了,就不需要再加载,直接返回。
- 如果没有加载过,判断一下是否有父加载器,如果有,则由父加载器加载
- 如果父加载器都没有找到,则由当前加载器负责加载
WHY?
- 沙箱安全机制:防止核心类库被随意篡改
- 避免类的重复加载:当父加载器已经加载类该类时,子ClassLoader没必要再重新加载
全盘委托机制
“全盘负责”当一个classLoader加载一个类时,除非显示的使用另外一个加载器,否则该类所依赖的引用类也由这个ClassLoader载入。
自定义类加载器
java.lang.ClassLoader类有两个核心方法
loadCLass():实现了双亲委派机制
findClass():默认是空,自定义加载器主要是重写此方法
5、Tomcat几个主要的类加载器
- commonLoader:Tomcat最基本的类加载器,加载的class可以被web容器及各个app所访问
- catalinaLoader:Tomcat私有类加载器,加载路径中的class对于webApp不可见
- sharedLoader:各个app共享的类加载器,加载路径中的class对于所有Webapp可见
- WebappClassLoader:webapp私有的类加载器,比如加载war包里的相关类
6、Tomcat打破双亲委派机制
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,因此要保证每个应用程序的类库都是独立的,是相互隔离的
- 部署在同一个web容器中的相同类库的相同版本可以共享
- web容器也有自己的依赖类库,不能与应用程序的类库混淆
- web容器要支持jsp的热加载,我们知道jsp也是翻译成class文件后执行的,要支持jsp修改后不用重启
二、JVM整体结构及内存模型
关于元空间JVM参数:
XX:MaxMetaspaceSize:设置原空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小
XX:MetaspaceSize:元空间触发FullGc的初始伐值,默认是21M,达到该值就会触发full gc进行卸载,同时收集器会对该值进行调整,如果释放了大量空间就降低该值,如果释放了较少空间就在不超过XX:MaxMetaspaceSize的情况下适当提高该值
由于调整元空间大小需要Full Gc,这是非常昂贵的操作,如果在刚启动时就发生了Full Gc ,通常是由于永久代或元空间的大小发生了调整,一般建议XX:MaxMetaspaceSize和XX:MetaspaceSize 设置成一样的值,并设置的比初始值要大,一般8G物理机内存将这两个值都设置为256M
Xss:设置的count值越小,说明一个线程里能分配的栈帧就越少,但是对JVM来说能开启的线程数就越多
JVM调优:就是尽可能让对象在新生代里完成分配和回收,尽量别让太多对象进入老年代,避免频繁对老年代进行回收,给系统充足的内存大小,避免新生代频繁的进行垃圾回收
三、JVM对象创建及内存分配机制
1、对象的创建
- 类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行类的加载过程。分配内存 - 分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
划分内存的方法:
指针碰撞:Java内存排列是绝对工整的,用过的内存放一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器
空闲列表:内存是不工整的,虚拟机维护一个列表,记录哪些内存是可用的
并发问题的解决办法:
CAS:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理
TLAB(本地线程缓冲):把内存分配的动作划分在不同的空间进行,即每个线程在Java堆中预先分配一小块内存通过XX:+/UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。 - 初始化
内存分配完成后,虚拟机将分配到的内存空间都初始化为零值,如果使用TLAB,这一过程也可提前至TLAB分配时进行,这一步骤保证了实例字段在JAVA代码中可以不赋初始值就直接使用。设置对象头初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息,这些信息存放在对象头之中。 - 对象在内存中的布局
对象头:对象自身的运行时数据,如哈希码,GC分代年龄、线程持有的锁、锁状态标志、偏向线程ID、偏向时间戳。另一部分是类型指针,指向类的元数据的指针,通过这个指针确定是哪一个类的实例。
实例数据:
对齐填充:
什么是java对象的指针压缩?
启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops
为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力2.为了减少64位平台下内存的消耗,启用指针压缩功能3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好
2、对象内存分配
- 内存分配流程
- 对象栈上分配(依赖于逃逸分析和标量替换)
JVM通过逃逸分析确定对象不会被外部访问,如果不会逃逸可以将对象在栈上分配内存,这样该对象锁占用的内存空间就可以随栈帧出栈而销毁,减轻了垃圾回收的压力。
逃逸分析:当一个对象在方法中被定义后,可能被外部方法所引用,JDK7以后会默认开启逃逸分析
标量替换:通过逃逸分析后,确定不会被外部所引用,JVM不会创建该对象,而是将对象分解若干个被这个方法所使用的成员变量,这些代替的成员变量在栈帧或寄存器上分配空间
聚合量:不可被进一步分解的量称之为聚合量,例如java对象 - 对象在Eden区分配
Minor GC/Young GC:新生代垃圾收集动作,minor GC 回收非常频繁,速度也比较快
Full GC/Major GC:回收老年代,年轻代的垃圾,回收速度比Minor GC慢十倍以上
Eden与Survivor区默认8:1:1
注意:当Eden区被分配完了时,虚拟机将发起一次Minor GC,GC期间又发现Survivor区满了,只好把新生代的对象提前移到老年代中去 - 大对象直接进入老年代(避免对大对象内存的复制操作而降低效率)
大对象就是需要大量连续内存空间的对象(比如:字符串,数组)。JVM参数-XX:PretenureSizeThreshold可以设置大对象的大小,超过这个大小会直接进入老年代 - 长期存活的对象将进入老年代
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。 - 老年代空间分配担保机制
年轻代每次minor gc之前,jvm都会计算下老年代的剩余可用空间,如果这个空间小于现有年轻代里所有对象大小之和(包括垃圾对象),就会看 -XX:-HandlePromotionFailure 参数是否设置(jdk1.8默认设置),就会看老年代可用大小是否大于之前每一次minor gc后进入老年代的对象平均大小,如果小于或者没有设置,那么就会触发一次Full GC对老年代和年轻代一起回收,如果还是没有空间就会发生OOM,当然minor gc 后老年代还是没有空间放minor gc 中存活的对象,也会触发 Full GC ,也会发生OOM - 如何判断对象已经死亡
引用计数法:给对象添加一个引用计数器,每当有引用到的地方,计数器+1,失去引用,计数器-1,当计数器为0时,对象已经死亡
可达性分析算法:GC Roots对象作为起点,开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余对象标记为垃圾对象 - 几种常见的引用类型:
强引用:普通的变量引用
public static User user = new User();
如何判断一个类是无用的类?
- 该类所有的实例已经被回收,java堆中不存在类的任何实例
- 加载该类的classLoader已经被回收
- 该类对应的class对象没有在任何地方被引用
四、垃圾收集算法
1、分代收集理论
java堆一般分为年轻代和老年代,这样我们就可以根据各块的特点选择合适的垃圾收集算法
2、复制算法
为了解决效率问题,复制算法出现了,他可以将内存分为大小相同的两块,每次使用其中的一块,当这一块内存使用完以后,将还存活的对象复制到另一块中去,然后把使用的空间一次清理掉,这样每次回收都是堆一半的内存进行回收。
3、标记清除算法
标记存活的对象,统一回收未标记的对象(也可以反过来)
会产生两个问题:1、标记的对象太多,效率不高
2、标记清除后会产生大量的不连续碎片
4、标记整理算法
第一步与标记清除算法一样,后续让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
五、垃圾收集器
1、Serial收集器(-XX:+UseSerialGC-XX:+UseSerialOldGC)
单线程,并且会暂停其他所有线程(STW),新生代采用复制算法,老年代采用标记整理算法。
2、Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
Serial收集器的多线程版本
3、ParNew收集器(-XX:+UseParNewGC)
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
4、CMS收集器(-XX:+UseConcMarkSweepGC(old)) ()
- 初始标记:STW,记录gc roots 直接能引用的对象,速度很快
- 并发标记:是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理
-
并发重置:重置本次GC过程中的标记数据。
-
缺点:对CPU资源敏感(会和服务抢资源);无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
- 核心参数
1. -XX:+UseConcMarkSweepGC:启用cms2. -XX:ConcGCThreads:并发的GC线程数3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值
5、G1收集器(-XX:+UseG1GC)
-
初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
-
并发标记(Concurrent Marking):同CMS的并发标记
-
最终标记(Remark,STW):同CMS的重新标记
-
筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划
6、ZGC收集器(-XX:+UseZGC)
TB级别收集器,不分代
六、常用命令
1、jps:查看所有java进程
2、jmap -histo 15322 (查看内存信息:实例个数、内存大小、类名)
3、jmap -heap 15322 (查看堆信息)
4、jmap‐dump:format=b,file=eureka.hprof 14660(内存很大的时候,可能会导不出来)
5、Jstack 找出占用cpu最高的线程堆栈信息
- top -p 15322 显示java进程的内存情况
- 按H获取每个每个线程的内存情况
- 找到内存和cpu占用最高的线程tid,假设是18929,转换16进制49f1
- 查看对应对应堆栈信息找出可能存在问题的代码
6、Jinfo 查看正在运行java程序的扩展参数
- jinfo -flags 15322 查看jvm参数
- jinfo -sysprops 15322 查看java系统参数
7、Jstat 查看堆内存各部分的使用量以及加载类的数量
- jstat -gc 15322 评估内存使用及GC压力情况
S0C:第一个幸存区的大小,单位KB
S1C:第二个幸存区的大小S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小EC:伊甸园区的大小EU:伊甸园区的使用大小OC:老年代大小OU:老年代使用大小MC:方法区大小(元空间)MU:方法区使用大小CCSC:压缩类空间大小CCSU:压缩类空间使用大小YGC:年轻代垃圾回收次数YGCT:年轻代垃圾回收消耗时间,单位sFGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间,单位sGCT:垃圾回收消耗总时间,单位s - jstat -gccapacity 15322 (堆内存统计)
NGCMN:新生代最小容量NGCMX:新生代最大容量NGC:当前新生代容量S0C:第一个幸存区大小S1C:第二个幸存区的大小EC:伊甸园区的大小OGCMN:老年代最小容量OGCMX:老年代最大容量OGC:当前老年代大小OC:当前老年代大小MCMN:最小元数据容量MCMX:最大元数据容量MC:当前元数据空间大小CCSMN:最小压缩类空间大小CCSMX:最大压缩类空间大小CCSC:当前压缩类空间大小YGC:年轻代gc次数FGC:老年代GC次数 - jstat -gcnew 15322 (新生代垃圾回收统计)
S0C:第一个幸存区的大小S1C:第二个幸存区的大小S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小TT:对象在新生代存活的次数MTT:对象在新生代存活的最大次数DSS:期望的幸存区大小EC:伊甸园区的大小EU:伊甸园区的使用大小YGC:年轻代垃圾回收次数YGCT:年轻代垃圾回收消耗时间 - jstat -gcnewcapacity 15322 (新生代内存统计)
NGCMN:新生代最小容量NGCMX:新生代最大容量NGC:当前新生代容量S0CMX:最大幸存1区大小S0C:当前幸存1区大小S1CMX:最大幸存2区大小S1C:当前幸存2区大小ECMX:最大伊甸园区大小EC:当前伊甸园区大小YGC:年轻代垃圾回收次数FGC:老年代回收次数 - jstat -gcold 15322 (老年代垃圾回收统计)
MC:方法区大小MU:方法区使用大小CCSC:压缩类空间大小CCSU:压缩类空间使用大小OC:老年代大小OU:老年代使用大小YGC:年轻代垃圾回收次数FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间 - jstat -gcoldcapacity 15322 (老年代内存统计)
OGCMN:老年代最小容量
OGCMX:老年代最大容量OGC:当前老年代大小OC:老年代大小YGC:年轻代垃圾回收次数FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间 - jstat -gcmetacapacity 15322 (元空间垃圾回收统计)
MCMN:最小元数据容量
MCMX:最大元数据容量MC:当前元数据空间大小CCSMN:最小压缩类空间大小CCSMX:最大压缩类空间大小CCSC:当前压缩类空间大小YGC:年轻代垃圾回收次数FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间 - jstat -gcutil 15322
S0:幸存1区当前使用比例S1:幸存2区当前使用比例E:伊甸园区使用比例O:老年代使用比例M:元数据区使用比例CCS:压缩使用比例YGC:年轻代垃圾回收次数FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间
七、如何调优?
full gc比minor gc还多的原因有哪些?
1、元空间不足导致频繁full gc
2、显示调用System.gc()造成多余full gc ,-XX:+DisableExplicitGC 参数金庸
3、老年代空间分配担保机制
什么是内存泄漏?
常见的多级缓存架构redis+jvm缓存,很多程序员图方便jvm缓存就只是适用一个hashmap,结果这个缓存map越来越大,一直占用老年代的空间,时间长了就会发生full gc,对于一些老旧数据没有及时清理,时间长了除了会导致full gc 还会导致OOM。这种情况考虑使用一些成熟的JVM框架来解决,如ehcache等自带的LRU数据淘汰算法的框架作为JVM级的缓存
三种字符串操作
String s = “abc”;// s指向常量池中的引用(用equals方法检查常量池中有没有这个常量,直接有返回引用,没有创建一个返回对象引用)
String s1 = new(“abc”);// s1指向内存中的对象引用(equals方法检查常量池中有没有这个常量,没有创建,然后在堆中在创建一个对象,返回引用)
String s2 = s1.intern(); // 如果池中已经包含一个等于s1对象的字符串,则返回池子中的字符串,否则直接指向s1
1 String s0="zhuge"; 2 String s1="zhuge"; 3 String s2="zhu" + "ge"; 4 System.out.println( s0==s1 ); //true 5 System.out.println( s0==s2 ); //true
都是字符串常量,在编译时期就确定了
1 String s0="zhuge"; 2 String s1=new String("zhuge"); 3 String s2="zhu" + new String("ge"); 4 System.out.println( s0==s1 ); // false 5 System.out.println( s0==s2 ); // false 6 System.out.println( s1==s2 ); // false new()创建的字符串不是常量,在编译时期不能确定
1 String a = "a1"; 2 String b = "a" + 1; 3 System.out.println(a == b); // true 4 String a = "atrue"; 5 String b = "a" + "true"; 6 System.out.println(a == b); // true 7 String a = "a3.4"; 8 String b = "a" + 3.4; 10 System.out.println(a == b); // true
1、true、3.4在字符串之后在编译时期就确定为常量
String a = "ab"; String bb = "b"; String b = "a" + bb; System.out.println(a == b); // false bb作为变量在编译时期不确定,在运行时才确定,会生成一个新的对象
String a = "ab"; final String bb = "b"; String b = "a" + bb; System.out.println(a == b); // true final在编译时期被解析为常量
String a = "ab"; final String bb = getBB(); String b = "a" + bb; System.out.println(a == b); // false private static String getBB() { return "b"; } getBB()在编译时期无法确定
String s = "a" + "b" + "c"; //就等价于String s = "abc"; String a = "a"; String b = "b"; String c = "c"; String s1 = a + b + c; // 编译时期ac作为变量不确定