Java基础知识梳理_javass框架访问触发时机不同-程序员宅基地

技术标签: JAVA  java  jdk8  基础梳理  

java程序运行包括2个重要阶段:编译阶段和运行阶段

编译阶段:检查java源程序是否符合Java语法,符合Java语法贼能够生成正常的字节码文件(.class)
javac使用规则:javac xxx.java
值得注意的是,在java语言里,类型的加载和连接过程都是在程序运行期间完成的。虽然在加载时增加了一些性能开销,但是也让java语言拥有了可以动态扩展的语言特性。

运行阶段过程:

  • java A(注意不要写成A.class)
  • java.exe会启动虚拟机JVM,JVM会启动类加载器ClassLoader
  • ClassLoader会去硬盘搜索A.class文件,找到后将该字节码文件装载到JVM当中
  • JVM将A.class解释成二进制数据
  • 操作系统执行二进制和底层硬件进行交互

虚拟机类加载机制

类加载时机(整个生命周期)

类从被加载到虚拟机内存中开始,到卸载出内存为止。它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,如下图所示。
在这里插入图片描述

Java的类加载过程

jvm将.class类文件信息加载到内存并解析成对应的class对象的过程,注意:jvm并不是一开始就把所有的类加载进内存中,只是在第一次遇到某个需要运行的类才会加载,并且只加载一次.
主要分为三部分(五步):1、加载,2、链接(1.验证,2.准备,3.解析),3、初始化

  1. 加载
    类加载器包括 BootClassLoader、ExtClassLoader、APPClassLoader
  2. 连接(包含验证、准备、解析)
    验证:(验证class文件的字节流是否符合jvm规范)
    准备:为类变量分配内存,并且进行赋初值
    解析:将常量池里面的符号引用(变量名)替换成直接引用(内存地址)过程,在解析阶段,jvm会把所有的类名、方法名、字段名、这些符号引用替换成具体的内存地址或者偏移量。
  3. 初始化
    主要对类变量进行初始化,执行类构造器的过程,简单点说,只对static修饰的变量或者语句进行初始化。

加载

在这里插入图片描述

验证

验证是虚拟机对自身保护的一项重要工作。确保Class文件的字节流中包含的信息符合当前虚拟机的要求。Java语言是相对安全的语言(相对于C/C++来看),他做不到一些事,比如访问数组边界以外的数据,如果这样做,编译器将拒绝编译。但是Class不一定要求用Java源码编译而来,可以使用任何途径。所以必须要验证。
在这里插入图片描述

准备

分配内存;设置初始值。(值得注意的是分配内存和设置初始值的不是全部变量)
在这里插入图片描述

解析

符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用的目标必定已经在内存中存在。
在这里插入图片描述

初始化

虚拟机中对于初始化阶段,严格规定以下情况必须立即对类进行"初始化"(而加载、验证、准备自然在此之前开始):

  1. new、getstatic、putstatic、invokestatic这四条字节码指定执行时。

new关键字实例化对象;读取、设置一个类的静态变量(被final修饰、已在编译期把结果放入常量池的静态字段除外);调用一个类的静态方法。

  1. 使用java.lang.reflect包的方法反射调用的时候。
  2. 初始化一个类发现其父类还没初始化,必须先进行父类的初始化。
  3. 虚拟机启动时,用户指定了一个要执行的主类(有main()方法),先初始化这个主类。
  4. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有经过初始化,则需要先触发其初始化。

在这里插入图片描述

类加载器

Java语言流行的重要原因之一。
对于任意一个类,都需要由加载他的类加载器它本身一同确立其在Java虚拟机中的唯一性。这里相等包括Class对象的一些方法(比如equals)还有instanceof关键字等。

双亲委派模型

从Java虚拟机的角度看,只有2种类加载器,C++语言实现的启动类加载器(Bootstrap ClassLoader),是虚拟机自身的第一部分。另一类就是其他类加载器,独立于虚拟机外部。(如Extension CLassLoader和Application ClassLoader)

当一个ClassLoader 实例需要加载某个类时,它会试图在亲自搜索这个类之前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器Bootstrap CLassLoader进行加载。

image

要求除顶层启动加载器(Bootstrap ClassLoader)之外,其余的类加载器都应该有自己的父类加载器(不是继承关系,是组合关系系)。

好处

Java类随着他的类加载器一起具备了一种带有优先级的层次关系。对于保障Java程序的稳定运作起了重要作用。

拿java.lang.Object来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar里面的java.lang.Object加载到JVM中。

实现双亲委派的代码在java.lang.ClassLoader的loadClass()方法,逻辑清晰易懂:先检查是否已被加载过,没有则调用父加载器的loadClass(),若父加载器为空,则默认使用启动类加载器作为父加载器。父类加载失败,抛出ClassNotFoundException异常后调用自己的findClass()方法加载,自己也找不到,ClassNotFoundException就正儿八经的抛出了。

双亲委派模型不是强制性约束模型,Java世界里大多数类加载器都遵循这个模型,但是也有意外。

String、StringBuffer、StringBuilder

StringBuffer里面的很多方法添加了synchronized关键字,是可以表征线程安全的,所以多线程情况下使用它。
执行速度:StringBuilder > StringBuffer > String
StringBuilder牺牲了性能来换取速度的,这两个是可以直接在原对象上面进行修改,省去了创建新对象和回收老对象的过程,而String是字符串常量(final)修试,另外两个是字符串变量

提示:不要使用String拼接,这样会频繁回收新建,使用另外2个通过append方法添加比较合适。

关于赋值运算符“=”

首先要搞清楚 基本类型 和 引用类型。

//基本类型num
int num = 10;
//引用类型str
String str = "hello";

在这里插入图片描述

  • 基本类型,值就直接保存在变量中
  • 引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。

再来说赋值运算符

//直接改变变量的值,原来的值被覆盖掉
num = 20;
//改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变
str = "java";

在这里插入图片描述

补充1:数组

//arr是引用
//stack上仅仅占用4字节空间,new int[10]会在heap中开辟一个数组对象,然后arr指向它。
int[] arr = new int[10]

二维数组

int[][] arr2 = new int[2][4]

分析:arr2同样仅在stack中占用4个字节,会在内存中开辟一个长度为2的,类型为int[]的数组,然后arr2指向这个数组。这个数组内部有两个引用(大小为4字节),分别指向两个长度为4的类型为int的数组。
在这里插入图片描述

补充2:String

