String对象是如何实现的?
- Java6以及之前的版本,String是对char数组进行了封装实现的对象,主要有,char数组,偏移量offset,字符串数量count,哈希值
- Java7到8String类中不再有offset和count两个变量了,String占用的内存稍微小些,同时,String.substring方法不再共享char,解决了可能导致内存泄漏的问题
- java9开始,char[]字段改为了byte[]字段,维护了一个新的coder,它是编码格式的标识
编译器会对,str+str进行优化,改成StringBuilder,每次+都会new一个新的StringBuilder的对象,所以,建议直接使用StringBuilder来优化
String的内存分配
- String str = "abc" 代码编译时,会在常量池中创建常量abc,运行时返回常量池中的字符串的饮用
- String str = new String("abc"),代码在加载时会在常量池中创建常量abc,在调用new时,会在堆中创建String对象,并且引用常量池中字符串对象char[]数组,并且返回String对象的引用
- public class Test{String test1,String test2}(String 为成员变量),在运行时,创建的String对象会在堆中,不会在常量池中创建
注意:使用intern方法需要注意一点,一定要结合实际场景,因为常量池实现类似于一个HashTable的实现方式,存储的数据越大,遍历的时间复杂度就会提升
正则表达式
目前实现正则表达式引擎的方式有两种,DFA(确定有限状态自动机)和NFA(非确定有限状态自动机),对比来看,构造DFA自动机的代价远大于NFA自动机,但DFA自动机执行效率高于NFA,NFA自动机的优势时支持更多的功能,例如,捕获group,环视,占有优先量词等高级功能,在编程语言里使用的正则表达式引擎都是基于NFA实现的
NFA自动机的回溯,NFA实现会引起大量回溯问题,比如ab{1,3}c,source=abbc,匹配b之后会不断的回溯来看是否满足,贪婪模式就是回溯的导火索
贪婪模式就是,如果单独使用+、?、*或者{minx,max}等量词,正则表达式会匹配尽可能多的内容
懒惰模式,在这种情况下正则表达式会尽可能少的重复匹配字符串,如果匹配成功则会继续匹配剩余的字符串,开启懒惰模式可以在量词后面拼接?,如"ab{1,3}c",如果匹配的结果是abc那么一次匹配即可成功
独占模式,独占模式和贪婪模式一样会最大限度的匹配更多的内容,不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯的问题,开始独占模式在字符串后面增加个+,如"ab{1,3}+bc"
正则表达式的优化
- 少用贪婪模式多用独占模式
- 减少分支的选择,如"abcd|abef"替换为"ab(cd|ef)"
- 减少捕获嵌套
I/O
- 传统I/O和NIO的最大区别就是传统I/O是面向流的,NIO是面向buffer的,Buffer可以将文件一次性读入内存再做后续的处理,而传统的方式是边读文件边处理数据,虽然传统I/O后面也是用了缓存快,如:BufferedInputStream,但是仍不嫩隔阂NIO媲美
- NIO的Buffer除了做缓存快块优化外,还提供了一个可以直接访问物理内存的类,DirectBuffer,普通的Buffer分配的是JVM堆内存,而DirectBuffer是直接分配的物理内存,
数据要输出到外部设备,必须先从用户空间复制到内核空间,再复制到输出设备,而在Java中,在用户空间中又存在一个拷贝,那就是从java堆内存拷贝到临时的直接内存中去,通过临时的直接内存拷贝到内存空间中去,此时的直接内存和堆内存就是属于用户空间(为什么Java要通过一个脸是非堆内存来复制数据?如果单纯使用Java堆内存进行数据拷贝,当拷贝的数据量比较大的情况,Java堆的GC压力会比较大,而使用非堆内存可以简低GC压力),DirectBuffer则是直接将步骤简化为直接保存数据到非堆内存中,从而减少一次拷贝
DirectBuffer申请的是非JVM的物理内存,所以创建和销毁的代价很高,DirectBuffer申请的内存并不是直接由JVM负责垃圾回收,但在DirectBuffer包装类回收时,会通过JavaReference机智来释放盖内存块
DirectBuffer只优化了用户空间内部的拷贝,MappedByteBuffer时通过本地类调用mmap进行文件内存映射的,map()系统方法会直接将文件从硬盘拷贝到用户空间,只进行一次数据拷贝,从何减少了read方法从硬盘拷贝到内核的这一步
零拷贝,DirectBuffer和MappedByteBuffer
偏向锁
锁状态流转
上下文切换
上下文切换是线程之间切换的时候保存之前线程的状态,加载新线程的数据,再垃圾回收的时候就可能导致Stop-the-world,就是线程暂停的行为
Linux可以使用 vmstat命令来查看切换的频率(vmstat -pid),如果监视某个应用上下文的切换,就可以使用pidstat命令监控指定进程的Context Switch上下文切换(pidstat -w -l -p <pid> 1 100)
什么时候会导致上下文切换
- 系统原因,如分配的时间片到了,I/O中断等
- 程序中调用slepp,wait,park等导致线程状态的改变
所以,在多线程中,锁其实不是性能的开销,竞争锁才是为了提高性能,优化点有
- 减少锁持有的时间,只在关键竞争处增加锁
- 降低锁的粒度,如锁分离,锁分段(ConcurrentHashMap)
- 乐观锁代替竞争锁
- wait和notify的优化,建议使用Lock锁结合Condition接口代替
- 合理设置线程池的大小,避免创建过多的线程,根据自己的业务场景,一般在N+1和2N两个公式中选择一个合适的
- 使用协程实现非阻塞等待
- 减少JAVA垃圾回收
线程模型
实现线程的主要有三种方式:
- 轻量级进程和内核线程一对一互相映射实现1:1线程模型(JAVA在LINUX下的实现)
- 用户线程和内核线程实现N:1线程模型
- 用户线程和轻量级进程混合实现N:M线程模型
1:1线程模型,通过fork函数创建一个子进程来代表一个内核中的线程,一个进程调用fork函数后系统会给新的进程分配资源,然后把原进程中所有的值都复制到新的进程中,只有少数的与原来不同,如:PID,由于每次都需要复制一摸一样的数据,LWP进行了优化使用clone函数来创建线程,没有复制的资源可以通过指针共享给子进程
N:1线程模型,由于1:1线程模型是和内核一对一映射,所以创建,切换都存在用户态和内核态的切换,开销比较大,同时由于系统资源有限,不能支持创建大量的LWP,但是N:1可以很好的解决这些问题,一个内核线程管理映射多个用户线程,所以用户线程切换的时候不会产生用户态和内核态的切换
N:M线程模型,主要解决了N:1线程模型如果一个线程阻塞,就会导致整个进程被阻塞,N:M通过LWP与内核线程链接,用户态的线程数量和内核态的LWP数量是N:M的关系
Java使用了1:1的线程模型,而Go协程实现了N:M的线程模型,所以Go语言并发行支持的更好,不需要上下文之间的切换(协程实现的本质就是在程序总实现函数的调度)
JAVA虚拟机
Java8为什么使用元空间替代永久代(方法区的实现)
- 为了融合HotSpot JVM与JRockit VM,因为JRockit没有永久代
- 永久代经常不够用或发生内存溢出,每次PermGen区FullGC的时候回收率偏低
类加载执行过程
- 类编译,把.java文件编译成.class文件
- 类加载,当类被创建实例或者被其他对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载将字节码文件加载到内存中,加载完成后class类文件常量池信息以及其他数据会被保存到JVM内存的方法区中
- 类连接,验证:验证是否符合规范,准备:为类的静态变量分配内存,初始化值,如int常量初始化0(此时还没有真正赋值),解析:将符号引用转为直接引用,包括类和接口的全限定名,类引用,方法引用以及成员变量引用等
- 类初始化,执行构造器的<clinit>方法,为变量赋值
- 即时编译(JIT),初始化完成后,类在调用执行过程中,执行引擎会把字节码转成机器吗,然后操作系统才能执行,字节码转机器码的过程中虚拟机还存在一道编译,那就是即时编译,HotSpot虚拟机中内置了两个JIT,分别为C1和C2编译器,这两个编译器过程是不一样的,C1编译器是一个简单快速的编译器,主要关注在局部性优化(如GUI程序),C2编译器是为长期运行的服务器程序做的性能调优