• 下面的笔记是看书时整理的,但因为我在看的时候会参考很多资料,笔记中会有很多部分并不来自于书中。
  • 关于String
    • 编译器通常会做常量折叠:例如 String a = "a"+"b"+1 会被编译为 String a ="ab1"
      • 如果这里的"b"换做final修饰的变量,也会被编译器优化折叠的
    • 关于常量池
      • Java的基本数据类型和String都使用了常量池
      • 常量池是JVM规范定义的,具体存放于哪个位置(PermGen/Heap)取决于JVM实现
      • 直接使用双引号声明出来的String对象会直接存储在常量池中,当然必须是常量折叠之后的
      • String.intern()
        • 如果常量池中存在当前字符串, 就会直接返回当前字符串;如果常量池中没有此字符串, 会将此字符串放入常量池中后(在JDK6及以下是拷贝对象,JDK7见下), 再返回
          • 由于JDK7的常量池实现移动到了Heap中,String#intern方法时,如果堆中存在字面相同的对象,会直接保存对象的引用,而不会重新创建对象
        • 内部实现:String常量池其实是一个HashMap(JDK6固定只有1006长度),因此intern()在常量池对象很多的情况下会变慢。(Hash冲突,链表上逐个进行字符串比对)
        • intern()通常用于类库中,实现String作为Key值的唯一性(例如添加的时候直接添加intern()过的String,查找的时候直接==比较即可)。在应用代码中很少见。
        • 参考文章 http://tech.meituan.com/in_depth_understanding_string_intern.html
    • String的+操作
      • a + b +"f" 会在字节码生成时编译成 new StringBuilder(a).append(b).append("f").toString()
        • 注意是以一行代码为粒度,即每一行new一个StringBuilder
      • 因此对于频繁进行+操作的String,使用StringBuilder可能会快一点因为会节省频繁创建StringBuilder对象和GC的时间
  • 基本数据类型的包装类
    • 基本类型和包装类型相互赋值时,会在编译阶段编译成转换方法(如Integer.valueOf)
    • 在基本数据类型转换为包装类型,会优先使用各个包装类的cache(类似常量池,本质是一个satic数组),cache范围内的值会始终返回相同的对象(此时==为true)。不在范围内或使用构造函数时会创建新对象(此时==为false)。
      • Integer, Short, Long的cache范围:默认-128~127
      • Byte的cache范围:全部
      • Boolean的cache范围:全部
      • Float, Double没有cache
  • 字节码增强
    • 常见的步骤:
      • 1. 从内存中获取原始字节码,然后通过一些API(例如ASM,Javassist,BCEL,SERP,CGLib)修改它的byte[]数组,得到一个新的byte[]。
      • 2. 然后通过Classloader加载这个byte[]来创建对象,或者加载/运行时替换原来的字节码(使用Instrumentation api)。
        • Classloader通过byte[]加载的方法是protected的,需要自己继承一个Classloader
        • Instrumentation机制,允许在类加载时进行对类的增强,以及在运行时对已经加载的类进行增强。但是必须将代码打包jar,以代理 (agent) 的形式通过参数导入JVM。
    • JDK的动态代理(java.lang.reflect.Proxy)也是通过字节码增强实现的
      • 使用时,使用Proxy.newProxyInstance(classLoader, new Class[]{InterfaceClazz}, invocationHandler)会返回一个代理对象,调用方直接使用这个代理对象就能实现透明调用。
        • 这个返回的代理对象的类就是sun.misc.ProxyGenerator.generateProxyClass()拼装的,拼装的结果就是一个extends Proxy implememts interfaceClazz的类,它对于InterfaceClazz的每个方法体,都写成了这样的形式:
          • 其中的h就是Proxy类的成员变量invocationHandler
        • 拿到这个类的byte[]后,会调用native方法defineClass0加载,得到Class对象,之后调用构造函数返回实例化后的这个代理对象。
  • JVM规范中的运行时数据区
    • 参见 http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html
    • PC(program counter)寄存器:每一条JVM线程都有自己的PC寄存器。如果执行的是java代码,PC寄存器保存JVM正在执行的字节码指令的地址(包括偏移量和实际内存地址),如果执行的是native代码,那么pc寄存器的值未定义——什么值都可以。
    • :每条JVM线程都有一个栈,栈中存储着栈帧(Frame)。栈帧在方法调用时创建,结束时销毁,用于保存方法调用时的局部变量、操作数栈、动态链接(记录方法在运行时常量池的引用)和方法方法出口。
      • 栈的内存地址不必连续,栈的大小可以是固定的也可以是动态的
        • 如果栈的大小固定,则会在超出大小时抛出StackOverflowError。
        • 如果栈的大小动态,则会在内存不足时抛出OutOfMemoryError。
    • 堆:被所有JVM线程共享。所有对象和数组都在这里分配内存,它通常由某种自动内存管理机制所管理,这种机制通常叫做“垃圾回收器”(GC)。
      • 堆的内存地址不必连续,堆的大小可以是固定的也可以是动态的。如果内存不足,抛出OutOfMemoryError。
    • 方法区:所有JVM线程共享。它存储着per-class数据,即加载过的类的信息、类中的final/static变量、运行时常量池、方法和构造函数的字节码。
      • 方法区的内存地址不必连续,大小可以是固定的也可以是动态的。如果内存不足,抛出OutOfMemoryError。
      • 运行时常量池:字节码中的常量池的运行时形式,包含了常量和引用。每个类/接口对应的常量池会在类装载时创建。
    • 本地方法栈:用于支持native方法的调用执行。
  • GC
    • 现在主流的JVM无一使用引用计数方式来实现Java对象的自动内存管理
    • Hotspot的GC方式:此处另开一篇文章描述,见 简述Hotspot的垃圾回收机制
  • 对象的内存结构
    • 从上到下按顺序:
      • Mark Word:存放标记位。32位系统下为4字节,64位系统下为8字节。
      • Class指针:指向Class对象。32位系统下为4字节,64位系统下开启指针压缩(默认开启,下同)时为4字节,不开启为8字节。
      • 父类属性区(如果多重继承,则会从继承树顶部开始)
      • 属性对齐(按照一个指针的字节数对齐)
      • 当前类属性区
      • 整个对象对齐区:整个对象按照8字节对齐
    • 属性区内的属性排布:
      • 和书写顺序无关,按照引用排最后、基本类型按照大小由大到小的顺序排列。
      • 属性内注意空隙对齐:按照一个指针的字节数对齐
      • 例如下图(已开启指针压缩),对象占24个字节。
  • Java IO
    • 关于缓冲
      • BufferedInputStream:默认持有一个大小8192字节的byte数组缓冲。每当有读请求时,如果下标范围在缓冲区中则直接返回,否则使用InputStream.read从内核中读入数据刷新缓存。
      • BufferedOutputStream:默认持有一个512字节的缓存数组,使用write()方法写数据时,实际上是先将数据写至缓存中。当缓存满时或者调用flush()时(包括BufferedOutputStream,一部分输出流也会在close()里调用flush()),再使用OutputStream.write方法把数据写进内核。
      • 写入内核/从内核中读入仅保证是操作系统级别的调用,例如磁盘IO和Socket IO在操作系统内核还会有一层缓冲
    • 获取一个用来读文件的BufferedReader:(BufferedWriter同理,JDK7+)
      • Files.newBufferedReader(Paths.get("C:/1.txt"),Charset.forName("GBK"))
    • java.nio.DirectByteBuffer
      • 谈到这个类之前,先谈谈写文件时,真正的内存流动过程:
        • 1. 用户调用FileOutputStream.write(),传入byte[]
        • 2. 内部调用private native void writeBytes(),byte[]传入JNI
        • 3. 在JNI代码中,使用GetByteArrayRegion()拷贝Java的byte数组成为native的byte数组(参见/openjdk/jdk8/jdk/src/share/native/java/io/io_util.c),然后执行write()系统调用。
        • 4. write()将byte[]拷贝至内核缓冲区
        • 5. 刷新内核缓冲区,数据落入磁盘
      • 使用DirectByteBuffer,会直接在操作系统层面分配一块JVM堆外内存,这样就可以避免JVM memory→native memory的拷贝,使操作系统可以直接使用要写入的数据。
        • 注意,DirectByteBuffer分配内存时的开销远大于在JVM内分配内存的开销
        • DirectByteBuffer的内存可以被垃圾回收(不是被垃圾回收器,而是通过内部类DirectByteBuffer线程)
        • 分配的最大值根据 -XX:MaxDirectMemorySize
      • 应用
        • Netty的“零拷贝”
        • 文件的拷贝
    • 内存文件映射(MappedByteBuffer
      • 原理:直接使用操作系统虚拟内存技术。在虚拟内存中分配一块地址(Linux为进程内存空间中),并将文件本身作为其交换分区。程序对该文件的读写就会被直接映射到虚拟内存地址上,使得磁盘操作变为了内存操作。(对应Linux的mmap)
      • 优点:对于频繁读写的文件和大文件可以很大的提高读写速度。
      • 写文件示例:
      • 即使不调用force(),操作系统也会定期刷盘,默认30s(Linux)
      • 经实测,在同样要求强制落盘的情况下,顺序使用文件映射写文件比普通写文件(rws/rwd)快一倍左右。
        • 普通写文件(RandomAccessFile)有一种可以达到和文件映射几乎相同性能的方法:先使用seek到文件末尾写一位数据来形成文件空洞,之后正常写入即可。(在文件较大时甚至这种方法更快)
        • 使用seek随机读/写文件相比顺序读/写的确会降低速度。但是在小文件(MB以下)中性能的影响几乎可以忽略不计。
        • 以上测试均在SSD和机械硬盘得出了相同的结论
      • 如何解除文件映射略微麻烦点,参见:http://stackoverflow.com/questions/2972986
  • 并发
    • 关于volatile
      • 作用一:可见性。对一个volatile变量的写,其他线程总是立刻能看到。
        • 由于各级缓存和寄存器(也就是JMM所指的线程本地内存)并没有做到与主内存实时同步,因此普通变量的可见性并不作保证。
        • (有观点认为缓存不同步也是内存系统重排序的一种,此处不做字面区分)
      • 作用二:防止相关性代码的重排序,无论是编译重排序还是处理器重排序。
        • 为了提高执行效率,大多数现代CPU和JIT编译器都会做指令重排序,在重排序时会保证单线程下的as-if-serial语义。
        • 通常来说,重排序的规则如下表所示(尽管JMM没有作此规定):
          • 其中第二个操作指的是第一个操作后面的所有操作
          • 能重排序指的是不违反as-if-serial语义和JMM规定的happens-before规则(此处略)
          • 如果一个volatile变量只能被单线程访问,那么也可能会把它做为普通变量来处理
        • 为了禁止重排序,JVM会根据下述规则设置内存屏障(Memory Barrier,一种CPU指令),Java编译器也会遵守这一规则
          • LoadLoad屏障:对于Load1; LoadLoad; Load2,保证load1在load2之前读取完毕。
          • StoreStore屏障:对于Store1; StoreStore; Store2,在Store2及后续写入执行前,保证Store1的写入对其它处理器可见。
          • LoadStore屏障:对于Load1; LoadStore; Store2,在Store2及后续写入可见前,保证Load1读取完毕。
          • StoreLoad屏障:对于Store1; StoreLoad; Load2,在Load2及后续读取执行前,保证Store1的写入对所有处理器可见。
            • 开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
          • 以上四种屏障仅是对CPU内存屏障的一种抽象,不同CPU的屏障指令与之对应方式各不相同。
        • 上述参考自 Java内存访问重排序的研究 & The JSR-133 Cookbook for Compiler Writers
    • 为什么wait()和notify()需要在synchronized同步块中?
      • 操作系统提供的条件变量也需要与互斥锁同时使用,二者道理相同。
      • 其根本原因在于:与wait/notify相关的逻辑操作必须是原子性的
        • 一个反面例子如下:
        • 上述两个线程,假设thread1先于thread2启动。如果在thread1执行完while(!condition)的判断后被thread2中断,那么thread2的notify就会失败。解决办法也就是让两段代码进入临界区,即使用互斥锁或synchronized。
      • 注意:
        • thread1使用了while进行条件判断,目的是防止虚假唤醒(小概率事件),这也是Java/操作系统API提出的要求。
        • 当然,你也要知道线程wait的时候其实释放了互斥锁的。释放锁-等待 这两步由操作系统api保证其原子性。
    • AbstractQueuedSynchronizer