实际上String对象内部仅需要维护三个变量,char[] chars, int startIndex, int length。所以上面关于String的例子是简化版的。String被设计成为了不可变类型。
在这里插入图片描述
总结:不用纠结于概念,其实区别在于操作的是一块内存还是新开辟了一块内存的区别。然后Java始终是传值的

equals()和“==”

equals方法是超类Object中的一个基本方法,用于检测一个对象是否与另外一个对象相等。而在Object类中这个方法实际上是判断两个对象是否具有相同的引用。

public boolean equals(Object obj) {  return (this == obj);   }

默认情况下也就是从超类Object继承而来的equals方法与‘==’是完全等价的,比较的都是对象的内存地址。

需要注意hashCode方法
object类的equals函数的API,里面有这样一句话:

注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode
方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。
hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的。

String s="ok";
String ss="ok";
//String t = new String("ok");
String t = new String(s);
StringBuilder sb =new StringBuilder(s);

System.out.println(s.hashCode()+"  "+ss.hashCode()+"  "+t.hashCode()+"  "+sb.hashCode());
System.out.println(s==t);
System.out.println(s.equals(t));
System.out.println(s==ss);
System.out.println(s.equals(ss));

打印结果

3548  3548  3548  1704856573
false
true
true
true

类比Interger:两个new出来Integer对象,即使值相同,通过“==”比较结果为false,但两个对象直接赋值,则通过“==”比较结果为“true,这一点与String非常相似。

字符串的散列码是由内容导出的,因为String类重写了hashCode方法,所以上述方法s和t的hashCode是一样的,又因为StringBuilder直接使用了超类的hashCode方法,所以sb字段的hashCode与另外2个不同。

四种引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这四种引用强度依次逐渐减弱。

强引用

就是指在程序代码之中普遍存在,类似“Object obj = new Object()”这类的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 如果强引用对象不使用时,需要弱化从而使GC能够回收

obj =null;

如果是方法的内部有一个强引用,引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。

软引用(SoftReference)

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

应用场景示例:浏览器的后退按钮。

// 获取浏览器对象进行浏览
Browser browser = new Browser();
// 从后台程序加载浏览页面
BrowserPage page = browser.getPage();
// 将浏览完毕的页面置为软引用
SoftReference softReference = new SoftReference(page);

// 回退或者再次浏览此页面时
if(softReference.get() != null) {
    // 内存充足,还没有被回收器回收,直接获取缓存
    page = softReference.get();
} else {
    // 内存不足,软引用的对象已经回收
    page = browser.getPage();
    // 重新构建软引用
    softReference = new SoftReference(page);
}

弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

值得一提的是谷歌官方推荐使用WeakReference。

虚引用(PhantomReference)

又称幽灵引用、幻影引用。虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动
能在这个对象被收集器回收时收到一个系统通知。

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);

JVM

image

JVM源码分析文集

  1. 类加载器classLoader,在JVM启动时或者类运行时将需要的.class文件加载到内存中。
  2. 执行引擎,负责执行class文件中包含的字节码指令。
  3. 本地方法接口,主要是调用C/C++实现的本地方法及返回结果。
  4. 内存区域(运行时数据区),是在JVM运行的时候操作所分配的内存区,主要分为以下五个部分:
    1.	方法区:用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。
    2.	Java堆(heap):存储Java实例或者对象的地方。这块是gc的主要区域。
    3.	Java栈(stack):Java栈总是和线程关联的,每当创建一个线程时,JVM就会为这个线程创建一个对应的Java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是线程私有的。
    4.	程序计数器:用于保存当前线程执行的内存地址,由于JVM是多线程执行的,所以为了保证线程切换回来后还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
    5.	本地方法栈:和Java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

垃圾回收机制GC

垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制

垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁。但不能说是回收对象,没错这就是文字游戏。

垃圾:无任何对象引用的对象
回收:清理“垃圾”占用的内存空间而非对象本身
发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中
思考:找到垃圾的算法和回收垃圾算法有哪些?堆内存为配合垃圾回收有什么区域划分?

新生代老年代概念

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

以HotSpot虚拟机为例子,默认新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )。新生代中,E默认den : From Survivor : To Survivor= 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio来设定 )

判断对象是存活还是死了?

  • 引用计数算法(Reference Counting Collector)
    堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。(比如a=b,b被引用)当引用失效时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。
    早期的JVM使用引用计数,现在大多数JVM采用对象引用遍历(根搜索算法)。

  • 根搜索算法(Tracing Collector)
    首先了解一个概念**:根集(Root Set)**

所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。

