java知识点总结
本文列举一下java的重要知识点,做一下知识总结和沉淀。
1. JRE、JDK、J2SE、Java SE
JRE仅包含运行java程序的必须组件,包括java虚拟机以及java核心类库。
JDK同样包含JRE,并且还附带一系列开发、诊断工具,例如jstat、jinfo、jmap、jstack等。
J2SE和Java SE,他们指同一个东西,J2SE,J2EE, J2ME更名为Java SE, Java EE, Java ME。J2SE 是java三大技术体系的一个,其他两个是J2EE,J2ME:1)J2SE(Java 2 Standard Edition),标准版技术体系,包含构成了java语言核心的类库,例如,数据库连接,网络编程,接口定义等,主要用于桌面应用软件的开发,包含了JDK的核心类。2)J2EE(Java 2 Enterprise Edition),企业版技术体系,除了包含J2SE中的类,还包含用于开发企业级应用的类,比如,ServerLet,JSP, EJB,主要用于分布式的网络程序开发。3) J2ME(Java 2 Micro Edition),包含J2SE的一部分类,主要用于消费类电子产品的软件开发,比如手机,pad等。
2. 一个java源文件只能有一个public类,源文件的名字必须和public类的名字相同,可以有多个非public类。
3. java的内置类型和其对应的包装类
byte:8位,有符号、以二进制补码表示的整数,[-128, 127], 默认值0,对应包装类java.lang.Byte。
short:16位,有符号、以二进制补码表示的整数,[-32768, 32767], 默认值0,对应包装类java.lang.Short。
int:32位,有符号、以二进制补码标识的整数,[-2147483648, 2147483647], 默认值0,对应包装类java.lang.Integer。
long:64位,有符号、以二进制补码表示的整数,[-9223372036854775808,9223372036854775807],默认值0L,L不分大小写,小写容易混淆,对应包装类java.lang.Long。
float:单精度,32位浮点数,默认值0.0f,对应包装类java.lang.Float。
double:双精度,64位浮点数,浮点数默认类型是double,默认值0.0d,对应包装类java.lang.Double。
boolean:表示一位的信息,true/flase, 默认值是false。
char:单一的16位的Unicode字符,这点和C++不一样,最小值是\u0000(即为0), 最大值\uffff(即为65536),可以存储任何字符,默认值是'u0000',对应包装类是java.lang.Character。
数据类型从低到高的顺序是byte,short,char—> int —> long—> float —> double,在把大容量的类型转换为小容量类型的时候,必须使用强制类型转换。
4. volatile
volatile修饰的成员变量,在每次被线程访问的时候,都强制从主内存重新读取该成员变量的值,而且当成员变量发生变化的时候,会强制线程将变化值写到主内存,这样任何时刻两个不同的线程总是看到某个成员变量的同一个值。
5. String,StringBuffer,StringBuilder
String类的对象是不可改变,若需要对字符串做多次修改,应该用StringBuffer(线程安全),StringBuilder(线程不安全,从而更快)。
6. String的getBytes方法
byte[]getBytes(),使用平台默认字符集将此String编码为byte序列,
byte[] getBytes(String charsetName),使用指定字符集将此String编码为byte序列。
7. 可变参数
typeName...parameterName ,一个方法中只能指定一个可变参数,且必须是方法的最后一个参数,可以传多个typeName的参数,或者传typeName的数组过来,函数内部都是当作数组使用。
public class VarargsDemo{
public static void main(String[]args){
printMax(34, 3, 3,2, 56.5);
printMax(new double[]{1,2,3});
}
public static void printMax(double...numbers){
if(numbers.length == 0)
{
System.out.println("no argument passed");
return;
}
double result = numbers[0];
for (int i = 1; i < numbers.length; ++i)
{
if(numbers[i] > result){
result = numbers[i];
}
}
System.out.println("The max value is " + result);
}
}
8. java的==和equals的用法区别
== 用于检测他们是否是同一个对象,即reference equality;equals 用于检测他们的value是否相等,即逻辑上是否相等:
new String("test").equals("test") // --> true;
new String("test") == "test" // --> false
"test" == "test" // --> true ,这里,常量字符串已经被编译器设置,所以指向同一个对象;
"test" == "te" + "st" // --> true,string 常量被编译器设置,所以指向同一个对象
可以用java.util中的Objects程序类,他会检测null:
Objects.equals("test", new String("test")) // --> true;
Objects.equals(null, "test") // --> false;
Objects.equals(null, null) // --> true
绝大多数场景应该用equals, 极少数你认为不会发错的情况下用==(比如就是比较是否是同一个对象,或者被编译器设置的常量)。
9. java内存模型
线程对变量的所有操作(读取、复制)都必须在工作内存中进行,不能直接读写主内存中的变量。不同线程之间不能直接访问对方工作内存中的变量,线程之间变量值的传递均需要在主内存来完成。
10. java线程池
newSingleThreadExecutor,如果这个唯一的线程因为异常结束,那么会有一个新的线程代替他,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newCachedThreadPool,工作线程的创建数量几乎没有限制(integer.MAX_VALUE),这样可灵活的往线程池中添加线程,如果长时间没有往线程池中提交任务,工作线程空闲了1分钟(默认值),该线程将自动终止,终止后若有提交任务,则重新创建工作线程。采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
newFixedThreadPool,创建一个定长线程池,可控制线程最大并发数,超出的任务(Thread对象)会在队列中等待,阻塞队列采用了LinkedBlockingQueue,它是一个*队列,不会拒绝任务。
newScheduledThreadPool,创建一个定长线程池,支持定时及周期任务执行。
11. 字节码翻译成机器码的三种方式:
解释执行,逐条将字节码翻译成机器码并执行,优点:无需等待编译。
即时编译,将一个方法中包含的所有字节码编译成机器码再执行。优点:实际运行速度更快。
混合模式,综合了解释执行和即时编译,会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。
即时编译后的java程序执行效率可能是会超过C++的,这是因为与静态编译相比,即时编译拥有程序的运行时信息,并且能根据这个信息做相应优化。
12. C1、C2编译器
C1:client编译器,面向的是对启动性能有要求的客户端GUI程序,优化手段相对简单,编译时间短。
C2:server编译器,面向的是对峰值性能有要求的服务端程序,优化手段复杂,编译时间长,生成的代码执行效率高。
热点方法首先会被C1编译,而后热点方法中的热点会进一步被C2编译,为了不干扰应用的正常运行,即使编译是放在额外的编译线程进行的。在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行,编译完成的机器码会在下次调用该方法时启用,以替换原本的解释执行。
13. 几个有名的VM
Sun HotSpot VM, 默认的java虚拟机,很强大,武林盟主。
BEA JRockit, 专注于硬件服务器和服务端应用场景的虚拟机,针对服务端场景做了很多优化,因此其不太关注程序启动速度。JRockit虚拟机内部不包含解释器实现,全部代码都靠即使编译器编译后执行。
IBM J9 VM:通用的虚拟机,从服务端到桌面应用,再到嵌入式都可以。
Azul VM 和 BEA Liquid VM 的专用商业及虚拟机:只运行在特定硬件平台,要求比较高,性能也很强悍,可以管理至少数十个CPU和数百GB的内存,还提供巨大内存范围内实现可控GC时间的垃圾收集器。
14. java虚拟机执行class字节码的过程
加载:把代码数据加载到内存中,接着为这个类在JVM的方法区创建一个对应的class对象,这个class对象就是这个类各种数据的访问接口。
验证:JVM规范校验,代码逻辑校验等。
准备:Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
解析:JVM针对类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类引用进行解析,将常量池中的符号引用替换成直接其在内存中的直接引用。
初始化:JVM 会根据语句执行顺序对类对象进行初始化。
使用:JVM完成初始化后,从入口方法开始执行用户的程序代码。
卸载:当用户代码执行完毕后,JVM便开始销毁创建的class对象,最后负责运行的JVM也退出内存。
15. 直接内存
NIO(New Input/Output)类,引入基于通道(Channel)和缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,能显著提升性能,避免了在java堆和native堆中来回复制数据。本机直接内存的分配不会受到java堆大小的限制,但是会受到本机总内存的大小以及处理器寻址空间的限制。在-Xmx等参数的设置,需要考虑到直接内存,避免两者总量大于本机内存,从而导致动态扩展的时候出现OutOfMemoryError异常。
16. 如何判断对象是垃圾
引用计数法,对象被引用时加1,被去引用时减1,通过判定引用计数是否为0来判断对象是否是垃圾,这有个缺陷,对循环引用没有办法。
GC Root Tracing,从GC Root 出发,所有可达的对象都是存活的,所有不可达的对象都是垃圾。Golang采用了三色标记法,思路差不多:1)起初所有对象都是白色的,2)从根出发扫描所有可达对象,标记为灰色,放入待处理队列。3)从队列取出灰色对象,将其引用对象(从白色里面挑选)标记为灰色放入队列,自身标记为黑色。4)重复3,直到灰色对象队列为空,此时白色对象即为垃圾,进行回收。
17. 垃圾回收算法
标记清除算法:分标记、清除两个阶段,标记阶段标记所有GC Root可达的对象,清除阶段清理掉所有未标记的对象,存在空间碎片问题,影响后续对象的分配效率。
复制算法:将原有空间分成2块,每次只使用一块,每次垃圾回收的时候将存活的对象拷贝到另外一块,然后将原先的一块清理掉,然后交换2块的角色,空间只能使用1块,折损太大。
标记压缩算法:分标记、压缩两个阶段,标记阶段标记所有存活的对象,压缩阶段,将所有存活的对象压缩到内存的一边,然后清理边界外的所有空间,移动和压缩会影响应用程序。
18. 新生代、老年代各自采用的回收算法
新生代,大部分是存活很短的,若用标记清除算法则会有大量碎片,采用复制算法较好,但是也要注意,并不是对半分的,采用Eden:From Suvior0:To Suvior1=8:1:1分发,对象优先分配在Eden、From Survior0中,少量存活的复制到To Suvior1 中, 这种情况,只浪费了10%的空间,若ToSuvior1放不下Eden、From Survior0的存活对象,则会借用老年代的内存(分配担保)。
老年代,标记清除算法、标记压缩,因为对象存活时间长,只有少量的对象需要清除或者标记,不会有大量的内存碎片;反过来,若用复制算法,则需要移动大量的对象,会严重影响应用程序。
19. 垃圾回收器
串行回收器,用单线程进行垃圾回收的回收器,每次只有一个线程,在并发能力较弱的计算机上,其专注性和独占性的特点可以让其有较好的性能表现,可以在新生代和老年代使用,分为新生代串行回收器和老年代串行回收器。
并行回收器,用多线程处理,对于并行能力强的机器,可以有效缩短垃圾回收所用时间:1)新生代ParNew回收器,将串行回收器多线程化,其回收策略、算法、参数和新生代串行回收器一样,在并行能力强的机器可以缩短垃圾回收时间,在并行能力弱的机器,由于存在线程切换,性能不一定比串行好。2)新生代ParallelGC回收器,与新生代ParNew回收器类似,也采用复制算法,多线程、独占式的,会导致stop-the-world,但是不同点是:注重系统的吞吐量,自适应调节策略,-XX:UseAdaptiveSizePolicy, 打开此策略,新生代的大小、Eden和Survior的比例、晋升老年代的对象的年龄等参数会自动调节,达到堆大小、吞吐量、停顿时间的平衡。-XX:MaxGCPauseMillis:设置最大垃圾收集器停顿时间,在ParallelGC工作期间,自动调整相应参数,将停顿时间控制在设置范围内,为完成此目标,会用较小的堆,从而也会导致频繁GC,-XX:GCTimeRatio:设置吞吐量大小,0~100的整数,假设为n,则系统将不花费超过1/(1+n)的时间用于垃圾收集,默认值是99,即不超过1%的时间用于垃圾收集。3)老年代ParallelOldGC回收器,注重吞吐量的回收器,多线程并发,因为是作用在老年代,算法用的标记压缩算法。
CMS回收器,主要关注系统停顿时间,Concurrent Mark Sweep, 标记清除法(会造成内存碎片),多线程并行回收的垃圾回收器。
G1 回收器,使用了分区算法,从而使得Eden区、From区、Survior区、老年代等各内存可以不用连续。分为初始标记,根区域扫描,并发标记,重新标记,独占清理,并发清理6个阶段,其中初始标记、重新标记、独占清理是独占式的,会stop-the-world,所有待回收的内存放到Collection Set中,首先针对Collection Set中的内存进行回收,当内存不足的时候,可能会触发FullGC。
20. HashMap和ConcurrentHashMap
HashMap,非线程安全,不能用于并发,在JDK1.7,采用数组+链表的方式,根据Key,Hash之后得到应该存放的桶的位置,若桶是空,则直接放进去,若桶是链表,则顺着链表进行查找,若没有则在最后添加,若有,则直接覆盖。当Key冲突较大的情况下,链表的长度会很长,降低了读写的效率。JDK1.8版本,有一个阈值,默认值是8,用于判定是否需要将链表转换成红黑树。
ConcurrentHashMap,JDK1.7版本采用分段锁技术,比Hashtable锁的粒度要细,支持Segment数量的线程并发。Hashtable不支持多线程间增加、迭代访问,迭代器会失效。ConcurrentHashMap支持此种场景,迭代器不会失效。用自旋锁+阻塞锁机制,先自旋锁,超过一定的重试次数后,改为阻塞锁。自旋锁一般用于竞争不频繁的场景,使用不当会造成CPU的浪费,优势是没有发生用户态、内核态的切换,效率高。与HashMap不同的是,ConcurrentHashMap最外层不是一个大的数组,而是一个Segment的数组。每个Segment包含一个与HashMap数据结构差不多的链表数组。JDK1.8版本摒弃了分段锁的方案,而是直接使用一个大的数组,对于PUT操作,如果Key对应的数组元素为NULL,则通过CAS操作将其设置为当前值。如果Key对应的数组元素(也即链表表头或者树的根元素)不为NULL,则对该元素使用synchronized关键字申请锁,然后进行操作。和HashMap一样,如果链表长度超过一定阈值,则会将其转成红黑树。
// JDK1.7版本结构
static final class HashEntry<K,V>{
final int hash;
final K key;
volatile V value;
volatile HashEntry<K, V> next;
}
static final class Segment<K, V> extends ReentrantLock implements Serializable{
private static final long serialVersionUID = 2249069246763182397L;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
transient int loadFactor;
}
final Segment<K, V> [] segments
21. synchronized的用法以及底层实现
可以锁静态方法(含static),其锁的是类的class对象(不同于类的实例对象),可以控制这个类的多个静态方法的互斥访问。这时若有一个synchronized 锁的是非static方法(锁的是类的实例对象),两者是不互斥的,多线程可以同时访问,因为锁的对象是不一样的。
可以锁代码块,锁的对象可以指定,比如某个实例对象,或者类的class对象。
java虚拟机中的同步(synchronized)基于进入和退出管程(monitor)对象实现。包括显示同步(同步代码块,有明确monitorenter和monitorexit指令)和隐示同步(synchronized 修饰的同步方法,无monitorenter和monitorexit,由方法调用指令读取运行时常量池中的方法的ACC_SYNCHRONIZED标志来隐示实现)。
在JVM中,对象在内存中的布局分为三块区域:实例数据,对齐填充,对象头。
实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分,还包括数组的长度,这部分内存按4字节对齐。
填充数据:虚拟机要求对象的起始地址是8字节的整数倍,故有时候需要对齐字节。
对象头:这是实现synchronized的锁对象的基础,主要由Mark Word和Class Metadata Address组成。
synchronized 是可重入的,比如,一个synchronized(锁的是实例的对象)方法可以调用另外一个synchronized方法。
synchronized与等待唤醒:
对象的wait/notify/notifyall 必须在synchronized 方法(或者代码块中)执行。
和sleep方法不同,调用sleep方法,只是让对应线程休眠,并未释放对应的锁。
调用wait方法后,会释放对应的锁,直到有人调用notify/notifyall方法才会继续。
调用notify/notifyall方法后不会立即释放锁,等相应的方法和代码块执行完毕才会释放。
java对synchronized的优化:
锁的状态:无锁状态,偏向锁,轻量级锁,自旋锁,重量级锁,随着锁的竞争,锁可以从低到高单向升级。
偏向锁:如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需做任何同步操作,即省去了大量有关锁的申请操作,提高了程序的性能。若失败则升级为轻量级锁。
轻量级锁:适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为自旋锁。
自旋锁:先尝试自旋几次,若得到锁,则进入临界区,若不能得到锁,则升级为为重量级锁。
重量级锁:即真正意义上的锁,会将线程在操作系统层面挂起(发生用户态到内核态的切换)。
锁消除:由JIT编译器识别,部分逻辑代码完全不会存在线程共享访问,可以将对应的锁逻辑去掉。比如下面的StringBuffer类,append是同步方法(含synchronized),但是sb变量是局部变量,完全不会有数据共享问题,所以会将对应的锁消除掉。
public void add (String str1, String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
22. lombook
lombook是一个开源项目,能够通过添加注解自动生成一些方法,这个包是在编译阶段起作用,免去一些常规小函数的编写,例如,@Getter/@Setter注解可以针对类的属性字段自动生成Get/Set方法。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
</dependency>
23. JDK性能监控工具
jstat -<option> [-t] [-h<lines> <vmid> [<interval> [<count>]]
option 可以由以下值构成:
-class, 监视类装载、卸载数量、总空间、以及类装载所耗费的空间
-gc,监视java堆的情况,包括Eden区、两个Survior区、老年代、永久代的容量、已使用空间、GC时间合计等信息
-gccapacity,监视内容基本和-gc相同,增加输出java堆各个区的最大、最小空间
-gcutil,基本和-gc相同,主要关注已使用空间占总空间的百分比
-gccause, 与-gcutil相同,额外输出导致上一次GC的原因
-gcnew, 监视新生代的GC状况
-gcnewcapacity,基本与-gcnew一样,主要关注使用到的最大、最小空间
-gcold, 监视老年代GC状况
-gcoldcapacity, 与-gcold基本一样,关注使用到的最大、最小空间
-gcpermcapacity,输出永久代的最大、最小空间
-compiler, 输出JIT编译器编译过的方法、耗时信息
-printcompilation 输出JIT编译过的方法
-t ,表示输出时间戳
-h,表示在多少行后输出一个表头
vmid, 虚拟机的进程ID
interval,输出间隔, 单位是毫秒
count,输出次数
举例,
[aaa@qq.com /usr/local/services/jdk1_8_0_111-1.0/jdk1.8.0_111/bin]# ./jstat -gcutil -h1000 -t 32160 10000 1
Timestamp S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
172641.0 0.00 31.79 50.93 13.92 97.22 95.73 1977 8.763 0 0.000 8.763
S0:Survior 0 ,还未使用
S1:Survior 1, 使用了31.79%,
E:Eden, 使用了50.93%
O:老年代使用了13.92%
M: 元空间使用了97.22%
YGC、YGCT:程序运行以来,共发生1977次Minor GC,总共耗时8.763秒
FGC/FGCT: 程序运行以来,共发生0次FullGC,耗时0秒
GCT:总的GC的耗时,这里是YGCT+FGCT=8.763秒
24. 堆栈空间的设置参数
-Xms:堆得初始空间大小。
-Xmx:堆的最大空间大小。
-Xmn:设置年轻代的内存大小, ms(mx) - mn的大小就是老年代的大小。
-XX:SurviorRatio= eden/from =eden/to,from和to, 同一时间只有一个可用,循环作为from或者to,即始终有一个是浪费的。
-Xss:栈空间大小。
-XX:PermSize:永久代初始大小,一般是用来存放方法的,1.8用元空间代替。
-XX:MaxPermSize:永久代的最大大小。
-XX:MetaSpaceSize:元空间发生发生GC的阈值。
-XX:MaxMetaSpaceSize:设置元空间的最大大小,默认基本是机器的物理内存大小。
-XX:MaxDirectMemorySize, 指定本机直接内存的容量,若不指定,则默认与java堆的最大值保持一致(-Xmx), NIO的方法可能会用到此块内存。