1、GC调优
- 掌握GC相关的JVM参数,会基本的空间调整
- 掌握相关工具
- 重点:调优与应用和环境有关,没有统一的规则
查看虚拟机运行参数
“jdk下bin目录下java命令的绝对地址” -XX:+PrintFlagFinal -version | findstr “GC”
1_1、调优领域
- 内存
- 锁竞争
- cpu占用
- io
1_2、调优目标
- 低延迟还是高吞吐量,选择合适收集器
- CMS G1 ZGC(超低延迟)
- Parallel GC
1_3、最快的GC是不发生GC
查看FULL GC前后的内存占用,考虑下面几个问题
- 数据是不是太多
- result = statement.Query(“select * from bigTable limit n”);
- 数据表示太臃肿
- 对象图
- 对象大小 16字节 Integer 24 int 4
- 是否存在内存泄漏
- static Map map 不断加入对象,强引用不得回收
- 软
- 弱
- 第三方缓存产品 redis等
1_4、新生代调优
新生代特点
- 所有new操作分配的内存都是廉价的
- TLAB thread local allocation buffer
- 死亡对象回收代价是0
- 垃圾回收将Eden和From中存活的对象复制到To中,剩下的垃圾对象直接全部删除,代价很小。
- 大部分对象用过即死
- Minor GC比Full GC 时间小很多。
新生代是否越大越好
- 新生代过小,会频繁触发Minor GC
- 新生代过大,老年代相应的可用空间会变小,FULL GC 门槛变低
- 在新生代变大的过程中,吞吐量首先是增长的,之后会下降。
- 新生代对象大多数都是用过即死,存活的对象极少,在新生代内存增大后,新生代的复制算法也不会受太多影响。
- 新生代能够容纳所有【并发量*(请求/响应)】的数据。
- 如果一次请求产生512k的对象,同一时间有1000个并发用户,则要保证新生代要能够存储512M的对象。(秒杀系统)
- (1/4-1/2)堆内存的大小
幸存区大到能保留(当前活跃对象以及需要晋升的对象)
- 这样能保证用不着的垃圾对象下次就能被回收
- 如果新存区不够存放,那么对象就会被转移到老年代,回收时间就会增加
晋升阈值配置得当,使长时间存活对象尽快晋升
- -XX:MaxTenuringThreshold=threshold 配置阈值
- -XX:+PrintTenuringDistribution 打印晋升细节
1_5、老年代调优
以cms为例。
- 老年代内存越大越好
- 先尝试不要做调优,如果没有FULL GC,那么已经满足需求,否则先尝试新生代调优
- 观察发生FULL GC时老年代的内存占用,将老年代的内存提高1/4-1/3
- -XX:CMSInitialOccupancyFraction = percent (老年代空间占用达到老年代空间总容量的百分之多少开始启用CMS垃圾回收)推荐设置为70-80 (CMS运行期间会产生浮动垃圾,所以得预留一些空间给浮动垃圾)
1_6、案例
- 案例一:FULL GC和Minor GC频繁
新生代内存过小会造成频繁的MinorGC , 同时会导致幸存区容量禁止,连锁反应幸存区的晋升阈值会被减低,大量的短存活对象进入老年代,引用高频率的Full GC。
所以,适当增大新生代内存大小,使得Eden可以存下新生的多个对象,不会过于频繁触发Minor GC,幸存区存下幸存对象能够使幸存对象在新存区中保存,不会过早进入老年代,老年代内存占用减轻。
- 案例二:请求高峰期发生FULL GC,单次暂停时间过长(CMS)
分析:请求高峰,并发用户很多,产生的新对象很多,堆中对象数目较大。CMS中重新标记会扫描整个堆内存来标记对象,所以会耗用大量时间。
调优:-XX:+CMSScavengeBeforeRemark 设置在重新标记前对新生代进行一次垃圾清理,会大大减少重新标记的时间。
2、虚拟机类加载机制
2_1、加载
- 通过一个类的全限定类名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个对象的各种数据的访问入口
将类的字节码载入方法区中,内部采用C++的instanceKlass这个数据结构描述Java类。
- _java_mirror Java类的镜像
- _super 父类
- _fields 成员变量
- _methods 方法
- _constants 常量池
- _class_loader 类加载器
- _vtable 虚方法表
- _itable 接口方法表
2_2、链接
- 验证:验证类是否符合JVM规范,安全性检查
- 准备:为static变量分配空间,设置默认值
- static 分配空间在准备阶段完成,赋值在初始化阶段完成
- static 变量是final的,那么编译阶段值就确定了,赋值在准备阶段完成
- static 变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
public class Demo02 {
static int a ;
static int b = 1 ;
static final int c = 2 ;
static final String d = "hello";
static final Object e = new Object() ;
}
{
static int a;
descriptor: I
flags: ACC_STATIC
static int b;
descriptor: I
flags: ACC_STATIC
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 2
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String hello
static final java.lang.Object e;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
public Demo02();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LDemo02;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
// 这两段代码在初始化阶段执行
//static int b = 1 ;
// static final Object e = new Object() ;
stack=2, locals=0, args_size=0
0: iconst_1
1: putstatic #2 // Field b:I
4: new #3 // class java/lang/Object
7: dup
8: invokespecial #1 // Method java/lang/Object."<init>":()V
11: putstatic #4 // Field e:Ljava/lang/Object;
14: return
LineNumberTable:
line 3: 0
line 6: 4
}
SourceFile: "Demo02.java"
2_3、解析
将常量池中的符号引用解析为直接引用
2_4、初始化
<cinit>()V 方法
初始化既调用<cinit>()V , 虚拟机会保证这个类的【构造方法】是线程安全的
public Demo02();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LDemo02;
类的初始化是懒惰的
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法
- 子类初始化时,如果父类没有初始化,会引发父类初始化
- 子类访问父类的静态变量,只会触发父类的初始化
public class SuperClass{
static{
System.out.println("Super Class Init") ;
}
public static int value = 123 ;
}
public class SubClass extends SuperClass{
static {
System.out.println("Sub Class Init") ;
}
}
public class Demo{
public static void main(String[] args){
// 输出的是Super Class Init
System.out.println(SubClass.value) ;
}
}
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的static final 静态常量(基本类型和字符串)
- 类对象.class
- 创建该类的数组时
- 类加载器的loadClass方法
- Class.forName的参数2为false
public class Demo03 {
public static void main(String[] args) {
System.out.println(E.a); // 不会导致E的初始化
System.out.println(E.b); // 不会导致E的初始化
System.out.println(E.c); // 会导致E的初始化
}
}
class E{
public static final int a = 10 ;
public static final String b = "hello" ;
public static final Integer c = 20 ;
static {
System.out.println("Init E");
}
}
输出:
10
hello
Init E
20
3、类加载器
名称 | 加载哪些类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
只有在这两个类是由同一个类加载器加载的前提下才有比较的意义,包括equal、instanceOf等
3_1、双亲委派机制
由于类加载器都拥有一个独立的类名称空间,所以不同的类加载器加载的类是不能进行比较的
双亲委派机制就是说,当请求一个类加载器加载某个类的时候,这个加载器先会委派给上级的类加载器进行加载,每一层的类加载器也是如此,最终到达bootstrap类加载器,只有当父加载器无法完成这个请求时,子加载器才会尝试自己去加载。
双亲委派机制的好处是,Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object,无论时哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各个类加载器环境中都是同一个类。如果没有使用双亲委派机制,由各个类加载器自行加载的话,应用程序会变得一片混乱