三个值得深入思考的 Android 问答分享(第 1 期)
这是 JsonChao 的第 175 期分享
一、5 个 Android 开发不得不透彻理解的字符串问题
1、String 是 java 中的基本数据类型吗?是可变的吗?是线程安全的吗?
String 不是基本数据类型,java 中的基本数据类型是:byte、short、int、long、char、float、double、boolean。
String 是不可变类,一旦创建了 String 对象,我们就无法改变它的值,因此,它是线程安全的,可以安全地用于多线程环境中。
2、为什么要设计成不可变的呢?
String 设计为不可变的原因有如下 3 点:
1、安全:由于 String 广泛用于 java 类中的参数,所以安全是非常重要的考虑点,包括线程安全,打开文件,存储数据密码等等。 2、效率:String 的不变性保证哈希码始终如一,所以在用于 HashMap 等数据结构的时候就不需要重新计算哈希码,提高效率。 3、空间:因为不同的字符串变量可以引用池中的相同字符串,所以可以在java 运行时节省大量的 java 堆空间,如果字符串是可变的话,任何一个变量的值改变,就会反射到其他变量,那字符串池也就没有任何意义了。
3、如果 String 是不可变的,那我们平时赋值的是改了什么呢?
平时使用双引号方式赋值的时候其实是返回的字符串引用,并不是改变了这个字符串对象。
4、String 有哪些创建方式?它们在 JVM 的存储方式是相同吗?
String 常见的创建方式有两种:
第一种:String s1 = “Java”
s1 会先去字符串常量池中找字符串 "Java",如果有相同的字符则直接返回字符串引用,如果没有此字符串则会先在常量池中创建此字符串,然后再返字符串引用。
第二种:String s2 = new String("Java")
s2 是直接在堆上创建一个变量对象,但不存储到字符串池,调用 intern 方法才会把此字符串保存到常量池中。
5、简单讲讲 String,StringBuffer,StringBuilder 的区别?
String 是不可变类,每当我们对 String 进行操作的时候,总是会创建新的字符串。操作 String 很耗资源,所以 Java 提供了两个工具类 StringBuffer 和 StringBuilder 来操作 String。
StringBuffer 和 StringBuilder 是可变类,StringBuffer 是线程安全的,StringBuilder 不是线程安全的。所以在多线程对同一个字符串操作的时候,我们应该选择用 StringBuffer,由于不需要处理多线程的情况,StringBuilder 的效率比 StringBuffer 要高。
二、如何停止一个线程?
终止线程有 3 种方式:
1、使用 stop 方法强行终止线程
但是这种方式是不安全的,主要有两点原因:
1)、thread.stop() 调用之后,创建子线程的线程就会抛出 ThreadDeathError 的错误,并且会释放子线程持有的所有锁。 2)、一个线程被 stop 停止并释放锁,其写入的内存数据写到一半没有被清除,但此时另一个线程得到锁发现此内存区块的数据是异常的。
2、使用 interrupt() 方法中断线程
这种方式的缺点在于线程不一定会终止。
Thread::Interrupted() 相比 Thread::IsInterrupted() 方法在native 底层仅仅多调用了一行 SetInterruptedLocked(false) 来清空当前的中断状态,其内部使用了 wait_mutex_ 互斥加锁来保证线程安全。
3、使用 volatile boolean 变量退出标志
这种方式能够使线程正常退出,也就是当 run 方法完成后线程终止。
因为 interrupt 是系统方法,并且使用了 JNI 调用、内部实现采用了加锁,且它的触发方式是采取了抛异常的方式,所以建议需要支持系统方法时采用中断,其它情况用 volatile boolean 标志位即可。
4、那如何终止线程池呢?
终止线程池有两种方式:
ExecutorService 线程池提供了 shutdown 和 shutdownNow 这样的生命周期方法来关闭线程池自身以及它所拥有的线程。
1、shutdown 关闭线程池:线程池不会立即退出,不再接受新的任务,但可以继续执行池子中已经添加到等待队列的任务。 2、shutdownNow 关闭线程池并中断任务:线程池不会立即退出,不再接受新的任务,也不再处理等待队列中的任务,并试图使用 Thread.interrupt() 去打断正在执行的任务。但是我们都知道,如果线程中没有 sleep、wait、Condition、定时锁等应用,interrupt() 是无法中断当前线程的,所以,ShutdownNow() 并不代表线程池就一定能立即退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
三、类加载机制
1、类加载的主要流程是怎样的?
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行加载、链接(验证、准备、解析)和初始化,最终形成可以被虚拟机直接使用的 Java 对象,这就是虚拟机的类加载机制,具体的流程如下:
1)、加载阶段
就是通过一个类的全限定名来获取其定义的二进制字节流,将这个字节流转化为方法区的运行时数据结构,然后在 Java 堆中生成一个代表这个类的 Class 对象,作为对方法区这些数据的访问入口。
加载阶段是开发人员可控性最强的阶段,因为开发人员可以自定义类加载器,对于数组而言,情况有所不同,数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。
2)、验证
它是链接阶段的第一步,这一阶段的目的是 为了确保 Class 文件的字节流中所包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。它包括:
1、文件格式校验:验证字节流是否符合 Class 文件格式的规范,例如是否以 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围内、常量池中的常量是否有不被支持的类型。 2、元数据校验:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,例如这个类除了 Object 外是否有父类。 3、字节码校验:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
3)、准备阶段
它是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
需要注意的是,这时候进行内存分配的仅仅包含类变量,不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆上。
其次,这里所说的变量初始值是该数据类型的零值,例如 0、null、false 等,而不是在 Java 代码中被显示赋予的值。
4)、解析阶段
它是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用以一组符号来描述所引用的目标,而直接引用则是直接指向目标的内存地址指针。
5)、初始化阶段
它是执行类构造器方法的过程。
类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,而编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
虚拟机会保证一个类的构造器方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其他线程都需要阻塞等待,而这也是静态内部类能实现单例的主要原因之一。
2、类加载的过程能举个具体的实例说明一下吗?
以 JsonChao jsonChao = new Person()为例进行说明:
1)、因为 new 用到了 JsonChao.class,所以会先找到 JsonChao.class 文件,并加载到内存中; 2)、执行该类中的 static 代码块,如果有的话,给 JsonChao.class 类进行初始化; 3)、在堆内存中开辟空间,以分配内存地址; 4)、在堆内存中建立对象的特有属性,并进行默认初始化; 5)、对属性进行显示初始化; 6)、对对象进行构造代码块初始化; 7)、对对象进行与之对应的构造函数初始化; 8)、将内存地址赋给栈内存中的 jsonChao 变量。
3、说说你对 Java 和 Android 类加载器的了解?
Java 类加载器包括 4 种:
BootstrapClassLoader:启动类加载器,使用 C++ 实现。 ExtClassLoader:扩展类加载器,使用 Java 实现。 AppClassLoader:应用程序类加载器,加载当前应用类路径的所有类。 UserDefinedClassLoader:用户自定义的类加载器。
Android 类加载器包括 3 种:
BootClassLoader:给系统预加载使用的。 PathClassLoader:给系统、应用程序加载 class 文件用的。 DexClassLoader:加载 apk、dex、zip 文件用的。
4、PathDexClassLoader 和 DexClassLoader 有哪些区别?
在 8.0(API 26)之前,它们二者的唯一区别是 第二个参数 optimizedDirectory,它是生成的 odex(优化的 dex)存放的路径。
PathClassloader 直接为空,而 DexClassLoader 是使用用户传进来的路径,所以 DexClassLoader 能够加载未安装的 apk/jar/dex,而 PathDexClassLoader 只能加载系统中已经安装过的apk。
而在 8.0(API 26)及之后,二者就完全一样了。
DexClassLoader 的参数含义如下:
dexPath:dex 文件以及包含 dex 的 apk 文件或 jar 文件的路径,多个路径用文件分隔符分隔,默认文件分隔符为:。 optimizedDirectory:Android 系统存放 ODEX 文件的路径。PathClassLoader 中默认使用 "/data/dalvik-cache",而 DexClassLoader 则需要我们指定 ODEX 优化文件的存放路径。 librarySearchPath:所使用到的 C/C++ 库的存放路径。 parent:这个参数的主要作用是为了保留 java 中 ClassLoader 的委托机制。
5、那 BootClassLoader 和 PathClassLoader 的异同有哪些?
BootClassLoader 是 PathClassLoader 的 parent,这里要注意 parent 与父类的区别。
但它们的创建时机是一样的,在 Zygote 进程启动调用 zygoteInit 的时候就创建了 BootClassLoader 和 PathClassLoader。
6、Class 加载的源码有看过吗?
Class 的加载方式有两种:Class.forName 和 ClassLoader.loadClass。
Class 文件最终被加载到内存当中,生成一个 Class 对象都是通过 class_linker.cp 当中的 DefineClass 实现的,而不同的是 Class.forName 之后还会对生成的 Class 对象进行初始化操作。
好了,今天的分享就到这里。
如果还想查看更多的高频问答分享,请扫描下方二维码查看:
END
参考链接:
1、深入理解Java虚拟机(第3版)
https://book.douban.com/subject/34907497/
2、Java编程思想 (第4版) https://book.douban.com/subject/2130190/
3、深入理解Android:Java虚拟机ART [Understanding Android Internals: ART JVM] https://item.jd.com/12510921.html
往期推荐
点击下方卡片关注 JsonChao,为你构建一套
未来技术人必备的底层能力系统
▲ 点击上方卡片关注 JsonChao,构建一套
未来 Android 开发必备的知识体系
欢迎把文章分享到朋友圈
星球门票出售火爆,本期星球优惠券仅剩最后 10 张,先到者先得,错过再无。