GC机制(针对堆内存)
一、检测垃圾有两种方式
引用计数法
给一个对象添加引用计数器,有地方引用他,计数器就加1,引用失效就减1,这种方式有个bug,就是两个对象相互引用,并且这两个对象没有其他引用,那么这两个对象就是垃圾,但是计数器又不为0,所以又不能当垃圾回收,那么就出现了“可达性分析算法”。
可达性分析算法
java中会定义一些对象为根集对象,垃圾回收器对整个对象图进行遍历,从根集对象开始,然后是根对象引用的其他对象,比如实例变量,便利完成后的对象全部为可达对象,遍历不到的对象则为不可达对象,视为垃圾对象,垃圾回收器则删除他们,标记的流程如下图所示:
注意:标记前,需要暂停应用程序,不然对象图一直变化无法标记,暂停的时间长短和堆内可达对象的数量有关,数量多时间长,反之时间短,切记!和堆内存的大小无关
二、删除垃圾有三种算法
上面一步骤检测完成后进入删除垃圾对象阶段
(1)标记—清除
标记完成后,不可达的对象所在的空间就被认为是空闲的,这些被认为是空闲的区域(就是可以在空闲区域创建对象),会被一个列表记录空闲的区域地址和大小,如下图所示:
缺点,灰色部分表示空闲区域,蓝色部分为正在用的区域:如果一个对象占用堆特别大,对象只能在某一个空闲区域,但是每个空闲区域部分空间都不够,所以对象会开辟一块新内存,导致这空闲区域一直被闲置。浪费内存
标记—清除—整理
标记-清除-整理算法修复了标记-清除算法的短板,它将所有标记的也就是存活的对象都移动到内存区域的开始位置,如下图所示:
缺点:暂定的时间较长,因为要更新引用地址
优点:显而易见,不会有碎片内存了
标记—复制
标记-复制算法与标记-整理算法非常类似,它们都会将所有存活对象重新进行分配。区别在于重新分配的目标地址不同,复制算法是为存活对象分配了另外的内存区域作为它们的新家 ,如下图所示:
优点:标记的同时,便可以复制,所以速度快
缺点:需要一块可以容纳可达对象的内存。
ClassLoader 机制(类加载器)
一、classloader做什么的:
将jdk编译好的class文件动态加载到内存中,程序刚启动时,会从特定的入口执行程序,class文件不会全部加载到内存中,根据需要加载,然后未加载的等到用到时再加载到内存中,classloader是动态加载。
二、classloader运行机制
双亲机制:
每个classLoader都包含一个父类加载器,(是包含不是继承),类在被加载到内存中前,会请求这个类加载器的上级类加载器,上级类加载器再次请求上级的类加载,直到虚拟机内置的类加载器(Bootstrap ClassLoader),Bootstrap ClassLoader尝试加载这个类,加载不到的话则向下传递,传递给Extension ClassLoader,如果也没加载到,则传給appClassLoader,也没加载到的话,则传递给自己定义的类加载器去加载,如果也没加载到的话则报异常。否则则加载到内存中,返回一个实例对象。如下图:
为什么采用双亲机制
避免重复加载,父类加载一次了,子类就不用加载了。考虑到安全因素,如果自己定义的String ,替换Java核心的Api的类型,那么有很大的隐患了,程序启动时,Bootstrap ClassLoader会把核心String
加载到内存中,所以自己的定义的ClassLoader加载自定义的String就不会被加载到内存中了。
但是 JVM 在搜索类的时候,又是如何判定两个 class 是相同的呢?
类名相同,且同一个类加载器实例加载的,则为同一个class,否则为两个class,否则就算两个类得字节码相同,被两个类加载,也是两个对象,他们相互转化时也是会报错的。
Android类加载器
android的类加载加载的是dex文件,dex文件是对class文件的封装,重新打包,同时对class文件的函数表,变量表进行优化,重新生成的dex文件,因此加载这种特殊的 Class 文件就需要特殊的类加载器DexClassLoader。
Volatile关键字(并发编程)
内存模型
概念
(1)作用:提高CPU执行效率
(2)模型:主内存和线程的本地缓存,cpu的操作每个指令都会在主存中获取变量的值,对变量进行操作,新值存入本地缓存,本地缓存获取新智刷新到主存。如下图:i=i+1操作
并发编程的相关概念
原子性:一个或多个操作不被打断
可见性:共享变量,一个线程修改,其他线程可以看到
有序性:程序的执行顺序按照代码的顺序执行,
因为程序要高速执行指令,所以一定要使用内存模型,导致程序在并发编程时,会出现各种错误,只要保证程序具备原子性、可见性、有序性就会保证程序执行不会出错。
非原子性出现的问题:
int i ;
public void setValue(){
Thread thread1= new Thread(new Runnable() {
@Override
public void run() {
i++;
}
});
Thread thread2= new Thread(new Runnable() {
@Override
public void run() {
i++;
}
});
thread1.start();
thread2.start();
}
执行结果
执行的结果:
可能是i=1;而并非是2;
原因:
线程1在主存中获取i的值,还未执行+1操作,这时线程2也在主存中获取i的值,进行+1操作,然后刷新到主存,这时主存的值为1,然后线程1刚刚进行+1操作,然后刷进主存,这是主存的值为1,并没有为2
非可见性出现的问题
和原子性出现的原因差不多 :
线程1对变量i修改了之后,线程2没有立即看到线程1修改的值,导致值错误
非可序性出现的问题
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
出现非有序性的原因:
程序在执行时,为了提高效率,会发生指令重排序,指令重排序就是代码执行的顺序不按照代码的书写顺序,但执行的结果和书写顺序是一致的。
上面执行的结果
可能进入死循环,线程1执行context = loadContext() ,并没有执行inited = true,紧接着线程2执行,进入死循环,最后执行inited = true; 这就产生问题了。
解决并发编程的出现的问题
解决根源:
保证一个线程读写操作并写入主存完成后再交给另一个线程去执行。
保证原子性:synchronized和lock
保证可见性:volatile、synchronized和lock
保证有序性:volatile、synchronized和lock
volatile原理:
(1)保证可见性:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,其他线程的本地内存会置为无效状态,会重新获取主内存的值。
(2)保证有序性:
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
因为变量flag为volatile修饰,所以语句4和语句5不会在语句1和语句2之前执行,执行到语句3时,保证语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。但是语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
(3)在执行volatile执行时,汇编代码发现一个lock前缀指令,lock其实相当于内存屏障(也成内存栅栏)。他有三个作用:
(一)、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(二)、它会强制将对缓存的修改操作立即写入主存;
(三)、如果是写操作,它会导致其他CPU中对应的缓存行无效。