(1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
(2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
(3)重复(2)。
(4)搜索所走过的路径称为引用链,当一个对象到GC。 Roots没有任何引用链相连时,就证明此对象是不可用的。
image

Java和C#中都是采用根搜索算法来判定对象是否存活的。
垃圾回收器将某些特殊的对象定义为GC根对象。所谓的GC根对象包括:

(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中的常量引用的对象;
(3)方法区中的类静态属性引用的对象;
(4)本地方法栈中JNI(Native方法)的引用对象。
(5)活跃线程。
注意点:开始进行标记前,需要先暂停应用线程。不然是没法统计的;统计完垃圾对象之后,回收器将会在接下来的阶段中清除它们;暂停时间的长短取决于存活对象的多少;实际上GC判断对象是否可达看的是强引用;根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程对象还有逃脱死亡命运的最后一次机会

1.如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于OC中的dealloc,Swift中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
2.如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

四种垃圾收集算法

标记-清除算法(Mark-Sweep)

分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。缺点是效率问题和空间问题。可以存在大量不连续的内存碎片,导致程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
image

复制算法(针对新生区)

为了解决效率问题。将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。优点是每次都是对其中的一块进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点是内存缩小了一半,代价太大
image

现在的商业虚拟机都采用复制收集算法来回收新生代,有研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。即如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代

标记-整理算法(Mark-Compact) 针对老年区

复制收集算法在对象存活率较高时就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制收集算法。
根据老年代的特点提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
部分整理算法:

  • 双指针回收算法:实现简单且速度快,但会打乱对象的原有布局。
  • Lisp2算法(滑动回收算法):需要在对象头用一个额外的槽来保存迁移完的地址。
  • 引线整理算法:可以在不引入额外空间开销的情况下实现滑动整理,但需要2次遍历堆,且遍历成本较高。
  • 单次遍历算法:滑动回收,实时计算出对象的转发地址而不需要额外的开销。

整理的顺序
不同算法中,堆遍历的次数,整理的顺序,对象的迁移方式都有所不同。而整理顺序又会影响到程序的局部性。主要有以下3种顺序:

  1. 任意顺序:对象的移动方式和它们初始的对象排列及引用关系无关.
    任意顺序整理实现简单,且执行速度快,但任意顺序可能会将原本相邻的对象打乱到不同的高速缓存行或者是虚拟内存页中,会降低赋值器的局部性。任意顺序算法只能处理单一大小的对象,或者针对大小不同的对象需要分批处理;
  2. 线性顺序:将具有关联关系的对象排列在一起.
  3. 滑动顺序:将对象“滑动”到堆的一端,从而“挤出”垃圾,可以保持对象在堆中原有的顺序.

所有现代的标记-整理回收器均使用滑动整理,它不会改变对象的相对顺序,也就不会影响赋值器的空间局部性。复制式回收器甚至可以通过改变对象布局的方式,将对象与其父节点或者兄弟节点排列的更近以提高赋值器的空间局部性。

image

分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并无新的方法,只是根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商,不同版本的虚拟机所提供的垃圾收集器可能有很大差别。下面以HotSpot JVM1.6的垃圾收集器为例
image
如果两个收集器之间存在连线,就说明他们可以搭配使用。并没有最好的收集器这一说,我们需要选择的是对具体应用最合适的收集器。

Serial收集器

关键字:新生代、单线程
特点是它在进行垃圾收集时,必须暂停其他所有工作线程,直到他收集结束。体验贼差。

ParNew收集器

关键字:新生代、Serial收集器的多线程版本

Parallel Scavenge收集器

关键字:新生代、复制算法、并行多线程、吞吐量优先(这里的吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间))
参数设置:

-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间。(大于0的毫秒数)停顿时间缩短是以牺牲吞吐量和新生代空间换取的。(新生代调的小,吞吐量跟着小,垃圾收集时间就短,停顿就小)。

-XX:GCTimeRatio 直接设置吞吐量大小,0<x<100 的整数,允许的最大GC时间=1/(1+x)。

-XX:+UseAdaptiveSizePolicy 一个开关参数,开启GC自适应调节策略(GC Ergonomics),将内存管理的调优任务(新生代大小-Xmn、Eden与Survivor区的比例-XX:SurvivorRatio、晋升老年代对象年龄-XX: PretenureSizeThreshold 、等细节参数)交给虚拟机完成。这是Parallel Scavenge收集器与ParNew收集器的一个重要区别,另一个是吞吐量。

Serial Old收集器

关键字:老年代、单线程

Parallel Old收集器

关键字:老年代、多线程

CMS收集器(Concurrent Mark Sweep)

关键字:以获取最短回收停顿时间为目标、并发收集。
目前很大一部分Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求。

优点:并发收集,低停顿。(使用的“标记-清除”算法)
缺点:对CPU资源非常敏感;无法处理浮动垃圾;算法原因会产生空间碎片。

G1收集器(Garbage First)

它是当前收集器技术发展的最前沿成果。与CMS相比有两个显著改进:

  • 基于“标记-整理”算法实现收集器
  • 非常精确地控制停顿
    G1收集器可以在几乎不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的由来)。区域划分、有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

Java对象

对象组成

  • 对象头
对象头组成:
1,Mark Word(Mark Word记录了对象和锁有关的信息)
2,指向类的指针(类结构信息保存在方法区。实例和对象则存在堆。这里指针指向方法区中类)
3,数组长度(只有数组对象才有)
  • 实例数据

  • 对齐填充字节
    JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数。

对象创建过程

在这里插入图片描述

类加载检查

  1. 检查该new指令的参数 是否能在方法区的常量池中定位到一个类的符号引用。
  2. 检查该类符号引用 代表的类是否已被加载、解析和初始化过。

为对象分配内存

虚拟机将为对象分配内存,即把一块确定大小的内存从 Java 堆中划分出来。(对象所需内存的大小在类加载完成后便可完全确定)

内存分配 根据 Java堆内存是否绝对规整 分为两种方式:指针碰撞 & 空闲列表。(选择方式取决于Java堆内存是否规整)

Java堆内存规整:已使用的内存在一边,未使用内存在另一边。
Java堆内存不规整:已使用的内存和未使用内存相互交错。

Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
使用带Compact过程的垃圾收集器时,采用指针碰撞。(如Serial、ParNew垃圾收集器)
使用基于Mark_sweep算法的垃圾收集器时,采用空闲列表。(如CMS垃圾收集器)

指针碰撞

(1)假设Java堆内存绝对规整,内存分配将采用指针碰撞。
(2)分配形式:已使用内存在一边,未使用内存在另一边,中间放一个作为分界点的指示器
那么,分配对象内存 = 把指针向 未使用内存 移动一段 与对象大小相等的距离。
空闲列表
(1)假设Java堆内存不规整,内存分配将采用空闲列表。
(2)分配形式:虚拟机维护着一个记录可用内存块 的列表,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

线程不安全的问题(举个例子,分配内存时指针还没修改就被拿去用了)
解决办法

  1. 同步处理分配内存空间的行为

虚拟机采用 CAS + 失败重试的方式 保证更新操作的原子性。

  1. 把内存分配行为 按照线程 划分在不同的内存空间进行

即每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁。(虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。)

将内存空间初始化为零值

内存分配完成后,虚拟机需要将分配到的内存空间初始化为零(不包括对象头)

对对象进行必要的设置

设置这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
信息存放在对象的对象头。值得注意的是,从java虚拟机角度看,一个新的java对象创建完毕,但是从程序开发角度来说,还需要进行初始化。

对象访问

最简单的访问也会涉及:Java堆、Java栈、方法区。
访问的对象是类型数据和对象实例数据,我举个例子就很容易理解了:

Object obj=new Object();

访问方式

  • 句柄
    好处是在reference中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,reference本身不需要被修改。
  • 直接指针
    好处是速度更快,节省了一次指针定位的时间开销。虚拟机Sun HotSpot就是使用这种来进行对象访问的。

在这里插入图片描述

集合

Collection接口

Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是Set和List。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。

在这里插入图片描述

Map接口

Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。

在这里插入图片描述

集合框架知识点小结

  • 同步容器:Vector、Stack、Hashtable;Collections类中提供的静态工厂方法创建的类。

Hashtable实现了Mpa接口,他不属于Set或者List。还需要注意的是,虽然同步容器的所有方法都加了锁,但是对这些容器的复合操作无法保证其线程安全性。好比Vector方法的size()方法都加了锁,如果有一个方法体,先调用size(),再调用remove(),那就需要给这个方法体加上synchronized。

  • 加入Set的每个元素必须是唯一的,否则,Set是不会把它加进去的。要想加进Set,Object必须定义equals(),这样才能标明对象的唯一性。
  • 插入TreeSet中的Object数据必须实现Comparable接口,具有比较性。
  • ArrayList与Vector的比较

Vector可以设置增长因子,ArrayList不行。Vector是一种老的动态数组,是线程同步的,效率很低。(补充:除开Vector,实现了Map接口的Hashtable、HashMap都可以设置增长因子。)然后ArrayList增加长度是有计算公式的,和Vector不一样。{((旧容量 * 3) / 2) + 1}

  • HashSet如何保证不重复的?
//这个挺有意思的,HashSet虽然没有直接继承Map接口,但是他内部有一个HashMap对象。没错,就是靠HashMap来实现的。

/**
 *往HashSet中添加元素
 */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

在HashMap中进行查找是否存在这个key,value始终是一样的:

1、	如果hash码值不相同,说明是一个新元素,存;
2、	如果hash码值相同,且equles判断相等,说明元素已经存在,不存;
3、	如果hash码值相同,且equles判断不相等,说明元素不存在,存;
  • HashSet与TreeSet的适用场景和区别

HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。

TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset中的数据是自动排好序的,不允许放入null值。
HashSet 是哈希表实现的,HashSet中的数据是无序的。允许一个key == null。

  • HashMap与TreeMap、HashTable的区别及适用场景

HashMap 非线程安全,基于哈希表(散列表)实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主要分两种,一种是开放定址法,另一种是链表法。HashMap的实现中采用的是链表法。
TreeMap:非线程安全基于红黑树实现,TreeMap没有调优选项,因为该树总处于平衡状态。
HashTable:线程安全基于哈希表。与HashMap很相似。

-HashMap和HashTable的区别:

  • HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
  • HashMap把Hashtable的contains方法去掉了,改成containsValue和 containsKey。因为contains方法容易让人引起误解。
  • HashTable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
  • HashTable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap就必须为之提供外同步。
  • Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。
  • 集合框架中常用数据结构底层用了啥
    • Collection接口
      • ArrayList:数组
      • LinkedList:双向链表 LinkedList.Node
      • Vector:数组
      • Stack:数组(补充:Stack继承自Vector,是实现了标准的后进先出的栈。)
      • ArrayDeque:数组(补充:ArrayDeque实现了接口Deque(双端队列),Deque继承接口Queue)
      • HashSet:HashMap(补充:HashMap可能为普通HashMap,或者LinkedHashMap,取决于调用的构造方法)
      • LinkedHashSet:LinkedHashMap
      • TreeSet:二叉树(补充:插入TreeSet中的数据必须实现Comparable接口,具有比较性)
    • Map接口
      • TreeMap:二叉树(补充:TreeMapEntry<K,V> 实现了Map.Entry<K,V>接口,看了源码,就是红黑树)
      • HashMap:HashMap.Node<K,V>(补充:HashMap.Node<K,V>实现了Map.Entry<K,V>接口)
      • Hashtable:HashtableEntry<K,V>,可以参考HashMap.Node<K,V>(补充:HashMap和Hashtable的底层实现都是数组+链表结构实现)
//截取一段TreeMapEntry<K,V>代码
private static final boolean RED   = false;
private static final boolean BLACK = true;
static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        TreeMapEntry<K,V> left;
        TreeMapEntry<K,V> right;
        TreeMapEntry<K,V> parent;
        boolean color = BLACK;
        //...
        //省略其他代码
    }
