一、类加载
java类加载分为5个阶段,加载,验证,准备,解析,初始化,使用,销毁
加载:会在内存中生成代表该类生成class对象。
验证:校验class文件的字节流是否符合jvm虚拟机规范
准备:为该类的变量设置初始值
解析:解析阶段是值常量池中符号引用变成直接引用的过程
符号引用:每种jvm的实现是不同的,但是他们识别的符合引用一定是相同的,应该他们被定 义在了java虚拟机class文件规范中
直接引用:可以是一个指向目标的指针,可以是相对的偏移量,也可以是一个能间接定位到 目标的句柄。只要直接引用,那么一定会在内存中存在
初始化:是类加载最后一个阶段,是执行类构造器<client>方法的过程,<client>是编译器自 动收集类中变量和静态代码块中代码合成出来的。
二、类加载机制
Java中有三种类加载器,BootstrapClassLoader启动类加载器(负责加载java_home/lib中的 类),ExtensionClassLoader扩展类加载器(负责加载java_home/lib/etx中的类),ApplicationClassLoader应用程序类加载器(负责加载用户路径中的类)。
类加载机制源码
当接收到一个类请求时,不会先使用自己的类加载器去加载,会先去找自己的父类去加载,每个阶层都是一样,当父加载提示无法完成请求时,才会尝试使用子类的加载器去加载,当所有的类加载器都无法完成请求时。会抛出classNotFound的异常。
ClassLoader#loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {//如果class没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {//如果父级不等于null,递归调用父级的类加载器
c = parent.loadClass(name, false);
} else {//等于null,则调用BootstrapClassLoader这个来加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
三、JVM内存区域
1、虚拟机栈:线程私有的,每个方法都对应着一个虚拟机栈
2、堆:java堆中存放中运行中产生的大部分对象。
3、本地方法栈:线程私有,存放native方法
4、程序计数器 线程私有的 java中存放虚拟机字节码指定的地址,native存的是undifined
5、方法区:线程是共享的,方法区是jvm规范规定的,逻辑划分
jdk1.7中方法区的实现叫永久代,1.8实现叫元空间
每一个类的结构信息,eg运行时常量池
1.6永久代,常量池也放到方法区中
1.7永久代,把常量池和静态变量放到堆中
1.8元空间 64位jvm默认元空间大小为21m,当元空间内存不足时会发生fullGc,当fullGc后内存空间还是不足时,会发生动态扩容,元空间属于本地内存,理论上系统内存有多大,元空间就有多大。
四、对象内存分配
(1)对象的创建方式:对象的创建方式,new,反射,序列化,clone
(2)对象存放在什么地方:大部分对象创建出来会分配在堆中,少部分对象在jvm开启逃逸分析后,发现没有方法逃逸的对象会在栈上分配。
public void demo(){
User user = new User();
user.sayHi();
StringBuffer stringBuffer = new StringBufer();
new Thread(){
stringBuffer.append("xxx");
}
}
此时user对象没有逃逸到方法外,直接在栈上进行内存空间分配即可。stringBuffer 线程逃逸了
优化:
2.1没有方逃逸,那么可以栈上分配
2.2没有线程逃逸,那么可以同步策略擦除
2.3标量、聚合量;标量替换
2.4 JVM优化中关于逃逸分析的参数
-XX:+DoEscapeAnalysit:开启了逃逸分析
-XX:+PrintEscapeAnalysit:如果开启了逃逸分析,那查看逃逸分析的结果
-XX:+EliminateAllocations:开启标量替换
-XX:+ElimitnateLocals:开启同步擦除
3、如何分配空间?
3.1 指针碰撞 如果被分配内存空间是连续的,只需向空闲内存方向移动一下指针
3.2 空闲列表 如果被分配内存空间不是连续的,需要用一个空闲列表来记录哪些空间可用,并在可用的内存中分配足够的空间,并修改空闲列表
在多线程环境中为了保证线程安全的,因为堆是线程共享的,所以在多线程环境中他的线程不是安全的。jvm会给每个线程在endn区分配一个私有的缓存区域,这一块也叫做TLAB(thread loacal allocation bufer),创建出来的对象首先会在TLAB中分配内存。如下图所示
-XX:+UseTLAB,TLAB在endn区默认占比是百分之1
-XX:+TLABWasteTargetPercent:TLAB占endn区的百分比
当对象大小大于TLAB的大小时,会cas竞争的方式在堆中分配空间。
4、对象的结构?
新创建的对象在堆中存储,那么对象内容的内存空间结构如何?new Object();
对象结构=对象头+实例数据+对齐填充对象头=markword+kclass+[数组长度]实例数据:相同宽度的数据放到一起对齐填充:8字节整数倍填充
对象指向
1、直接指向
一次指向,对象发生变化时需要查询指向
2、句柄指向
两次指向,对象发生变化时不用重新指向
五、垃圾回收算法
判断对象存活
1.引用计数法 任何有对象关联的引用,对象的引用计数都不为0,说明该对象不太可能被用到,变成可回收的目标 优点:标记到的就可以立刻被清。不会频繁触发,减少系统卡顿。缺点:循环引用无效
2.可达性分析法 解决引用计数循环引用无效的问题 ,如果在GCroots和一个对象没有可达的路径,则该对象是不可达的,不可达对象变为可回收对象至少经过两次标记后才会是可回收对象,面临回收
垃圾回收算法
1.标记清除算法 :先遍历一次对存货对象进行标记,在遍历一次把非存活对象回收,缺点会产生碎片
2.复制算法:把内存分成相等容量的两份,每次只使用一份,当一份使用完后,把存活对象复制到另一份中,缺点浪费空间,复制算法的效率和存活对象的多少有关,存活对象数量越多,复制算法的效率就越低
3.标记整理算法:首先对存活对象进行标记,然后使存活对象先一端移动,最后把端边界外对象全部清除
4.分代收集算法:核心思想是对象存活分为多个区域,一般虚拟机中堆分为新生代和老年代,新生代中需要回收的对象比较多,老年代中需要回收的对象比较少,所以可以采用不同垃圾回收算法。
(1)、新创建的对象尝试放到eden,,如果该对象比eden总量都大,那么直接放到老年代
(2)、eden没有足够的空间,触发一次minorGC,讲enden和from区的存活对象,移动到to区,对象年龄+1。然后将enden和from区进行回收。最后from区和to区互换
(3)、如果to去没有足够的空间,那么讲满足条件的对象移入到老年代,对象的年龄达到了一定数值默认值为15
(4)、移动过程中老年代空间也不足了。需要回收老年代的mojorGC,往往回收老年代的时候需要将整个堆空间一并回收fullGC
六、垃圾回收器
图中,线1的分代回收组合在jdk8中废弃,jdk9中移除。线2和CMS在jdk14中废弃。jdk8中默认
Parallel Scavenge和Parallel Old
CMS垃圾回收器
1、初始标记: stop-the-world,标记GCRoots直接关联的对象
2、并发标记:并发追溯标记,程序不会停顿
3、重新标记:暂停虚拟机,扫描CMS堆中的剩余对象>并发清理:清理垃圾对象,程序不会停顿
4、并发重置:重置CMS收集器的数据结构
CMS回收器耗时多的地方在于并发标记和并发清除,总体来说cms垃圾回收是和用户线程并发执行的。
缺点:1.会产生浮动垃圾,jvm默认8%的浮动垃圾预留空间,当预留空间满了之后会退化serialOld单线程回收。
2.因为和用户线程是并发执行的,所以对cpu资源比较敏感
3.因为使用的标记清除算法,会产生碎片,可以通过参数CMSFullGCsBeforeCompaction设置使其大于一定次数后进行碎片整理,默认值为0,就是默认每次fullGC后都需要进行碎片压缩。该参数设置减少full GC压缩的次数,节省了gc时间,也就更容易受碎片化问题的困扰。
G1回收器
1、G1垃圾收集器将整个 JVM 内存分为多个大小相等的region,年轻代和老年代逻辑分区 。
2、G1 是 Java9 以后的默认垃圾回收器了
3、G1 在整体上使用标记整理算法,局部使用复制算法
4、G1 的每个 Region 大小在 1-32M 之间,可以通过 -XX:G1HeapRegionSize=n 指定区大小。
5、总的 Region 个数最大可以存在 2048 个,即heap最大能够达到32M*2048=64G
6、obj<0.5 可以称为年轻代,0.5<obj<1,那么放到old区,old标记为H 1<obj<n,连续的n个region,作为H
概念
rset:每个region都有一个叫做rset的小区,它代表了其他region引用了当前region
对象的记录
cset:本次GC需要清理的Region集合
mixGC的过程
1、初始标记:标记出GCRoot对象,以及GCRoot所在的Region(RootRegion)
Root Region Scanning:扫表整个old的Region
2、并发标记:并发追溯标记,进行GCRootsTracing的过程
3、最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
4、清理回收:根据时间来进行价值最大化的回收,重置rset
G1相关的参数配置
-XX:+UseG1GC :设置使用 G1 垃圾回收器
-XX:MaxGCPauseMillis=n :最大 GC 停顿时间,毫秒值
-XX:InitatingHeapOccupancyPercent=n:当堆空间占用到 n 兆时就触发 GC(45)
-XX:GoncGCThreads=n:并发 GC 使用的线程数
-XX:G1ReserverPercent=n:设置作为空闲空间的预留内存百分比(10%)
标记算法
CMS和G1使用的标记算法都是三色标记算法
三色标记算法
白色:没有访问到
黑色:本对象已经访问到,而且对象中的引用也访问到
灰色:本对象访问过,但是对象中的属性没有访问过
CMS 三色标记算法会有浮动垃圾 错标:写屏障+增量更新
浮动垃圾:并发标记时,业务线程把GC线程已经标记对象变为null,那么该对象的引用就没有意义了,但是已经被标记过,不会回收,会产生浮动垃圾。
错标:黑色节点出现重新指向了一个白色节点,需要把黑色节点变成灰色节点,不然白色节点会被回收 。
错标解决方案:
var G = objE.fieldG; //读
objE.fieldG = null; //写(G1增加写屏障,断开时,生成一个SATB快照)
objD.fieldG = G; //写(CMS这重新指向时增加写屏障)
CMS:写屏障(重新指向时使用锁防止不被其他线程影响)+增量更新
G1: 写屏障+SATB(快照,根据快照可把对象都标记出来)