java基础(1)

来源:互联网 发布:sqlserver 数据类型 编辑:程序博客网 时间:2024/05/20 20:44

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();   //语句1inited = 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;        //语句1y = 0;        //语句2flag = true;  //语句3x = 4;         //语句4y = -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中对应的缓存行无效。

原创粉丝点击