欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java知识点总结

程序员文章站 2022-05-28 13:26:34
...

本文列举一下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内存模型

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,用于判定是否需要将链表转换成红黑树。

java知识点总结

java知识点总结

  • 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组成。

java知识点总结

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的方法可能会用到此块内存。