类加载
加载时机
使用new实例化对象时读取和设置类的静态变量,调用静态方法等;
反射;
初始化类先初始化父类;
main所在的类;
通过子类调用父类静态变量,不会初始化子类;
定义对象数组或者集合,不会触发初始化该类;
引用一个类的静态常量(必须为字面值常量,如,p s f int a = 1可, 而a = random.next则不可)不会初始化类;加载
通过类全限定名获取类的二进制字节流;
将字节流代表的静态数据结构转换为运行时结构;
生成class对象,作方法区各种数据访问入口;
执行引擎
JAVA的解释器配和即时编译混合使用是半编译半解释语言;
解释执行: 运行时候,针对字节码,通过解释器解释后执行;
编译执行: 编译为计算机识别的语言后执行;
即时编译(JIT):
运行过程中通过编译器编译优化,缓存,直接生成机器码,执行;
三种模式,字节码解释器,模板解释器,混合模式;提升性能原因:运行期的热点代码编译和缓存;
-
字节码解释器(如C1) 运行前期,收集信息少,编译优化少,生成代码执行效率较模板解释器编译后低;
java字节码 -> c++代码 -> 计算机可识别的硬编码
-
模板解释器(如C2) 运行一段时间,收集信息多,编译优化点多,生成代码执行效率高,优化多
java字节码 -> 硬编码
逃逸分析:
JIT一种重要技术,根据变量作用域是否只在方法内,并且没有赋值给变量外的变量,就不会发生逃逸,如:局部变量,方法参数,相反则可能会发生逃逸;
-
栈上分配:
jvm动态判断不会逃逸对象是否直接在栈上分配内存,这样对象随着方法出栈销毁,减少gc次数,提高程序执行性能,一般针对高频、高量、低容量的引用对象: -
同步消除:
分析不会逃逸对象,如 果不会被其他线程访问,编译优化去除同步; -
标量替换:
标量:简单理解为java基本数据类型,不可再分割,聚合量:如:对象
分析对象如果不会被外部访问,使用标量替换对象的成员变量,直接在栈上或者寄存器分配空间,如:用 20 替换 user.age(age=20);
对象
对象结构
对象头
MarkWord:
占8B,存放对象运行时数据,如:hashcode,锁标志,gc年代,gc标志等;
类型指针:
指针压缩或32位:4B,否则8B,指向方法区的instanceKlass对象,用于表示属于哪个类实例;
数组长度:
只有数组才有,占4B;实例数据
存放对象成员变量信息,包括父类继承;对齐填充
8B倍数,不够0填充;
创建方式
- new;
- 反射
clazz.newInstance(),
User.class.getConstructor(Integer.class).newInstance(123)) - clone,不会调用构造器;
- 反序列化,不会调用构造函数;
创建过程
1.类加载检测
虚拟机接受new指令,根据指令参数去常量池定位这个类的符号引用,如果没有找到则加载这个类;
2.分配内存
在类加载的时候对象的大小就已经确认,通过指针碰撞或者空闲列表的方式给对象分配内存
-
指针碰撞
内存规整
通过一个指针指向内存使用的区域与未使用的区域的分界值,分配内存时中间值指针向未使用区域移动需要分配对象大小的长度; -
空闲列表
内存不规则
jvm维护一个记录内存空间中可用的部分,分配内存时,在列表里寻找一个分配; -
并发分配内存安全
因为堆是线程公用所以分配对象内存存在多线程竞争问题,因此分配内存时通过自旋CAS的方式设置,但是自旋会消耗处理器性能,因此每个线程会有一个位于Eden区初始默认1%大小的本地内存(TLAB),对象优先在本地内存中分配;
3.初始化0值
内存分配完成后,jvm将分配到的内存基本数据类型赋0值,引用类型赋null值;
4.设置对象头
包含类型指针,hashcode,gc信息,锁信息等;
5.执行<init>方法,完成对象创建
init方法在编译时生成,在创建对象是执行,所以称为实例构造器(clinit在类加载时候执行,所以称为类构造器),执行顺序是:
- 父类变量初始化;
- 父类代码块;
- 父类构造函数;
- 子类变量初始化;
- 子类代码块;
- 子类构造函数;
对象访问
1.句柄
将堆中的一块区划分出来作为句柄池,引用类型指向就是句柄池地址,句柄包含对象的实例对象指针和类型指针;
优点是对象的移动只需改变句柄中对象指针,而不需要改变引用地址;
缺点是多次寻址,性能较低;
2.直接指正(Hotspot使用)
引用类型指向就是对象的内存地址;
优点是直接寻址,效率高;
缺点是对象移动指针也要随之改变;
多线程程序计数器;
方法运行,栈:局部变量表,操作数栈...,方法执行过程;
堆(物理内存的1/64-1/4),内存分配,一次GC过程;
分配比例:
新生代
1/3
Eden区
8/10
From区
1/10
From区
1/10老年代
2/3
GC
一般情况对象会分配在新生代,在jdk1.8中默认的垃圾回收器为 parallel scavenge,他是多线程并行的垃圾收集器,因其设计理念为吞吐量优先,又称吞吐量优先收集器,吞吐量 = 代码执行时间/(代码执行时间+垃圾回收时间),采用的是复制算法;
1.8在老年代默认的垃圾回收器为Parallel Old,他并行并行的标记-整理算法;
1 触发
当创建对象的某一时刻,eden区全部挤满后就会触发Monitor GC;
2 GC前判断
老年代可用空间是否大于新生代所有对象,是则进入MonitorGC,否则触发老年代空间分配担保规则(如果配置了的话),
- 老年代剩余大小,大于历次MonitorGC后剩余,进行MonitorGC,
- 否则,进行FullGC;
3 MonitorGC
- 通过标记阶段后将判断eden区和from区存活对象是否超过to区大小,不超过则全部移动到to区,清空eden和from区,原to区转换为from区;
- 超过to区总大小,将全部存活对象年龄+1放入老年代,年龄超过15(默认)的全部加入老年代;
- 默认年龄可以设置,最大为15,对象GC年龄是存在于对象头的MarkWord里的ObjerHeader里的4个bit位存放,其最大只能到15;
4 OOM
FullGC后任然放不下剩余对象;
垃圾回收算法等;
垃圾判断算法
引用计数算法
记录对象被引用的次数,被其他对象引用计数+1,引用失效计数-1,计数为0,表示可回收,缺点是循环引用的对象无法被回收;
可达性分析算法
通过GC Roots对象作为起点,向下搜索引用链,无法达到的对象被认为可回收;可作为GC Roots的对象:
正在被调用方法的栈帧中指向堆中的对象的参数,局部变量,临时值等;
jvm中的静态变量指向的对象;
3.正在被被加载的类;
4.运行时常量池中的引用型常量;
5.字符串常量池的引用;
垃圾回收算法
标记-清除算法(基础算法)
分为标记阶段
和清理阶段
;缺点:标记和清理阶段需要stw,且效率不高,另外清理后会产生内存碎片,可能会导致无法获取连续内存分配给较大对象,非内存空间不足;
标记-复制算法(解决内存碎片)
分为标记阶段
和复制阶段
和清理阶段
;将内存分为两块,每次使用其中一块,标记后将存活对象复制到未使用内存块,清理原内存块;
优点:无内存碎片,空闲内存时连续的不需要使用空闲列表分配对象,提高了效率;
缺点:空间浪费,每次只能使用一半的空间,对存活率高的对象,每次gc都需要进行复制操作,因此复制算法只适用于生命周期短的对象区域的回收,如新生代的gc算法;
标记-整理算法
分为标记->清除->整理
三个阶段;类似标记清理算法,只是在标记后将存活对象向一端移动,清理调边界外的所有对象;
优点:无内存碎片,空间利用高;
缺点:性能较低;
jvm调优,工具,命令,含义;
常用命令
jps 线程pid
jstat class 加载类数量 gc次数等
jstack 看线程相关信息
jmap 看堆内存相关信息
频繁Full GC处理流程
1.查看GC日志,jvm启动参数加gc日志,分析gc;
2.dump GC日志:jmap -dump:format = b,file=文件名.hprof pid;
3.jvisualvm分析;
CPU高处理流程
1.top找到占用CPU最高的进程pid;
2.top -H -p pid找到该进程中占最高的线程;
3.线程id转化为16进制,用jstack 进程id |grep 16进制线程id -A 30 >> 文件名.dump 获取线程dump文件,分析代码问题;