//截取一段HashMap.Node<K,V>代码
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        //...
        //省略其他代码
    }

泛型

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现ClassCastException。

泛型擦除
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

限定通配符

  1. 一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界。
  2. 另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。
  3. 另一方面<?>表 示了非限定通配符,因为<?>可以用任意类型来替代。

关于List<?>和List

List<?> 是一个未知类型的List,而List<Object> 其实是任意类型的List。
可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给 List<Object>。

反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

效率低的原因

  • Method#invoke方法会对参数做封装和解封操作,参数类型不知道,数量不知道,频繁调用Long、String等的转型方法。
  • 需要检查方法可见性
  • 需要校验参数
  • 反射方法难以内联
  • JIT 无法优化(反射涉及到动态加载的类型,所以无法进行优化。)

1. 反射的入口:java.lang.Class

反射和自省

Reflection和Introspection。

内省作用:在运行时检查一个对象的类型或者属性。
内省示例:instanceof 判断对象类型

反射作用:在运行时检查或者修改一个对象信息。

日常开发对象类型

基本类型
  • 整数:byte, short, int, long
  • 小数:float, double
  • 字符:char
  • 布尔值:boolean
引用类型
  • 所有的引用类型都继承自 java.lang.Object
  • 类,枚举,数组,接口都是引用类型
  • java.io.Serializable 接口,基本类型的包装类(比如java.lang.Double)也是引用类型

获取Class

  1. 通过对象的getClass方法
  2. 在要获得的类名后加上 .class (适用于当前没有某个类的对象的情况)
Class c = int[][].class;
  1. Class.forName()方法获取(只适用于引用类型)
Class<?> c = Class.forName("java.lang.String");
  1. 静态属性 TYPE(适用于基本类,和有处理的包装类:Void)
Class<Integer> integerWrapper = Integer.TYPE;
Class<Double> doubleWrapper = Double.TYPE;
Class<Void> voidWrapper = Void.TYPE;
  1. 其他一些api方法
//返回Field所在的类
java.lang.reflect.Field.getDeclaringClass()
//返回Method所在的类
java.lang.reflect.Method.getDeclaringClass()
//返回Constructor所在的类
java.lang.reflect.Constructor.getDeclaringClass() 

//返回调用类的父类
Class.getSuperclass() 
//返回调用类所有公共类、接口、枚举组成的 Class 数组,包括继承的
Class.getClasses() 
//返回调用类显示声明的所有类、接口、枚举组成的 Class 数组
Class.getDeclaredClasses() 

Modifier修饰符

java.lang.reflect.Modifier
提供了对 Class 修饰符的解码,我们可以使用 Class.getModifiers() 获得调用类的修饰符的二进制值,然后使用 Modifier.toString(int modifiers) 将二进制值转换为字符串,Modifier.toString() 方法实现如下:

