Java核心技术36讲
最后更新于
最后更新于
主要是站在整体的角度来审视Java语言
语言特性
基础类库
JVM
JDK提供的工具
JIT(即时编译器)中的C1对应着虚拟机的Client模式,适用于启动速度敏感的应用。C2对应着Server模式,他的优化是为了长期运行的程序设定的,默认采用所谓的分层编译
Java9提供了平台相关性更强的编译性工具AOT,直接将某个类或者模块编译成AOT库,避免虚拟机启动时候过长时间的预热(适应微服务的一种体现吧)
提供的工具,JDK里面就有了:jaotc,但是使用起来感觉一般,估计还得需要时间普及
主要讨论的是Throwable类的两个实现:Exception和Error、以及运行时异常和一般异常有什么区别
Exception是在程序运行情况下可以预料到的意外情况,应该捕获并进行处理
Error是正常情况下不大可能出现的情况,会导致程序(JVM)处于非正常的,不可恢复状态,例如OutOfMemoryError类
Exception可以分为检查check异常和不检查uncheck异常,检查异常需要在编译器强制要求处理,不检查异常就是所谓的运行时异常,例如NullPointException、ArrayIndexOutOfBoundsException之类,不会在编译器强制要求处理
简单类图:
Java对异常处理的支持(应该是Java7之后的):主要就是Try-with-resources和Multiple catch两个语法糖的加入
几个常见的使用误区:
尽量不要捕获Exception这样的通用异常,而是需要将捕获异常具体化
不要生吞异常,就例如catch不作任何异常处理,诊断起来非常难
e.printStackTrace(); 不要在产品代码中使用如下代码,很难判断出到底输出到哪里去了,特别是对于分布式架构的系统更为如此
尽量遵循Throw early,Catch late原则
throw early:也就是对参数进行检查,避免最常规的空指针异常问题,例如以下的两段代码:
new FileInputStream都可能会因为传入的filename为null而报错,而1中生成的异常堆栈信息就不是很适合读取了,而2中可以直接定位到第二行代码报错,非常容易解决
catch late:中庸的办法:保存原有的cause信息,直接再抛出或者是构建新的异常再抛出
自定义异常:
自定义异常是否需要声明成checked Exception
几点不需要声明成checked Exception的例子:
Checked Exception的初衷就是为了从异常情况恢复,但是绝大多数情况不支持
不兼容functional编程,lambda和Stream无法使用
Spring、Hibernate就基本抛弃了Checked Exception,例如新的编程语言Scale就直接抛弃掉了Checked Exception
当然也不是绝对,在少数环境下异常是可以恢复的,如IO,网络的重复连接等等。
在保证诊断信息足够的前提下,避免敏感信息存储到了异常中去了,例如Java中的Connection refused异常就不会包含用户的IP,端口号,机器名的输出。
异常处理的性能开销:
try-catch会产生额外的性能开销,并往往会影响JVM对代码的优化,因此try代码块不要过大
实例化Exception会生成一个栈的快照,重量级操作
第二点也是优势的地方,可以大大减少代码诊断的难度,尤其是在分布式项目和大型项目中
课后问题:对于反应式编程,因为其本身是异步,基于事件处理的,所以出现异常决不能简单的抛出去,另外,因为代码堆栈不是垂直调用形式,生成的异常和日志往往都是特定executor的堆栈,而不是业务方法调用关系,对此你有什么想法呢?
优质回答:
先说问题外的话,Java的checked exception总是被诟病,可我是从C#转到Java开发上来的,中间经历了go,体验过scala。我觉得Java这种机制并没有什么不好,不同的语言体验下来,错误与异常机制真是各有各的好处和槽点,而Java我觉得处在中间,不极端。当然老师提到lambda这确实是个问题... 至于响应式编程,我可以泛化为异步编程的概念嘛?一般各种异步编程框架都会对异常的传递和堆栈信息做处理吧?比如promise/future风格的。本质上大致就是把lambda中的异常捕获并封装,再进一步延续异步上下文,或者转同步处理时拿到原始的错误和堆栈信息
作者回复:
是的,非常棒的总结,归根结底我们需要一堆人合作构建各种规模的程序,Java异常处理有槽点,但实践证明了其能力; 类似第二点,我个人也觉得可以泛化为异步编程的概念,比如Future Stage之类使用ExecutionException的思路
final可以修饰类,变量,方法。被final修饰的方法默认是不可重写的
final!=immutable:只能约束引用不被另外赋值,无法保证添加元素等操作是正常的,如被final修饰的List依然可以执行add操作,如果要保证绝对的immutable,可以借助List.of()方法等相应的类和方法来进行支持,Java没有提供原生的支持
如果要实现immutable的类,需要注意以下几点:
将class自身声明成final,避免别人使用扩展类来绕开限制
向所有成员变量定义成private和final的,不实现setter方法
构造函数进行赋值时候要使用深拷贝来代替浅拷贝,避免被final修饰的内部对象改变了的问题
如果需要实现getter方法,使用copy-on-write原则,复制出一个副本
finally保证重点代码一定会被执行,如资源的关闭,锁的unlock动作等
不过现在更加推荐Java7的try-with-resources语句
也有特例:如以下代码就不会产生输出:
finalize是Object的一个方法,保证在对象被垃圾回收前完成特定的动作,从JDK9开始不推荐使用
会导致对象回收变慢,大约是四十到五十倍的速率变慢
Java平台正在使用Cleaner来逐步替换掉原有的finalize实现,比finalize更加轻量和可靠。仍然是有缺陷的,要避免所有对对象的强引用,否则很容易造成内存泄漏
强引用,软引用,弱引用,虚引用(幻象引用)
不同的引用类型,主要影响的就是对象的可达性状态和垃圾收集的影响
强引用:无论如何都不会被回收
软引用:JVM会在确保抛出OutOfMemoryError之前清理软引用对象,常用于实现内存敏感的缓存
弱引用:无法使得对象豁免垃圾收集,仅仅提供对象的一种访问途径。例如维护一组非强制性的映射关系,如果对象还在就使用,否则就重新实例化,也是很多缓存的实现
虚引用:无法通过虚引用来访问对象,虚引用仅仅提供了确保对象被finalize以后,做某些事情的机制
对象引用的转换关系:
String:是一个经典的Immutable类,被声明成final class,所有属性也都是final的,导致字符串拼接,裁减等操作都会产生一个新的String对象,会对性能有明显的影响
StringBuffer:提供线程安全的方式来修改字符序列,也正是由于其线程安全,导致其效率不高,所以除非有线程安全的需要,还是建议使用StringBuilder(因为大部分的对象都可以是线程私有的,绝对的线程安全)
StringBuffer是直接将修改数据的方法加上Synchronized来实现的,非常直白,不必纠结于Synchronized性能之类的,有人说:“过早优化是万恶之源”,考虑可靠性,正确性和代码可读性才是大多数应用开发最重要的因素
StringBuilder:1.5以后新增的一个类,相当于去掉了线程安全的StringBuffer,是绝大多数情况下字符串拼接的首选
通常不用使用到以上这些类,在JDK8之前Javac操作会直接将其转换为StringBuilder的实现,在Java8之后则不会再Javac时期执行优化了,而是直接使用JVM的InvokeDynamic来进行优化
String的存储方式从JDK9之前的使用char数组变成使用一个byte数组加上一个标识编码的coder以减少Char占用两个字符所带来的额外的内存开销
谈到动态代理就不得不谈到反射机制
反射机制是Java语言层面提供的一种机制,允许我们可以直接操作类或者是对象,例如:获取某个对象的类定义,获取类声明的属性和方法,调用方法或者是构造对象,甚至可以运行时修改类定义
动态代理是一种方便运行时动态构建代理,动态处理代理方法调用的机制。使用到的场景有:AOP面向切面编程,包装RPC调用等等
实现动态代理的方法有很多,例如JDK自身提供的动态代理,主要就是利用了反射机制,还有更高级别的字节码操作机制:类似cglib,ASM等
动态代理底层并不一定是由反射来实现的
Java语言通过本省语言的反射机制做到了灵活操作很多运行时才能确定的信息,而动态代理则是延伸出的一种广泛应用于产品开发中的技术,很多繁琐的重复操作可以通过动态代理优雅的解决
动态代理是一个代理机制,代理可以被看做是对调用目标的一个包装,这样对目标代码的调用不是直接发生的,而是通过代理完成,有点python装饰器的味道了
通过代理可以使得调用者与实现着之间的解耦,例如常规的序列化,反序列化对调用者来说是毫无意义的,通过代理可以提供更友善的界面,也为应用插入额外的逻辑提供了便利的入口
Java提供的动态代理使用:
仍然需要实例化Proxy对象,而不是真正的调用类型,带来了一定的不便
如果使用另一种方式(参考Spring AOP实现的两种方式) cglib就完全可以避开Proxy对象的使用,直接操作接口。cglib使用的是创建目标类的子类的方式来实现的,就可以直接通过Java的向上转型机制来完成很好的调用
且cglib拥有跟高的性能和更流畅的编码体验,但是在JDK版本升级的时候不如JDK Proxy那般过度平滑,且编写门槛要低得多
包装类都设有缓存,以防止太过频繁的创建对象导致性能的提前降低
只是包装类缓存了数值,对应的基本类型并没有缓存有相应的数据
包装类的缓存范围:
Integer:-128~127
Boolean:true/false
Short:-128~127
Byte:数值有限,全部缓存来了,表示范围 -128~127
Character:缓存了从'\u0000'~'\u007F'
的字符
在实战中,建议避免自动装箱和自动拆箱的行为
但还是以开发效率为先
之所以泛型不支持原始数据类型,主要是因为:在javac的过程当中需要将泛型全部都向上转型称为Object对象,而原始数据类型是无法转换成为Object对象的,如果每次转换都需要单独处理会显得过于慢了
其实使得Java的List无法存储原始的基本数据类型也是一件好事情,因为List存储的都是对象的引用,对象通常都分布在堆的各个区域当中,无法保证对CPU缓存的最大利用。而单独的原始类型数组则可以很好的利用CPU的缓存机制。
所有技术都是有利有弊的,例如这个存储对象的分布虽然降低了缓存的利用率,但也极大的避免了JVM的垃圾收集器工作过于频繁的问题。
当然,OpenJDK现在正在致力于解决这些问题。
主要的成员有:Vector、ArrayList、LinkedList
Vector是早期的线程安全的动态数组,可以根据需要进行扩容,扩容时候会创建新的数组并拷贝原数组的数据
ArrayList是动态数组的实现,不是线程安全的,效率高很多,也存在扩容问题,与Vector不同,Vector是直接扩容1倍,而ArrayList仅仅只是扩容50%
LinkedList是双向链表,不存在扩容问题,不是线程安全的
利用JDK9提供的容器静态工厂方法可以很轻易的创建出不可变的集合对象,如:
可以使用如下代码代替(不可变的):
最常见的Map的实现:HashMap,HashTable,Treemap,
HashMap和HashTable都是哈希表的实现,前者不是线程安全的,后者是同步容器
HashMap进行get和put操作,可以达到常数时间的性能,因此它是绝大部分利用键值对存储场景的首选
TreeMap则是基于红黑树访问的一种Map,他的操作的时间复杂度为O(log(n)),具体顺序可以指定Comparator来决定,或者根据键的自然顺序来判断
着重了解HashMap
HashMap的性能表现非常依赖于哈希码的有效性,以下是一些hashCode和equals的基本约定:
主要就是:hashCode相等不一定equals,equals了的hashCode一定相等
有序Map的分析:LinkedHashMap和TreeMap
LinkedHashMap提供的是一种遍历顺序符合插入顺序的一个Map,顺序主要是插入顺序,一些特定场景例如空间占用敏感的资源池就可以使用LinkedHashMap来实现
TreeMap是由键的顺序来实现的,因此键不能为null。通过Comparator或者Comparable来决定
HashMap的源码分析:
内部实现
容量和负载稀疏
树化
内部主要是有数组和链表来实现,数组被分为一个个桶,通过键的哈希值来决定这个键值对在数组的位置,对哈希值相同的键,则以链表方式存储。如果链表大小超过阈值(TREEIFY_THRESHOLD 8),图中的链表会被改造为树状结构便于访问时候的高效性
HashMap具有懒加载的特性,数组并不会在你new一个HashMap的时候创建,而是在putVal中时候发生对size的判断再进行HashMap的初始化(其中后续的扩容,树化操作都与这个方法有关)
HashMap的key的哈希值并不是key的hashCode方法算出来的,而是通过以下方法处理:
这里为什么要将高位数据移到低位进行异或运算呢?主要就是为了防止某些计算出来的hashcode只有高位数据有效,这样可以有效地避免哈希冲突
resize方法也很重要,分配到扩容,很容易被面试官问道
树化改造的逻辑主要在treeifyBin中
Collections的synchronized方法,粗粒度的加锁,在高并发条件下性能比较地下
使用JUC包来代替
主要的面试问题:
基本的线程安全工具
传统的同步容器Map存在的问题
梳理JUC包,尤其是ConcurrentHashMap采取了哪些方式来提高并发表现
ConcurrentHashMap的历史演进,很多都是停留在早期的版本
ConcurrentHashMap的设计实现:在Java8发生了较大的变化
早期ConcurrentHashMap,其实现是基于分段锁来实现的,在进行并发操作时,只需要锁定相应的段(HashEntity,结构直接类似于HashMap,也直接以链表的形式存放数据),这样就可以有效避免了类似HashTable整体同步的问题,大大提高了性能
HashEntity的大小为concurrencyLevel,默认是16,可以通过构造方法来实现
ConcurrentHashMap的扩容不是对整体进行扩容,而是对每一个HashEntity进行扩容
当需要获取全部锁,例如计算所有HashEntity的总和的时候,或许会因为并发put导致获取结果后续被修改了,又或者是获取全部的锁,但代价果高
ConcurrentHashMap采取类似CAS的重试机制(RETRIES_BEFORE_LOCK)尝试获取可靠值,如果检测到了修改就需要重新获取了。
Java8和之后的版本中ConcurrentHashMap发生的变化
同步与HashMap的数据结构,当链表过长时候转换成红黑树
初始化操作改成懒加载模式,应该和HashMap一样是在putVal中分配内存
内部数据使用volatile来保证可见性
使用CAS操作,实现特定场景无锁并发
包括NIO
传统的java.io,也是我们说的BIO,会一直阻塞在数据流的读取或写入阶段里,是同步阻塞的方式,他们之间提供了可靠的线性顺序
优点:代码简单直接
缺点:效率和扩展性存在局限性,容易成为应用的瓶颈
有时也将net包下的基于网络的操作也归类到了IO中
1.4提出了NIO框架,Channel、Selector、Buffer等新的抽象,是多路复用的,具有同步非阻塞的特点
同步非阻塞的由来(网上看到的,不一定准确):对IO流,非阻塞:程序执行不会一直阻塞,而是去干别的了(具体是啥也没讲清楚,可能是向下执行代码),同步:该线程需要定时读取Stream,判断数据是否准备好。提供了更接近操作系统底层的高性能数据操作方式。
1.7的NIO2,异步非阻塞方案,也叫AIO(Asynchronous IO),直接将读取工作交给另外一个线程去执行
专栏里给的概念解释:
使用IO库中的Stream或者是NIO库中FileChannel方法实现
Java在语言层面已经提供了Files.copy方法。
区别:
使用输入 流来进行文件操作时候,会进行多次用户态与内核态的状态切换
频繁的状态切换会带来一定的开销,降低IO效率
使用NIO方式则会使用到零拷贝技术,不需要用户态参与,提高性能
Java标准库(如果这样回答就要小心了,因为很少回答仅仅只是调用到某个方法就完事儿了的)
没有使用NIO机制
主要是JUC及其子包的类,如:
CountDownLatch
主要的API为:await、countDown、getCount。当CountDownLatch减到零的时候await的线程就会继续运行,且CountDownLatch不可重置
CyclicBarrier
主要的API为:await,当await的线程达到指定数量后,这一组线程同时开始运行,且CyclicBarrier会自动发生重置
Semaphore
主要的API为:acquire、release两个方法。就对应着操作系统中的信号量
具体使用可见Java并发编程实战的笔记,里面进行了详细的说明
JUC提供的支持并发操作的集合:
CopyOnWriteArraySet的底层就是使用到了CopyOnWriteArrayList,因此只需要学习一种即可
看下CopyOnWrite的具体表现:
应该使用到读多写少的场景里面。
至于Map,如果不追求有序推荐使用ConcurrentHashMap,如果需要对大量数据进行非常频繁的修改,建议使用ConcurrentSkipListMap
其实在并发编程实战笔记中有,这里在敲一遍增加印象
CachedThreadPool
构造方法:
特点:缓存是对于线程来说的,会试图缓存线程并重用,当线程闲置60s之后就会被回收,内部使用SynchronousQueue作为工作队列。适用于处理大量短时间工作任务的线程池,由于没有指定初始化大小,线程个数无上限
FixedThreadPool
构造函数:
特点:构造函数都需要指定线程个数,因此线程个数是固定的,其中的Fixed指的是工作队列,即无界的性质,如果工作任务积压过多或许会造成OutOfMemoryException异常
SingleThreadExecutor
构造函数:
特点:工作线程数目被限制为1,依旧是操作一个无界的工作对列
SingleThreadScheduledExecutor和ScheduledThreadPool
构造函数:
特点:都含有Scheduled(周期性调度)这个特性,区别是单一工作线程还是可以指定多个工作线程
WorkStealingPool
构造函数:
大部分时候使用Executors这几个静态方法即可,但是有时候仍然需要我们指定其中的细枝末节,这就需要我们对ThreadPoolExecutor构造方法足够了解
Executor框架的结构组成:
线程池的足证部分都体现在线程池的构造函数中
corePoolSize:核心线程数,也可以理解成常驻线程数,如newFixedThreadPool这个就为设置的nThread,对newCachedThreadPool则是0(因为会在无工作的时候缩容)
maximumPoolSize:最大线程数,对newFixedThreadPool就是指定的nThread(固定大小了),对于newCachedThreadPool则是Integer.MAX_VALUE
keepAliveTime和unit:共同决定额外的线程能够闲置多久,超过了就会回收
workQueue:阻塞任务队列
线程池的问题:
避免任务堆积,如newFixedThreadPool就会造成OOM错误
避免过度扩展线程:有时候面对大量短期任务直接使用CachedThreadPool,因为很难明确一个线程数目
可能发生线程泄露(线程数不断增长):可能是因为任务逻辑有问题,很多线程卡在了同一个位置迟迟不得释放
尽量避免在使用线程池时候使用ThreadLocal(因为工作的生命周期往往会超过线程的生命周期)
线程池大小的确定:
CPU密集型任务:推荐设置线程数为N+1
等待较多的任务:线程数 = CPU 核数 × 目标 CPU 利用率 ×(1 + 平均等待时间 / 平均工作时间)
原子类如AtomicInteger支持对其封装的数据的原子性的访问和更新操作,底层是基于CAS操作
例如AtomicInteger的getAndIncrease方法的底层实现可以导到Unsafe类的这段代码中来
可以明显的看到do while这个CAS重试机制的具体使用
CAS的具体底层实现是基于CPU提供的特定指令的,在大多数情况下CAS是个非常轻量级的操作,这也是他的优势所在
问题:如何在实际场景中使用到CAS操作,毕竟直接调用Unsafe往往不是一个好的选择
如果想要使用,可以使用AtomicLongFieldUpdater,核心APi为:
但是有个局限性,只能支持Long数据类型的操作,至于其他的数据操作就需要换了
在Java9以后可以使用VarHandle类中的方法:
传参顺序依旧是obj,expect,update
不能过度依赖CAS,CAS的问题:
通常大部分问题只要重试一次即可成功,但是如果压力过大,重试次数过多,往往会造饥饿等问题
ABA问题,加版本号即可解决,Java的实现类为:AtomicStampedReference
往往不会直接与CAS打交道,而是通过与Doug Lea的JUC包打交道间接使用到了CAS
JUC包的基础:AbstractQueuedSynchronizer(AQS)
Doug Lea选择将基础的同步操作抽象在了AbstractQueuedSynchronizer当中去
对AQS内部结构和方法的简单拆分:
一个volatile的整型成员表征状态,提供get/set方法
一个FIFO队列,实现线程之间的等待和竞争
基础基于CAS方法,如acquire/release方法,实现对资源的获取与释放
ReentrantLock的内部实现的简单抽象:
CountDownLatch中对AQS的利用页大差不差
大概可以分为三个步骤:加载、链接、初始化
加载过程(Loading):JVM将字节码数据从不同的数据源(jar文件,class文件,甚至是直接来源于网络的数据源等等)读入的过程,最终都会在JVM内部构建出Class对象(并没有直接读入到内存当中去,仅仅只是存储了Class的结构而已),如果不满足构成Class对象的规范,那么会抛出ClassFormatError异常。我们可以定义自己的类加载器来完成对加载过程的参与。
链接过程(Linking):核心步骤,将原始的类定义信息平滑的转入JVM中,可以进一步分为三步:
验证(Verification):虚拟机安全的保证,检测输入的类定义信息是否有害于JVM(主要还是因为class文件的结构开源导致的,为了让JVM支持不同的语言),如果有害就会抛出VerifyError异常
准备(Preparation):创建类或接口中的静态变量并赋初始值
解析(Resolution):将常量池中的符号引用替换为直接引用(因为内存地址已经定了)
初始化阶段(Initialization):执行静态变量的赋值,类中静态代码块的运行
双亲委派模型:当前类加载器去试图加载某个类型的时候,除非父加载器找不到相应的类型,否则尽量将这个任务代理给父加载器去做,这样做的主要目的是为了避免重复加载Java类型
直接使用javap仅仅只会出现反编译之后的Java文件,不会出现字节码信息,如果要查看详细信息可以加上-c参数,查看全部信息使用-v参数
实际上就是动态代理的底层分析
我们使用到的动态代理(框架底层频繁使用,Java语言层面提供支持,我们只是简单使用)本质上就是等待特定的时机,去修改已有类型实现,或者创建新的类型。
那么,如果生成一个java文件了,如何将其编译成一个class文件以供JVM读取呢?
可以使用java.compiler
或者如果我们可以直接书写一个Class文件吗,难度太大
Proxy内部实现逻辑——内部类ProxyBuilder中的代码段:
这里就是ProxyBuilder内部读取Java文件并且通过ProxyGenerator编译成class字节流并交给Unsafe类进行读取成Class的过程
Java提供的动态代理,实现过程可以简化为:
提供一个普通接口作为共同接口,作为被调用类型和代理类之间的统一接口
实现InvocationHandler接口,实现其中的invoke方法,该方法为我们使用代理对象真正调用的方法
通过Proxy.newProxyInstance静态方法生成的代理对象,我们即可直接操作代理对象了
字节码操作技术除了动态代理还用在什么地方:
Mock框架(测试框架)
ORM框架
IOC容器
部分Profiler工具
生成形式化代码的工具
可以大致划分为几个方面:
程序计数器,每个线程都有且唯一,其中存储当前正在执行的Java方法的JVM指令地址
Java虚拟机栈,简称栈,每个线程都有且唯一,内部保存栈帧,对应着一次次的方法调用。JVM对栈帧的操作只有入栈和出栈操作
栈帧中存储有:局部变量表、操作数栈、动态链接、方法正常退出和异常退出的定义等
堆:放置Java对象实例,被所有线程共享,可以通过Xmx等参数指定堆指标
堆内的空间会被不同的垃圾收集器细分
方法区:所有线程共享,存储元数据(类结构信息、运行时常量池、字段、方法代码等)
运行时常量池可以存放版本号、字段、方法、超类、接口等信息,还有就是存储常量池(存放各种常量信息,无论是编码的字面量还是运行时候决定的符号引用等)
本地方法栈:每个线程都会创建一个,类似于Java虚拟机栈。HotSpot虚拟机直接将本地方法栈与虚拟机栈使用同一块区域,JVM标准并未强制限定
我这里简要介绍两点区别:
直接内存(Direct Memory)区域,它就是我在专栏第 12 讲中谈到的 Direct Buffer 所直接分配的内存,也是个容易出现问题的地方。尽管,在 JVM 工程师的眼中,并不认为它是 JVM 内部内存的一部分,也并未体现 JVM 内存模型中。
JVM 本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT Compiler 在运行时对热点方法进行编译,就会将编译后的方法储存在 Code Cache 里面;GC 等功能需要运行在本地线程之中,类似部分都需要占用内存空间。这些是实现 JVM JIT 等功能的需要,但规范中并不涉及。
Serial GC:最古老的单线程收集器,会出现Stop The World的状态
对老年代采用了标记-整理算法,新生代使用复制算法
开启参数为:-XX:+UseSerialGC
ParNew GC:新生代的GC实现,实际是Serial GC的多线程版本,一般配合老年代的CMS GC使用
开启参数为:-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
CMS(Concurrent Mark Sweep) GC:老年代GC,基于标记-清除算法,目的是尽量减少停顿时间
Parallel GC:JDK8早期版本中的默认使用GC,特点是新生代和老年代GC都是并行的
开启参数为:-XX:+UseParallelGC
G1 GC:兼容吞吐量和停顿时间的GC实现,JDK9之后默认的GC选项
G1 GC仍然存在着年代的划分,但是将内存直接划分成一个个Region,推荐使用G1 GC
可以使用参数:java -XX:+PrintCommandLineFlags -version
来查看Java使用的GC
其中GC使用的算法在《深入理解Java虚拟机》中有
Docker看起来类似于虚拟机,拥有自己的shell、能够独立安装软件包、运行时候与其他容器互不干扰,但是后来你会发现Docker不是一种虚拟化技术而是一种隔离技术
Docker仅仅在类Linux内核上实现了有限的隔离和虚拟化,而不是像传统虚拟软件那样,独立运行出一个新的操作系统。运行在Docker之上的多个程序只需要像调用操作系统API那样来操作Docker就可以获取资源,基本不存在兼容性改变问题
Java程序运行在Docker环境的问题:
JVM会在启动时候检测内存大小,设置堆的内存起始大小为1/64,最大堆内存大小为1/4。
JVM会检测CPU核心数目,会直接影响到Parallel GC,JIT Compiler甚至是ForkJoinPool的执行
如何解决这些问题呢?升级到最新的JDK即可解决问题。
从JDK10开始就开始完善了,完全可以自动化实现内存和CPU盒数的检测了
如果无法更新JDK版本,可以限制堆,元数据区的大小,明确制定可用CPU核数
注入类攻击是源于程序允许攻击者将不可信的动态内容注入到程序中并将其执行,这就可能改变最初预计的执行过程并产生恶意效果
场景:
SQL注入攻击
如果只是简单的生成SQL语句通过JDBC让MySQL去执行,如:
Select * from use_info where username = “input_usr_name” and password = “input_pwd”
传入参数:“ or “”=”
于是拼接出来的SQL为:Select * from use_info where username = “input_usr_name” and password = “” or “” = “”
这种情况下MySQL一定会返回True
期望输入数值,但是用户实际输入了SQL语句片段,这就导致了问题的产生,甚至可能加上Delete语句
XML注入攻击
Java提供了工具操作XML文件,如果使用不当可能导致恶意访问
Java自身提供的安全检查:
运行时安全机制
主要是类加载过程中的验证,利用SecurityManager机制,限制代码的运行时行为能力
JDK提供的安全工具
keytool:管理密钥、证书等
jarsigner:对jar包的签名和认证等
达到攻击需求也未必需要绕过各种权限设置,直接使用哈希碰撞也可以达到攻击的目的,模拟大量相同HASH值的数据,通过JSON方式发送到服务器当中去,会使得算法的复杂度上升一个数量级,从而导致严重的性能退化
隔离级别:在数据库事务中,为保证并发数据读写的正确性而提出的定义
各家的关系型数据库都提供了自己的事务隔离级别,按照隔离级别由低到高,MySQL事务隔离级别可以分为:
读未提交:一个事务可以看到其他事务未提交的修改,非常低的隔离级别,可能出现脏读。
读已提交:事务能够看到的数据都是其他事务以及提交的修改,虽然不会出现脏读,但是这仍然是比较低的隔离级别,并不保证在读取两次可以获取到相同的数据,也就是允许其他事务并发修改数据,可能有不可重复读和幻读的出现。
可重复读:保证同一个事务中多次读取到的数据是一致的,也就是保证在事务执行过程中其他事务无法进行数据修改,也是InnoDB的默认隔离级别,可以简单认为不会出现幻读。
串行化:最高的隔离级别,在读取过程中需要获取共享读锁,在更新数据的时候需要获取排他写锁,如果存在WHERE字句,还会获取区间锁如:行锁等。
至于乐观锁和悲观锁,不是MySQL或者是数据库独有的概念,而是并发编程的基本概念。区别在于:
悲观锁认为数据出现冲突的可能性较大
乐观锁认为数据出现冲突的可能性较小
悲观锁一般是由SELECT......FOR UPDATE 语句导致出现的锁,防止数据被意外修改。乐观锁则是使用的CAS机制,不会直接对数据进行加锁,而是对比数据的时间戳或者是版本号来保证并发程序的正确执行。
MVCC本质可以看做乐观所的一种实现,而读写锁,双阶段锁则可以认为是悲观锁的实现
主流ORM框架的对比:
Hibernate
一个JPA Provider,以对象为中心,屏蔽底层SQL语句,使用HQL语言来进一步屏蔽对底层数据库之间的差异,降低维护数据库的成本,内部大量使用Lazy-Load来提升性能,同时提供了强大的持久化功能。但是缺点也相当明显,HQL需要额外的学习成本,数据库管理人员无法方便的对SQL进行优化,隔绝了与数据库层的交互。
MyBatis
以SQL为中心,开发者更加侧重于SQL和底层数据库,仅仅提供了半自动如数据封装等功能,支持较高的自定义化。
Spring JDBC Template
更加倾向于SQL层面,是对原生JDBC的简单封装
参数
意义
-Xint
告诉JVM只进行解释执行
-Xcomp
告诉JVM关闭解释器,启动会非常慢
-XX:SoftRefLRUPolicyMSPerMB=3000
设置软引用在最后一次引用过后的3000ms后被回收