volatile保证数据同步的原理:
至于volatile底层是怎么实现保证不同线程可见性的,这里涉及到的就是硬件上的,被volatile修饰的变量在进行写操作时,会生成一个特殊的汇编指令,该指令会触发mesi协议,会存在一个总线嗅探机制的东西,简单来说就是这个cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据。
JVM内存模型
大致分为:方法区、堆、程序计数器、本地方法栈、虚拟机栈。
线程共享:方法区、堆。 线程私有:程序计数器、本地方法栈、虚拟机栈。
程序计数器:记录线程执行指令。为了保证线程之间切换能够够正常执行,所以需要程序计数器来记录执行行号。
本地方法栈:同虚拟机栈类似,不过本地方法栈是为Native方法服务的。
虚拟机栈:虚拟机栈是描述java方法执行的内存模型,方法的执行都会在虚拟机栈中创建一个栈帧,用于存放动态连接、局部变量表、方法出口信息和操作数栈等。方法的执行相当于一个栈帧从入栈到出栈的过程。局部变量表包括:一片连续的内存中间用于存放方法参数、方法内定义的局部变量等数据。
方法区:也称为非堆,是线程共享的区域。用于存放虚拟机加载的类信息、静态变量、常量等信息。
堆:堆是java虚拟机内存管理最大的区域,因为堆是线程共享的,所以在多线程下也可能需要同步机制。堆存放一些对象的引用、数组等信息。
内存溢出:
1.StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度
2.OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]
public class MemErrorTest {
public static void main(String[] args) {
try {
List<Object> list = new ArrayList<Object>();
for(;;) {
list.add(new Object()); //创建对象速度可能高于jvm回收速度
}
} catch (OutOfMemoryError e) {
e.printStackTrace();
}
try {
hi();//递归造成StackOverflowError 这边因为每运行一个方法将创建一个栈帧,栈帧创建太多无法继续申请到内存扩展
} catch (StackOverflowError e) {
e.printStackTrace();
}
}
public static void hi() {
hi();
}
}
GC:
三个角度考虑GC:哪些对象需要回收、什么时候进行回收、怎么回收。
哪些对象需要回收
我们知道程序计数器、虚拟机栈、本地方法栈,由线程而生,随线程而灭,其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定。所以这部分不需要担心。而Java堆、方法区则不一样。方法区存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样【只有在运行期间才可知道这个方法创建了哪些对象没需要多少内存】,这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。
判断对象是否存活
1.引用计数算法
早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。
2.可达性分析算法
目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
垃圾回算法
标记清除算法、标记整理算法、复制算法
标记清除算法:
标记所有的可达对象(存在引用的对象),则未被标记的对象就是不存在引用的垃圾对象,GC时清除所有未被标记的对象
GC过程:先标记后清除。使用可达性分析算法进行标记存活对象,删除未标记的“死亡”对象。会删除循环引用却没有其他引用对象。
缺点:可能会造成碎片空间。
标记整理算法:
和标记清除算法类似,先标记存活对象,然后将所有存活对象整理到一块,将存活和死亡对象分开然后再进行清除,目的是为了解决造成空间碎片。
复制算法:
将存活对象复制到另一块区域,然后清除所有数据。