//基于sdk29 android.jar
public static String toString(int mod) {
    StringBuilder sb = new StringBuilder();
    int len;

    if ((mod & PUBLIC) != 0)        sb.append("public ");
    if ((mod & PROTECTED) != 0)     sb.append("protected ");
    if ((mod & PRIVATE) != 0)       sb.append("private ");

    /* Canonical order */
    if ((mod & ABSTRACT) != 0)      sb.append("abstract ");
    if ((mod & STATIC) != 0)        sb.append("static ");
    if ((mod & FINAL) != 0)         sb.append("final ");
    if ((mod & TRANSIENT) != 0)     sb.append("transient ");
    if ((mod & VOLATILE) != 0)      sb.append("volatile ");
    if ((mod & SYNCHRONIZED) != 0)  sb.append("synchronized ");
    if ((mod & NATIVE) != 0)        sb.append("native ");
    if ((mod & STRICT) != 0)        sb.append("strictfp ");
    if ((mod & INTERFACE) != 0)     sb.append("interface ");

    if ((len = sb.length()) > 0)    /* trim trailing space */
        return sb.toString().substring(0, len-1);
    return "";
}

注意事项

  • Java 中预定义的注解 @Deprecated,@Override, 和 @SuppressWarnings 中只有 @Deprecated 可以在运行时被访问到。
  • 被 @Retention(RetentionPolicy.RUNTIME) 修饰的注解才可以在运行时被发射获取
  • Interface 默认是 abstract 的。编译器会在编译器为每个 Interface 添加这个修饰符。不需要手动添加。

Member

Member是一个接口,代表 Class 的成员。
Member 有三个实现类:

  • java.lang.reflect.Constructor:表示该 Class 的构造函数
  • java.lang.reflect.Field:表示该 Class 的成员变量
  • java.lang.reflect.Method:表示该 Class 的成员方法

2. Field

成员变量。每个成员变量有类型和值。

java.lang.reflect.Field 提供了两个方法获去变量的类型:

  • Field.getType():返回这个变量的类型
  • Field.getGenericType():如果当前属性有签名属性类型就返回,否则就返回 Field.getType()
try {
        Class<Double> doubleWrapper = Double.TYPE;
        Field[] fields = doubleWrapper.getFields();
        for (Field field : fields) {
            printFormat("Field:%s \n",field.getName());
            printFormat("Type:\n  %s\n",field.getType().getCanonicalName());
            printFormat("GenericType:\n  %s\n",field.getGenericType().toString());
            printFormat("\n\n");
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

3. Method

继承的方法(包括重载、重写和隐藏的)会被编译器强制执行,这些方法都无法反射。
因此,反射一个类的方法时不考虑父类的方法,只考虑当前类的方法。由修饰符、返回值、参数、注解和抛出的异常组成

Class<Double> doubleWrapper = Double.TYPE;
Method[] declaredMethods = doubleWrapper.getDeclaredMethods();

代理

房产中介就是一个常见的代理。代理的概念里有几个关键词:静态代理、动态代理、代理模式下面我们只讨论动态代理。

来个简单的动态代理的例子

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method);
                if (method.getName().equals("morning")) {
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(), // 传入ClassLoader
            new Class[] { Hello.class }, // 传入要实现的接口
            handler); // 传入处理调用方法的InvocationHandler
        hello.morning("Bob");
    }
}

interface Hello {
    void morning(String name);
}

Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

注解

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。

内置的注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

@Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
@SuppressWarnings - 指示编译器去忽略注解中声明的警告。
作用在其他注解的注解(或者说 元注解)是:

@Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
@Documented - 标记这些注解是否包含在用户文档中。
@Target - 标记这个注解应该是哪种 Java 成员。
@Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
从 Java 7 开始,额外添加了 3 个注解:

@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
@FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
@Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

例子

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

@interface 用来声明 Annotation,@Documented 用来表示该 Annotation 是否会出现在 javadoc 中, @Target 用来指定 Annotation 的类型,@Retention 用来指定 Annotation 的策略。

有一篇写的特别好的文章可以看一看:https://www.cnblogs.com/xdp-gacl/p/3622275.html

线程和锁

Java中线程创建的方式

关键字:Thread、继承Thread、Runnable接口、Callable接口和FutureTask、线程池。

总结

  • Thread常用的构造函数
    • Thread()
    • Thread(Runnable target)
  • Callable经常配合FutureTask一起使用,FutureTask间接继承Runnable。
  • Callable接口相比Runnable而言,会有结果返回。
  • 调用FutureTask#get()会阻塞。
  • 线程池经常配合Callable和Runnable使用。

