.class文件什么情况下会被加载?
JVM启动进程后,会寻找程序入口,包含Main方法的类,Main方法执行过程里用到哪些类就加载哪些类
正常的war程序如何加载的? Tomcat本身是一个java程序,Web程序只是他下面的子程序,目前不知道他的加载机制,Tomcat搞一个Main方法作为程序入口也不是不可以
.class文件加载的过程
加载主要分成3个部分
-
加载
将静态文件数据加载到方法区,同时在堆内存生成一个代表对应类的Class对象
-
链接
-
验证
确保Class文件内容符合java规范
-
准备
为类变量分配内存、并为其设置初始值 public static int x = 123 在准备阶段过后,x的 值是0而非123。真正的赋值操作是在这个类初始化时进行
-
解析
将运行时常量池中的符号引用替换为直接引用,类编译时期是不知道他所依赖的类的具体内存位置的,所以用符号来代表引用关系,直接引用就是真实地址,"书里对这快的描述是JVM没有限制解析阶段具体的触发时间,只是规定了在某些操作符号引用的字节码指令之前,先对他们所使用的符号进行解析 "
-
-
初始化
1)父类——静态变量
2)父类——静态代码块
3)子类——静态变量
4)子类——静态代码块
5)父类——非静态变量
6)父类——非静态代码块
7)父类——构造器
8)子类——非静态变量
9)子类——非静态代码块
10)子类——构造器
-
卸载
class文件被加载到元空间,当满足以下3个条件时表示他可被GC
类加载器对象已经被回收
Class对象不存在引用关系
没有该Class对应的实例对象
类的加载是基于双亲委派模型,双亲委派模型的出发点/好处
启动类——加载器
扩展类——加载器
应用程序类——加载器
自定义——加载器
首先要说明,同一个class文件被不同类加载器加载,这两个类是不相同的
双亲模型保证了JDK基础类只能被特定类加载器加载且只能加载一次,这样就不会出现用户在ClassPath下写一个Object类被加载,导致应用出现多个Object类致使程序混乱
既然启动类加载器是加载lib包下的类,写一个类扔里面会不会被加载?
常见的类加载器?
为什么说Tomcat破坏了双亲委派模型
首先说双亲委派模型要求类的加载要先交由父加载器去做
容器有几个特点
要加载相同jar包的不同版本,默认加载机制,仅根据类名判断是否加载成功,那是做不到这点
JSP要动态生效,默认加载机制,JSP改了内容但类名没变不会再次加载
Tomcat自定义了加载器,针对这种隔离情况做了特殊处理,即自己加载不向父加载器抛
内存区域划分
-
程序计数器 【线程私有】
用于记录每个线程执行到了哪行代码
-
虚拟机栈 【线程私有】
生命周期与线程相同,方法被执行时会同时创建一个栈帧,用于存储
局部变量表(指向堆中的对象)
操作数栈
动态连接(方法调方法)
方法出口,即返回地址及返回方法调用的位置
-
本地方法栈
-
方法区
1.8后方法区从堆中移除,放到了叫元空间MetaSpace,元空间在本地内存,不再受堆空间大小限制,存储类元信息
验证
循环cglib动态生成类
1.7 java.lang.OutOfMemoryError : MetaSpace space
1.8 java.lang.OutOfMemoryError : PermGen space
-
堆
对象、字符串常量池、静态变量
验证
字符串常量池
循环拼接字符串
静态变量
定义大的静态数组 private static Long array_1… = new Long(9999999);
最终都是堆溢出 java.lang.OutOfMemoryError : Java heap space
新生代
分为一个Eden和两个Survivor(From、To),流转过程:
创建对象分配内存时,发现Eden区快要满了,触发Minor GC,Eden区和From中存活的对象放入To,
清空Eden、From,同时将From、To对调
老年代
进入老年代的情况
1、对象年龄达到15后
2、GC过后Survivor区不够存,存活对象直接全放到老年代
3、大对象直接进入老年代,默认3M,新对象大于他不管Eden是否够用,直接进入老年代
4、动态判断,From区某个年龄的对象大于空间的50%,则大于等于该年龄的对象进入老年代
实际上计算规则和描述不太一样,是年龄1+年龄2+年龄n总和超过50%,移动年龄大于n的对象
Minor GC
复制算法
触发时机
执行过程
- 首先将老年代剩余空间与新生代对象总量比较,如果大于则正常执行Minor GC,如果小于
- 判断是否开启空间担保选项,如果未开启,则将本次Minor GC升级为Full GC,如果开启
- 判断老年代剩余空间是否大于平均晋升对象占空间大小,如果小于,则将本次Minor GC升级为Full GC,如果大于
- 尝试进行Minor GC(本来就是Minor GC,检查完真正开始执行,这才是广义上的Minor GC过程)
- Survivor区够存,那么进入Survivor即可
- Survivor区不够,但是老年代剩余空间够存,那就直接进入老年代,进入老年代还有几个情况,对象年龄达到15、超过设定值的大对象(默认3M)、同一个年龄的对象空间大于From区50%
- Survivor区不够,并且老年代剩余空间也不够,本次Minor GC升级为Full GC
Full GC
Full GC可以认为是老年代GC,"老年代GC前会先进行一次新生代GC"是可配的,老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度
标记-清理算法
触发时机
- Minor GC之前,检查老年代剩余空间小于新生代对象总量,并且不满足Minor GC条件(1.未开启空间担保 2.开启空间担保但老年代剩余空间小于平均晋升空间),将触发Full GC
- Minor GC之后,老年代剩余空间小于晋升的对象空间,将触发Full GC
- 方法区(MetaSpace)空间不足
- 显式System.gc
- 如果使用CMS回收器,并且老年代内存占用达到了一定比例
执行过程
Full GC只是一个概念,表示对整个堆和方法区进行回收,就是让各个区分别使用不同的垃圾收集器进行GC
Full GC等同于Old GC这种说法,本质上是上面的条件实际上触发的是Full GC,进而引发新生代、老年代、分发区的GC
垃圾回收器
CMS
标记-清理(即支持清理又支持整理,并且默认是整理)
有两个参数控制整理行为
1、GC过后把对象整理到一起避免碎片,默认是开启的
2、还有个参数表示经过多少轮GC后开始一次碎片整理,默认0表示每次都整理)
在CMS遇到空间不足时,可以使用串行收集器作为后备
触发条件
- 本质就是Full GC条件
- 老年代内存占用达到一定比例,默认92%,自动触发
- 元空间容量不足
处理过程
-
初始标记 【STW】
单线程执行,标记直接能被GC Root和新生代引用到的对象
-
并发标记
并发执行,由初始标记的对象开始,标记所有能被引用的对象
-
并发预清理
并发执行,这个过程可能产生新的对象或垃圾,在这里提前对其中一部分情况的对象做标记,这里和重新标记做的都是同一个事,这里是为了减小重新标记的压力,此阶段标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象
-
重新标记 【STW】
并发执行,再次对引用关系进行确认
-
并发清理
G1
复制算法
对整个堆进行管理,即用一种回收器处理新生、老年代的回收
适合作为大内存的回收器,因为内存越大回收时间越长,G1可以控制回收时间
将内存分割成大小相同的区域
延续了新生代、老年代的概念,即某些块属于新生代某些块属于老年代
回收时间是可控的,利用**“回收价值”**做到这点,他知道每个块有多少垃圾回收要多长时间,在有限时间内尽可能多回收垃圾
触发条件
- 新生代块达到60%开始新生代垃圾回收
- 老年代块达到45%开始混合GC
ParNew
复制算法
多线程处理,线程数默认为CPU核心数
某个服务的 CPU使用率很高,甚至达到90%,如何排查?
两种原因
- 正常耗资源业务处理,如规则引擎
- 频繁Full GC导致CPU占用过高
定位过程
-
使用**【TOP】**命令观察CPU和内存使用情况
先观察一段时间,因业务里确实有耗资源操作
发现持续飙高,怀疑是GC
-
使用**【jstat -gc PID 1000 5】**命令查看JVM使用情况(每隔1000毫秒统计一次,执行5次)
显示内容包括新生代(Eden、Survivor)、老年代、方法区内存占用情况,YGC、FGC次数和总耗时
可以通过这些信息统计出
一、各个区的对象增长速度,即每次统计的值相减
二、Young GC触发频率
三、每次Young GC有多少对象进入老年代
发现Full GC次数比较多,所以要定位是什么对象占用空间比较多
这里只是估算,可用JVM运维监控工具OpenFalcon进行监控,监控JVM变化过程,预警(短时间超过3次GC就报警)并通知
-
使用**【jmap -histo PID】**命令,查看对象分布,这个命令会按照对象占用空间从大到小进行排列输出
(这里只是个大概信息,要详细的就用【jmap -dump】取出堆快照信息)
Full GC频率过高?
两种原因
-
JVM参数设置不合理,导致对象频繁进入老年代
这个和系统业务类型、处理能力有关,举个例子,4核8G,参数默认,JVM内存默认1/4物理内存分得2G,新生代/老年代默认1:2,新生代分700M,Eden分560M,Survivor分70M
-
程序问题,存在大量回收不掉的对象
JVM参数优化?
参数优化最核心就是:通过调整各个空间比例,使对象尽量在新生代分配和回收,减少Full GC次数
三个影响因素
- 资源情况,即部署多少机器,机器什么配置,资源充足机器管够不需要什么优化
- 业务类型,SF这种属于单量不大,但都是大对象,单个报文能达到2M,某些业务能达到10M,这些都要转成对象再做处理,其他业务如电商,单量大但流程相对简单,对象较小
- 业务总量,量小谈不上优化
这三点直接影响JVM内存占用情况的估算,不过这种估算不太靠谱,占用情况是受全部业务影响,很难准确判断,可以一边压测一边用监控工具观察
-Xmx 堆最大值,默认为物理内存的1/4
-Xms 堆初始值,默认为物理内存的1/64,一般这俩设置成一样
-Xmn 新生代大小,剩下的就是老年代 新:老默认1:2
-XX:MetaspaceSize 元空间触发GC阈值,默认20.8M 其对应的最大值默认是没有限制
-Xss 每个线程的栈大小,默认1M
4核8G,JVM分得4G左右内存
-Xmx 堆最大值, 3072M 设为3G,因大对象较多
-Xmn 新生代大小,2048M 设为2G,因大对象较多
-XX:MetaspaceSize 元空间触发GC阈值,256M 设为265M,因模版引擎使用到字节码操作,动态生成类
没有直接参与,但说一下思路
首先,说一下大的背景
资源情况,4核心8G,4个机器是一组
业务类型,大报文,处理时间长
业务总量,每天6万单,集中在早上9、10点
什么是最优不好说,拿一个默认的JVM,没做过调整的JVM
4核8G
堆默认为1/4物理内存,即2G
新生代老年代默认1:2,即新生代700M Eden分560M,Survivor分70M + 70M
老年代1.4G
单量为每天6万,集中在早上9、10点两个小时(这个时间段基本只有下单操作)
每小时3万,每分钟500,每秒8,4个机器,每个机器每秒分2个单
每单报文1M左右,加上各种校验、信息补全、系统交互等等,算翻5倍,每单要占用5M,每秒2单,即10M/S,每个任务要执行10s左右
-
看多长时间会Young GC(Eden区多长时间会满)
700M / 10M/s = 1分钟左右就满了
-
看Young GC后多少对象进到老年代
每秒2单,每单执行耗时10秒,所以当Eden满的时候,还有20单没执行完,当GC过后这20单不会被回收,即有200M左右不会被回收
Survivor为70M,不够存,所以GC后的对象会直接进到老年代,即每分钟有200M进入老年代
当然这个还有细节,每次Minor GC要判断老年代剩余空间是否大于新生代,并且还要根据空间担保策略判断
-
看多长时间就会触发Full GC(老年代多长时间满)
1.4G / 200M/分 7分钟左右就满了,相当于每7分钟就会触发一次Full GC
怎么优化?
这个例子能看出几个问题
- JVM资源分配不足,一般不用默认1/4,4核8G可以给到4G的内存
- 新生代和老年代比例不合理,导致Survivor区一直存不下Minor GC下来的晋升对象使其直接进入老年代
- 因为业务类型特殊,全是1M左右大对象,所以可能直接绕过Survivor区,直接进入老年代
解决办法
- 多给JVM分配一些内存,调整到4G
- 调整新生代老年代的比例,使得Survivor区资源够用,Eden对象可以晋升
- 适当调整大对象限制,默认3M,因为业务上是大对象,可能超过3M导致直接进入老年代
最终目的是为了让对象都在新生代里分配和回收,减少Full GC的频率
运维监控工具 OpenFalcon
JVM运维工具,监控JVM变化过程,预警(短时间超过3次GC就报警)并通知