记录在《极客时间》平台上《java核心技术36讲》专栏的学习笔记
作者信息:杨晓峰 前Oracle首席工程师
课程海报如下:欢迎扫描海报右下方二维码订阅(有优惠哦)
- 开篇词,以面试题为切入点有效提升Java内功
- 第1讲 谈谈你对Java平台的理解
- 第2讲 Exception和Error有什么区别?
- 第3讲 谈谈final、finally、finalize有什么不同?
- 第4讲 强引用、软引用、弱引用、幻象引用有什么区别?
- 第5讲 String、StringBuffer、StringBuilder有什么区别?
- 第6讲 动态代理是基于什么原理?
- 第7讲 int和Integer有什么区别?
- 第8讲 对比Vector、ArrayList、LinkedList有何区别?
- 第9讲 对比Hashtable、HashMap、TreeMap有什么不同?
- 第10讲 如何保证集合是线程安全的?
- 第11讲 Java提供了那些IO方式?NIO如何实现多路复用?
- 第12讲 Java有几种文件拷贝方式?哪一种最高效?
- 第13讲 谈谈接口和抽象类有什么区别?
- 第14讲 谈谈你知道的设计模式
- 第15讲 synchronized和ReentrantLock有什么区别?
- Java学习和面试看法
- 第16讲 synchronized底层如何实现?什么是锁的升级和降级?
- 第17讲 一个线程两次调用start()方法会出现什么状况?
- 第18讲 什么情况下Java程序会产生死锁?如何定位、修复?
- 第19讲 Java并发包提供了哪些并发工具类?
- 第20讲 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别与联系?
- 第21讲 Java并发类库提供的线程池有哪几种?分别有什么特点?
- 第22讲 AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用?
- 第23讲 类加载过程,什么是双亲委派模型?
- 第24讲 有哪些方法可以在运行时动态生成一个Java类?
- 第25讲 JVM内存区域的划分,哪些区域可能发生UotOfMemoryError?
- 第26讲 如何监控和诊断JVM堆内存和堆外内存使用?
- 第27讲 Java常见的垃圾收集器有哪些?
- 第28讲 谈谈你的GC调优思路?
- 第29讲 Java内存模型中的happen-before是什么?
- 第30讲 Java程序运行在Docker等容器环境有哪些新问题?
- 第31讲 Java应用开发中的注入攻击
- 第32讲 如何写出安全的Java代码?
- 第33讲 后台服务出现明显“变慢”,谈谈你的诊断思路?
- 第34讲 “Lambda能让Java程序慢30倍”你怎么看?
- 第35讲 JVM优化Java代码时都做了什么?
- Java工程师必读书单
- 第36讲 谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的应用和原理
- 第37讲 谈谈SpringBean的生命周期和作用域
- 第38讲 对比Java标准NIO类库,你知道Netty是如何实现高性能的吗?
- 第39讲 谈谈常用的分布式ID的设计方案?Snowflake是否受冬令时切换影响?
- 结束语 技术没有终点
- Java并发
- 完结
- Java初级、中级工程师要求:扎实的Java和计算机科学基础,掌握主流开发框架的使用。
- Java高级工程师考察Java IO/NIO,Java虚拟机,底层源代码,分布式、安全和性能领域。
- 涉及内容:Java基础,Java进阶,Java应用开发扩展,Java安全基础,Java性能基础
问题:谈谈你对Java平台的理解?“Java是解释执行”这句话正确吗?
- Java是面向对象的语言,显著特点:(1)write once run anywhere;(2)垃圾回收,内存的分配和回收;
- JRE是Java运行环境,包括JVM和Java类库及一些模块;JDK是JRE的一个超集,提供了更多的工具如编译器、各种诊断工具;
- Java源代码首先通过javac编译成为字节码,然后通过JVM内嵌的解释器讲字节码转化为机器码。但是我们使用的Oracle JDK提供的Hotspot JVM提供了JIT编译器(动态编译器),可以在运行时将热点代码编译成机器码,这时候就是编译执行。
- Java源代码经过javac编译成“.class”类型的字节码,在运行时JVM通过类加载器(Clacc-Loader)加载字节码,解释或者编译执行。-Xint参数表示只进行解释执行;-Xcomp参数表示关闭解释执行;-Xmixed参数表示混合模式;
- 其他新的编译方式:AOT:直接将字节码编译成机器代码,Java9中就实验性的引入AOT特性。
- Java语言的基本特性:面向对象,反射,泛型、Java类库:集合,网络,安全、Java虚拟机:垃圾收集器,运行时,动态编译,辅助功能、Java工具:jlink,jar,javac,sjavac,jmap,jstack、Java生态:spring,hadoop,spark,elasticsearch,maven.
问题:对比exception和error,运行时异常与一般异常有什么区别?
- exception和error都继承了throwable类,Java中只有throwable类型的实例才可以被抛出或者捕获;
- exception和error是Java平台设计者对不同异常情况的分类处理。exception是程序正常运行中,可以预料的意外情况,应该被捕获并处理;error是不大可能出现的情况会导致程序处于非正常的不可恢复的状态;
- exception可分为可检查异常和不可检查异常,可检查异常在源代码中必须显式进行捕获处理,不检查异常就是运行是异常。
- 常见error:LinkageError,NoClassDefFoundError,ExceptionInInitializerError,VirtualMachineError,OutOfMemoryError,StackOverflowError;常见exception:IOException,RuntimeException,ClassNotFoundException;
- ClassNotFoundException当动态加载class的时候找不到类会抛出,一般在执行class.forName(),classLoader.loadClass()时候抛出;NoClassDefFoundError当编译成功以后执行过程中class找不到导致抛出该错误由JVM的运行时系统抛出。
- 异常处理的基本语法:try-cache-finally,throw,throws
- 异常处理注意:1尽量不要捕获类似于exception这样的通用异常,应该捕获特定异常;2不要生吞异常,不要假设这段代码可能不会发生。
- try-cache代码段会产生额外的性能开销,要仅对有必要的代码段进行捕获,不用异常进行代码流程控制;Java每实例化一个exception都会对当时的栈进行快照,这是一个比较重的操作。
- final可以用来修饰类、方法和变量,final修饰的类不可以继承扩展,修饰的变量不可以修改,修饰的方法不可以重写。
- finally是Java保证重点代码一定要被执行的一种机制,可以使用try-finally来进行类似关闭JDBC连接、保证unlock锁等动作。
- finalize是基础类java.lang.Object的一个方法,目的是保证对象在被垃圾收集前完成特定资源的回收。
- final可以防止API被更改保证平台安全,可以避免意外赋值导致的编程错误,保护只读数据,减少额外开销。
- final只能约束strList这个引用不可以被赋值,但是strList对象的行为不被final影响。List.of()创建的本身就是不可变的List.
- 实现一个immutable类需要做到:class自身声明为final,成员变量定义为private和final且不实现set方法,构造对象时成员变量使用深度拷贝来进行初始化,实现get方法时使用copy-on-write原则建立私有copy。
- finally中的代码不被执行的情况:1 try-cache异常退出,system.exit(1),2 无限循环,3 线程被杀死;
不同的引用类型主要体现在对象不同的可达性状态和对垃圾收集的影响。
- 强引用:(直接调用,不回收)最常见的普通对象引用,只要还有强引用指向一个对象就能表明对象还“活着”,垃圾收集器不会进行收集。对于一个普通的对象,如果没有其他引用关系,只要超过了引用的作用域或者显式地将强引用赋值为null,就可以被收集。
- 软引用:(通过get()方法,视内存情况回收)可以豁免一些垃圾收集,只有当JVM认为内存不足时才会去试图回收软引用指向的对象。软引用通常用来实现内存敏感的缓存,保证使用缓存的同时不会耗尽内存。
- 弱引用:(通过get()方法,永远回收)不能豁免垃圾收集,仅仅提供一种访问在弱引用状态下对象的途径。例如维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重新实例化,是很多缓存实现的选择。
- 幻想引用(虚引用):(无法取得,不回收)不能通过他访问对象。提供了一种确保对象被finalize以后做某些事情的机制。Java平台自身的cleaner机制,利用幻想引用监控对象的创建和销毁。
- 除了幻想引用,如果对象还没有被销毁,可以通过get方法获取原有对象,利用软引用和弱引用可以改变对象的可达性状态,如果错误的保持了强引用,对象就不能变回类似弱引用的可达性状态了,就会产生内存泄露。
- string是Java语言非常基础和重要的类,是immutable类,其不可变性导致类似拼接、剪裁字符串等动作都会产生新的string对象。
- stringbuffer可以使用append或者add方法把字符串添加到已有序列的末尾或者指定位置,是一个线程安全的可修改字符串序列。
- stringbuilder去掉了线程安全,有效减小了开销。
- 字符串设计和实现考量:stringbuffer中通过添加synchronized关键字实现线程安全。构建时初始字符串长度加16,底层采用char(JDK9之后是byte)数组。
- 字符串缓存:intern是一种显式地排重机制。
- string自身的演化:Java9之前string采用char数组存储,Java9中通过一个byte数组加上一个标识编码来存储。
谈谈Java反射机制,动态代理是基于什么原理?
- 反射机制是Java语言提供的一种基础功能,通过反射可以直接操作类或者对象,例如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
- 动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,例如用来包装RPC调用、面向切面的编程(AOP)。JDK中的动态代理基于反射机制,还有字节码操作机制,类似ASM,cglib,javassist。
- 反射机制及其演进:通过反射该类对我们是完全透明的,想要获取任何东西都可以,Java9以后只有当被反射操作的模块和指定的包对反射调用者模块open,才能使用setAccessible。
- 动态代理:通过代理可以让调用者与实现者之间解耦。Spring AOP 支持两种模式的动态代理,JDK Proxy和cglib,cglib对接口的依赖被克服,cglib动态代理采取创建目标类的子类的方式。前者最小化依赖关系,可以平滑进行JDK版本升级,代码实现简单;后者适用调用目标不便实现额外接口,只操作我们关心的类,高性能。
- int是Java的8个原始数据类型(Primitive Types,boolean,byte,short,char,int,float,double,long)之一;Integer是int对应的包装类,有一个int类型的字段存储数据,并且提供基本操作,Java5中引入自动装箱(Integer i = 10;自动调用valueOf(int)方法,注意在调用该方法时如果数值在[-128,127]之间返回cache中已经存在的引用对象,否则创建新的对象)和拆箱(Integer i = 10; int n = i;自动调用intValue方法)功能,equals方法不会进行类型转换。
- 自动装箱和拆箱:发生在编译阶段,保证生成的字节码是一致的。booble缓存了连个常量实例,short缓存了[-128,127],byte全部缓存,character缓存范围0000到007F。建议避免无意中的装箱和拆箱行为。
- 源码:缓存的上限值可以通过参数进行确定。Integer也是不可变类。如果有线程安全计算需要建议使用AtomicInteger,AtomicLong这样的线程安全类。部分比较宽的数据类型不能保证更新操作的原子性,可能出现程序读取到只更新了一半数据位的数值。
- 对象的内存结构:Mark Word:标记位4字节,偏向锁标记位;class对象指针4字节,指向对象对应class对象的内存地址;对象实际数据,对象所有成员变量;对齐填充字节,按照8个字节填充。可以通过jol,jmap查看
这三者都是实现集合框架中的list即有序集合,5提供按照位置进行定位、添加或者删除的操作以及迭代器遍历内容。
- vector是java早期的线程安全的动态数组,同步过程需要有额外的开销。内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。
- arrayList是应用更加广泛的动态数组实现,本身不是线程安全的,性能要好一些。可以根据需要进行容量调整,vector在扩容时会提高1倍,arrayList增加50%。
- linkedList是java提供的双向链表,不是线程安全的。
- vector和arrayList作为动态数组内部以数组进行存储,适合随机访问场合,除了尾部插入和删除元素外其他操作性能往往较差。linkedList进行结点插入和删除要高效但是随机访问性能要比动态数组慢很多。
- 三大集合类:list(有序集合),set(不允许重复元素),queue/deque(标准队列结构,除集合基本功能还支持类似FIFO操作)
- treeSet支持自然顺序访问但是添加、删除、包含等操作低效(logn);hashSet若哈希散列正常,可以提供常数时间的添加、删除、包含操作,但是不保证有序;linkedHashSet支持按照插入顺序遍历的能力,保证常数时间的添加、删除和包含等操作;hashSet性能受自身容量影响,初始化时不要将其背后的hashmap容量设置过大。
- java中原始数据类型采用双轴快速排序,对象数据类型使用TimSort思想上是一种归并和二分插入排序。java8引入了并行排序算法。
hashtable、hashmap、treemap都是最常见的一些map实现,是以键值对的形式存储和操作数据的容器类型。
- hashtable是早期java类库中哈希表的实现,本身是同步的,不支持null键和值。由于同步开销很小被推荐使用。
- hashmap不是同步的,不是线程安全的,支持null键和值,通常情况下进行put或者get操作可以达到常数时间的性能。
- treemap基于红黑树的一种提供顺序访问的map,其get,put,remove之类的操作都是logn时间复杂度,具体顺序有comparator来决定或者根据键的自然顺序来判断。
- hashmap的性能表现非常依赖于哈希码的有效性,哈希码和equals有一些基本约定,compareTo的返回值需要和equals一致,equals相等hashcode一定要相等;重写了hashcode也要重写equals;hashcode需要保持一致性,状态改变返回的哈希值仍然要一致;equals的对称、反射、传递等特性;
- LinkedHashMap通常提供的是遍历顺序符合插入顺序,实现是通过为条目维护一个双向链表。对于treemap它的整体顺序是由键的顺序关系决定的,通过comparator或comparable来决定。
- hashmap中hash值的源头,其并不是key本身的hashcode,而是来自于hash方法,值为:key.hashCode()^key.hashCode>>16,原因是有些数据计算出的哈希值差异主要在高位,而hashmap里面的哈希寻址是忽略容量以上的高位的,这种处理就可以有效避免类似情况下的哈希碰撞。
- resize()方法中,门限值等于 负载因子乘以容量,如果没有指定就是依据相应的默认常量值;门限通常是以倍数进行调整;扩容后,需要将老的数组中的元素重现放置在新的数组是主要的开销来源。
- 容量和负载因子的存在主要为了保证存在合理的桶数量以达到最高效率。容量设置:要满足大于“预估元素数量/负载因子”,同时是2的幂数。负载因子:没有特别需求不要轻易更改使用JDK的默认负载因子,如果需要调整,建议值不要超过0.75,如果负载因子太小需要按照前面公式对预设容量进行调整,否则会导致频繁的扩容。
- hashmap进行树化本质上是安全问题,如果一个对象哈希冲突都放置在一个桶中会形成链表,影响存取性能,现实世界中构造哈希冲突的数据并不是非常复杂,恶意构造哈希冲突就会导致服务器端CPU大量占用,构成哈希碰撞拒绝服务攻击。
- 解决哈希冲突的方法:开放定址法,哈希地址p冲突时,以p为基础产生另一个哈希地址p1;再哈希法,同时构造多个哈希函数,一个不行尝试另外一个;链地址法,将所有哈希地址为i的元素构成一个同义词链的单链表,将单链表的头指针存在哈希表的第i个单元中;简历公共溢出区,凡是和基本表发生冲突的元素一律填入溢出表。