线程创建练习

    private void ThreadInit(){
        Thread t1 = new Thread(){
            @Override
            public void run() {
                System.out.println("第1种方式:new Thread 1");
            }
        };
        t1.start();
       SystemClock.sleep(1);


        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("第2种方式:new Thread 2");
            }
        });
        t2.start();
        SystemClock.sleep(1);


        FutureTask<String> ft = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                String result = "第3种方式:new Thread 3";
                return result;
            }
        });
        Thread t3 = new Thread(ft);
        t3.start();


        // 线程执行完,才会执行get(),所以FutureTask也可以用于闭锁
        String result = null;
        try {
            //这里会阻塞,直到线程返回值
            result = ft.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        SystemClock.sleep(1);


        ExecutorService pool = Executors.newFixedThreadPool(5);

        Future<String> future = pool.submit(new Callable<String>(){
            @Override
            public String call() throws Exception {
                String result = "第4种方式:new Thread 4";
                return result;
            }
        });
        pool.shutdown();
        try {
            //shutdown方法调用了,future.get()依然能拿到数据,但是使用shutdownNow()就不行了。
            System.out.println(future.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

Java中线程状态

New

新建、初始。
新创建了一个线程对象,但还没有调用start()方法。

Runnable

就绪。
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

BLOCKED

表示线程阻塞于锁。"阻塞状态"和"等待状态"的区别是"阻塞状态"在等待一个排他锁,这个事件将另一个线程放弃这个锁的时候发生;而"等待状态"则是等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

WAITING

进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

TIMED_WAITING

该状态不同于WAITING,它可以在指定的时间后自行返回。

TERMINATED

表示该线程已经执行完毕。

image

image

等待队列

image

与线程有关的一些方法说明

Object#wait()与Thread#sleep(long millis)的区别

简单来说wait()会释放对象锁资源而sleep()不会释放对象锁资源(wati是超类的方法,他才和锁机制密切相关)。但是 wait 和sleep 都会释放cpu资源;sleep是线程方法,wait是object方法;

Thread#yield()方法会让出CPU调度

一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

Object#notify()

唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

Thread#join()/Thread#join(long millis)

一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。
等待调用join方法的线程结束,再继续执行。

线程安全

当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

相对线程安全

以Vector为例,他里面的方法都加上了synchronized关键字,但是他是相对线程安全的。

例子:往某个Vector对象里存值,要求集合中没有重复的元素存在。

public static void put(int size,Vector vector) {
	if (!vector.contains(size)) 
	     vector.add(size); 
}

首先分析一波:生活中,代码是有先后执行顺序的,所以要模拟一种理想情况。不考虑线程在运行时环境下的调度和交替执行的情况下,假设有两个线程同时进入put()方法,传递的参数都是一样的,当线程1执行if (!vector.contains(element)) 后还没有执行vector.add(element); 时,线程2进来了,此时**vector.contains(element)**还是返回false,这样的结果会导致两个数据都加入到了vector。这就是不安全的。

解决办法也很简单

  1. 给put方法加上synchronized关键字
  2. 双重判断
if (!vector.contains(element)) 
    synchronized(this){
        if (!vector.contains(element)) {
        vector.add(element); 
        }
    }

保证线程安全的必要条件

  • 使用线程安全的类
  • 使用synchronized同步代码块,或者用Lock锁
  • 多线程并发情况下,线程共享的变量改为方法局部级变量

死锁

产生死锁的四个条件

  • 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  • 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  • 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  • 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

volatile

定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

补充知识点:JMM原子操作和操作规定

JMM定义了8种原子操作

1.lock 锁定 : 把主内存中的一个变量标志为一个线程独享的状态
2.unlock 解锁 : 把主内存中的一个变量释放出来
3.read 读:将主内存中的变量读到工作内存中
4.load 加载:将工作内存中的变量加载到副本中
5.use 使用:当执行引擎需要使用到一个变量时,将工作内存中的变量的值传递给执行引擎
6.assign 赋值:将执行引擎收的的值赋值给工作内存中的变量
7.store 存储:将工作内存中的变量的值传到主内存中
8.write 写入:将store得到值放到主内存的变量中

对于上述原子操作有以下规定

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现。
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

特点

可见性(其修饰的变量是内存可见的)

通过加入内存屏障禁止重排序来优化实现的。

硬件层内存屏障分为2种:Load Barrier 和 Store Barrier即读屏障和写屏障。
java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad

基于保守策略的JMM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

oadLoad,StoreStore,LoadStore,StoreLoad实际上是Java对上面两种屏障的组合,来完成一系列的屏障和数据同步功能:

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。

(1)线程写 volatile 变量的过程:先改变线程工作内存中 volatile 变量副本的值。再将改变后的副本的值从工作内存刷新的主内存。

(2)线程读 volatile 变量的过程:从主内存中读取 volatile 变量的最新值到线程的工作内存中。再从工作内存中读取 volatile 变量的副本。

JDK1.5之后,可以使用volatile变量禁止指令重排序。
处理器为了提高运行效率,在JVM中的及时编译存在指令重排序的优化,它会改变各个语句的执行顺序,但是不改变运行结果。指令重排序只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
内存屏障与禁止重排序有一定联系,编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

原子性(复合操作时不保证原子性)

对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不
具有原子性。
可以通过synchronized和Lock来实现更大范围操作的原子性。

synchronized

JAVA线程间通信

JAVA线程间通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
内存模型抽象示意图如下:
在这里插入图片描述
如图所示,线程A和线程B之间要通信的话,必须要经历2个步骤。

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 线程B到主内存中读取线程A之前已更新过的共享变量。

在这里插入图片描述

锁的内存语义

synchronized的底层是使用操作系统的mutex lock实现的。
  • 内存可见性:同步快的可见性是由JMM原子操作规定中获得的。(对一个变量执行lock操作会怎样?对一个变量执行unlock操作之前需要做什么?)见上文原子操作和操作规定第7和第9条。
  • 操作原子性:持有同一个锁的两个同步块只能串行地进入
锁的内存语义

(1)当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
(2)当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

锁释放和锁获取的内存语义
  1. 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。
  2. 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  3. 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

特点

实现原子性

持有同一个锁的两个同步块只能串行地进入。

实现内存可见性

由JMM原子操作规定中获得的。lock和unlock是成对的,可多次lock。线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

synchronized锁

synchronized用的锁是存在Java对象头里的。

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。

根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

由于Java的线程是映射到操作系统的原生线程之上的
,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态
中,因此状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中:

synchronized与java.util.concurrent包中的ReentrantLock相比,由于JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

注意事项
  1. synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
  2. 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
Mutex Lock(互斥锁)

监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

mutex的工作方式:
image

  1. 申请mutex
  2. 如果成功,则持有该mutex
  3. 如果失败,则进行spin自旋. spin的过程就是在线等待mutex, 不断发起mutex gets, 直到获得mutex或者达到spin_count限制为止
  4. 依据工作模式的不同选择yiled还是sleep
  5. 若达到sleep限制或者被主动唤醒或者完成yield, 则重复1)~4)步,直到获得为止

由上可知,synchronized 线程执行互斥代码过程可简述为:获取互斥锁;清空工作内存;从主内存中拷贝变量最新值到工作内存;执行代码;将更改后的共享变量刷新到主内存;释放互斥锁。

volatile和synchronized区别

  • volatile 本质是在告诉 jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

ThreadLocal

ThreadLocal简述

ThreadLocal提供了线程的局部变量,每个线程都可以通过ThreadLocalMap的set()、get()和remove()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。ThreadLocal可以类比浏览器,不同的浏览器对Cookie是隔离的(在IE会登录bilibili记住账号,换Chrome登录依然要输入账号密码)。

ThreadLocalMap

ThreadLocal的静态内部类ThreadLocalMap。Thread中也有用到。
他有一个Entry[] table类,里面存着以ThreadLocal<?> 为键,Object为值的变量。

public class Thread implements Runnable {
   ......(其他源码)
    /* 
     * 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,主要用于父子线程间ThreadLocal变量的传递。
     * 该映射由InheritableThreadLocal类维护。
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ......(其他源码)
}

使用示例

ThreadLocal可以让我们拥有当前线程的变量,对于新建对象与当前工作线程有关的操作比如,JDBC中频繁创建数据库连接池的操作,交由ThreadLocal来进行管理,可以保证事务不会混乱。当前线程的操作都是用同一个Connection


public class DBUtil {
    //数据库连接池
    private static BasicDataSource source;

    //为不同的线程管理连接
    private static ThreadLocal<Connection> local;


    static {
        try {
            //加载配置文件
            Properties properties = new Properties();

            //获取读取流
            InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("连接池/config.properties");

            //从配置文件中读取数据
            properties.load(stream);

            //关闭流
            stream.close();

            //初始化连接池
            source = new BasicDataSource();

            //设置驱动
            source.setDriverClassName(properties.getProperty("driver"));

            //设置url
            source.setUrl(properties.getProperty("url"));

            //设置用户名
            source.setUsername(properties.getProperty("user"));

            //设置密码
            source.setPassword(properties.getProperty("pwd"));

            //设置初始连接数量
            source.setInitialSize(Integer.parseInt(properties.getProperty("initsize")));

            //设置最大的连接数量
            source.setMaxActive(Integer.parseInt(properties.getProperty("maxactive")));

            //设置最长的等待时间
            source.setMaxWait(Integer.parseInt(properties.getProperty("maxwait")));

            //设置最小空闲数
            source.setMinIdle(Integer.parseInt(properties.getProperty("minidle")));

            //初始化线程本地
            local = new ThreadLocal<>();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        
        if(local.get()!=null){
            return local.get();
        }else{
        
            //获取Connection对象
            Connection connection = source.getConnection();
    
            //把Connection放进ThreadLocal里面
            local.set(connection);
    
            //返回Connection对象
            return connection;
        }

    }

    //关闭数据库连接
    public static void closeConnection() {
        //从线程中拿到Connection对象
        Connection connection = local.get();

        try {
            if (connection != null) {
                //恢复连接为自动提交
                connection.setAutoCommit(true);

                //这里不是真的把连接关了,只是将该连接归还给连接池
                connection.close();

                //既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了
                local.remove();

            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


}

与Synchronized的对比

  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
  • Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
  • ThreadLocal 并不需要通过缓冲区与 主内存中的变量进行交互。

ThreadLocal 可以让线程独占资源,存储于线程内部,避免线程堵塞造成 CPU 吞吐下降。在每个 Thread 中包含一个 ThreadLocalMap,ThreadLocalMap 的 key 是 ThreadLocal 的对象,value 是独享数据。

线程池

构造方法参数解释

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler) 
corePoolSize

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务,也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列,则maximumPoolSize则不起作用,因为无法提交至核心线程池的线程会一直持续地放入workQueue。

keepAliveTime

非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长。

unit

keepAliveTime的单位。可选单位在枚举类TimeUnit里。比如天、时、分、秒、毫秒、微妙、纳秒。

workQueue

用来保存等待被执行的任务的阻塞队列. 在JDK中提供了如下阻塞队列:

  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
  2. LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;(无界队列,队列容量为Integer.MAX_VALUE)
  3. SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
  4. priorityBlockingQuene:具有优先级的无界阻塞队列;
threadFactory

创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory。

handler

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

  1. ThreadPoolExecutor.AbortPolicy:直接抛出异常,默认策略;
  2. ThreadPoolExecutor.CallerRunsPolicy:用调用者所在的线程来执行任务;
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  4. ThreadPoolExecutor.DiscardPolicy:直接丢弃任务;

也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略

线程池分类

Executors#newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

分析:核心线程数和允许最大线程数一致,有关超时的设置无效了(除非ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true),线程池的线程数量达corePoolSize后,不会释放线程。而且使用无界队列,可以一直往里添加任务。

Executors#newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

分析:初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.使用了无界队列,所以SingleThreadPool永远不会拒绝, 即饱和策略失效。

Executors#newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

分析:线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;

特点如下

  1. 主线程调用SynchronousQueue的offer()方法放入task, 倘若此时线程池中有空闲的线程尝试读取 SynchronousQueue的task, 即调用了SynchronousQueue的poll(), 那么主线程将该task交给空闲线程. 否则执行(2)
  2. 当线程池为空或者没有空闲的线程, 则创建新的线程执行任务.
  3. 执行完任务的线程倘若在60s内仍空闲, 则会被终止. 因此长时间空闲的CachedThreadPool不会持有任何线程资源.

线程池的优势

  • 重用存在的线程,减少对象创建,消亡的开销。 线程的创建和销毁的开销是巨大的,减少开销意味着性能提升。
  • 有效控制最大并发线程数,提高系统资源利用率。控制线程池的并发数可以有效的避免大量的线程池争夺CPU资源而造成堵塞。

并行、并发

  • 并行: 同一时刻同时做许多事情。CPU调度基本单位是线程,对于多核心多线程CPU来说,同一时刻同时做多件事没什么。
  • 并发: 又称为伪并行,表示在一个时间段多个任务同时发生,但是在一个时刻点上只有一个程序在处理机上运行。

当一个任务提交至线程池,他会如何处理

在这里插入图片描述

往线程池添加任务

execute和submit。

execute 只能接受Runnable类型的任务。

void execute(Runnable command); 

submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null。

<T> Future<T> submit(Callable<T> task);  
<T> Future<T> submit(Runnable task, T result);  
Future<?> submit(Runnable task); 

如果是不需要关注返回值的场景,使用execute

工作中用的比较多submit方法,可以通过Future的get()方法得到线程的运行结果

停止线程池

shutdown()和shutdownNow()

void shutdown(); 
List<Runnable> shutdownNow(); 

方法解析:

shutdown()这种方法是有序的进行停止,在此之前提交的任务都可以继续执行,而执行此方法后如果继续往线程池丢任务,则不会再去执行任务。调用这个方法,线程池不会等待(wait)在执行的任务执行完成,可以使用awaitTermination实现这个目的。他会依次中断那些没有中断,并且是空闲的线程。
shutdownNow()这种方法是停止所有线程(正在执行的和等待的),并返回任务列表(等待执行的任务),已经执行的任务是不会返回的。

感谢

部分参考文章及参考书籍,侵删。

《深入理解Java虚拟机》
《Java语言规范》
《Java并发编程的艺术》
双亲委派模型与线程上下文类加载器
4种垃圾收集算法及7种垃圾收集器
synchronized原理总结
volatile内存可见性和指令重排
ThreadLocal就是这么简单
深入理解 Java 反射:Class (反射的入口)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16692517/article/details/106714595

智能推荐

计算机的外围设备简介_计算机外围固定-程序员宅基地

文章浏览阅读6.1k次,点赞3次,收藏5次。外围设备介绍计算机的外围设备(简称外设)虽然很多,但按功能分大类只有四类:输入、输出、存储、网络通讯。有些专业计算机需要的外围设备也不尽相同,并不都需要这四类外围设备。外围设备可以按需要组装,有些专业计算机甚至可以将存储设备和主芯片集成到一片芯片上,从而不再需要外加存储设备。最早的计算机(那时还只能称为计算器,只能做简单运算,如ABC机和ENIAC机)输入只是一些拨码开关,只能输入数字(还得是二进_计算机外围固定

java 图片中加文字_java怎么在图片上加文字-程序员宅基地

文章浏览阅读1.5k次。java 图片中加文字_java怎么在图片上加文字

GBase8cGDCA认证模拟题题库(三)_如果需要打开delete语句的审计功能,需要开启下面哪个参数-程序员宅基地

文章浏览阅读720次,点赞20次,收藏6次。B 选项,在创建模式时,可以不指定模式名。C 选项,兼容模式可选值为 AB、C、PG.安装GBase 8c分布式集群时所需的配置文件gbase.yml,在解压GBase8cV5 S3.0.0BXX CentOS x86 64.tar.bz2压缩包生成的目录中得到。真值的有效文本值是: TRUE、t、"true'、y、yes'、"1'TRUE'、true、整数范围内1~2^63-1、整数范围内-1~-2^63。GBase 8c 使用create table 创建表时,不指定参数,默认是astore,行存表。_如果需要打开delete语句的审计功能,需要开启下面哪个参数

xml文件中几个名词_xml文件里面的名词-程序员宅基地

文章浏览阅读334次。1 xmlns是XML Namespaces的缩写,中文名称是XML(标准通用标记语言的子集)命名空间。 web-app是web.xml的根节点标签名称 version是版本的意思 xmlns是web.xml文件用到的命名空间 xmlns:xsi是指web.xml遵守xml规范 xsi:schemaLocation是指具体用到的schema资源_xml文件里面的名词

【OpenGL】中点圆、椭圆生成算法_用setpixel函数中点画圆算法代码c++-程序员宅基地

文章浏览阅读1.6w次,点赞12次,收藏69次。OpenGL 中点圆、椭圆生成算法_用setpixel函数中点画圆算法代码c++

HTML-CSS实现背景图片出现不同的位置_css背景图高度占据一半另一半有别的背景色-程序员宅基地

文章浏览阅读2.1k次。首先在HTML中写入div,命名为img,在这个div中加入一个span标签并命名为img-bg和img50(5星为50).<div class="img"> <span class="img-bg img50"></span> <span class="img-bg img45"></span> <span class="img-bg img40"></span> </div> 在css代码._css背景图高度占据一半另一半有别的背景色

随便推点

duilib vs2015 安装_DuiLib(1)——简单的win32窗口-程序员宅基地

文章浏览阅读169次。资源下载https://yunpan.cn/cqF6icWRN5CTc 访问密码 92e3 注:DUILIB库.7z 是vs2015下编译好的动态库及静态库,如上图所示一、新建一个win32工程项目设置中选择:debug,常规中:全程无优化-全程无优化,多线程调试 (/MTd);我的项目选择的是静态编译,使用的是静态库,就不需要带duilib.dll文件了代码如下:#include #inclu..._vs2015使用duilib

OpenGL: 渲染管线理论详解_通过此次实验你对固定渲染管线的opengl编程有什么了解。-程序员宅基地

文章浏览阅读5k次,点赞4次,收藏13次。学习着色器,并理解着色器的工作机制,就要对OpenGL的固定功能管线有深入的了解。首先要知道几个OpenGL的术语:渲染(rendering):计算机根据模型(model)创建图像的过程。模型(model):根据几何图元创建的物体(object)。几何图元:包括点、直线和多边形等,它是通过顶点(vertex)指定的。 最终完成了渲染的图像是由在屏幕上绘制的像素组成的。在内存中,和像素有关的信息(如像素的颜色)组织成位平面的形式,位平面是一块内存区域,保存了屏幕上每个像素的一个位的信息。_通过此次实验你对固定渲染管线的opengl编程有什么了解。

Android MPAndroidChart:动态添加统计数据线【8】_android 动态统计-程序员宅基地

文章浏览阅读3.9k次。Android MPAndroidChart:动态添加统计数据线【8】本文在附录相关文章6的基础上,动态的依次增加若干条统计折线(相当于批量增加数据点)。布局文件:

vmware中的linux虚拟机如何增加磁盘容量_linux虚拟机磁盘空间不足-程序员宅基地

文章浏览阅读6.3k次。vmware中 centos的磁盘大小 20G->30G现象:fdisk -l可以看到增大后的磁盘总量,但是需要增加分区并格式化然后挂载才能使用.一、vmware中的设置先关闭虚拟机vm->settings->hard disk->utilities->expand->输入大小(增加后的大小)二、启动虚拟机,进入命令行1、 fdisk /dev/sda进入命令行Comman_linux虚拟机磁盘空间不足

Hadoop2.7.3下Mysql8.0下Hive2.3.8的安装_hive2.3.8安装-程序员宅基地

文章浏览阅读927次。hive安装前提:1.基于hadoop2.7的完全分布式集群搭建完成hadoop2.7集群搭建2.MySQL8.0安装完成 安装centos7上MySQL8.0Hive2.3.8的安装下载链接:https://mirrors.tuna.tsinghua.edu.cn/apache/下滑找到hive点击进去点击hive2.3.9(hive2.3.9和hive2.3.8差别不大)下载画红线的也就是bin.tar.gz后缀的hive解压安装下载完成后通过xftp传到虚拟机上(基操不在赘述)_hive2.3.8安装

The‘grub-efi-amd64-signed‘ package failed to install into /target/. Without the GRUB boot loader,_the grub-efiamd64-signed' package failed to instal-程序员宅基地

文章浏览阅读430次,点赞8次,收藏4次。在进行安装的时候有一个是否联网的选择,选择链接网络,则在安装的时候,可以看到在安装过程中,它会主动下载grub-efi-amd64-signed' package,确确实实,我在安装详情里看到了它有这个的download过程以及update过程。_the grub-efiamd64-signed' package failed to install intotarget without the g

推荐文章

热门文章

相关标签