Java常见语法机制总结

volatile机制

  1. CPU与三级缓存:为了解决CPU按照摩尔定律提升的计算能力和内存缓慢发展的不平衡,三级缓存以其比内存更加强悍的读写能力,在CPU和内存中间充当了一层缓存,缓解了这种发展不平衡带来的矛盾。
  2. 三级缓存与MESI协议:CPU内核(流行的架构例如X86)有各自的一级缓存或者二级缓存,而三级缓存和主内存是多核共享,内核之间的数据共享和数据独占将导致缓存一致性问题,从而有了MESI CPU缓存一致性协议来保证数据的一致性问题。
  3. MESI协议与强一致性:MESI协议本质上是多核操作共享数据的串行化强一致性保障,这种串行将导致CPU的资源浪费。
  4. 指令重排优化与最终一致性:在多核间Invalidate消息的同步响应浪费了CPU一定的计算资源,从而有了指令重排。在内核处理器中对数据的修改不直接写到缓存中,而是先放入store buffer,然后向其他核心发送Invalidate消息的期间,本内核可以继续执行其他指令。而收到Invalidate消息的内核将Invalidate消息放到Invalidate Queue中延时处理。当core1本内核收到其他内核core2关于Invalidate的invalidate acknowledge响应才会将store buffer中的对应指令刷新到缓存中。这样store buffer和Invalidate Queue实现的指令重排就通过异步确认和延时消费保证了数据的最终一致性。假设core1对数据A的修改通知没有被core2立即处理(因为在invalidte queue中),core2紧接着又修改了数据A,是不是就造成了数据的不一致。其它内核对数据的修改对本内核是不可见的。
  5. 内存屏障与强一致性:如上所说指令重排是采用的是异步确认和延时消费保障了数据的最终一致性,但是在有些场景下这种机制也将导致数据的不一致,所以在一些特殊场景下我们需要禁止指令重排从而保证数据的强一致性。禁止指令重排的机制就是对CPU加入内存屏障,从而禁止一些会引起错误的指令重排。
  6. 在CPU角度,他是无法判断什么时候需要加入内存屏障,什么时候不需要,所以这个加入内存屏障的活就抛给了上层应用也就是软件层。在Java语言中,这个加入内存屏障的命令就是volatile关键字。所以JMM的一些规定比如happens-before和一些可见性和有序性的规则,将会减少程序员的压力,因为在一些JVM可预测的场景中,JMM规范会自动实现这种内存屏障。而程序员需要关心的就是在一些高并发场景下我们可以使用Volatile关键字去告诉JVM和CPU我要加入内存屏障。当然这不是Volatile存在的所有意义。

垃圾回收yang gc和full gc区别
https://juejin.cn/post/7085174576950280200

反射与动态代理的联系
通过反射机制,可以在运行时访问 Java 对象的属性,方法,构造方法等。
反射的主要应用场景有:

  • 开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
  • 动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。
  • 注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。
  • 可扩展性功能 - 应用程序可以通过使用完全限定名称创建可扩展性对象实例来使用外部的用户定义类。

内存泄漏什么情况会发生?

  • 静态属性导致内存泄露,大量使用static静态变量(list.add方法)。
  • 当我们new一个java流对象之后,不仅在计算机内存中创建了一个相应类的实例对象。而且,还占用了相应的系统资源,比如:文件句柄、端口、数据库连接等。在内存中的实例对象,当没有引用指向的时候,java垃圾收集器会按照相应的策略自动回收,但是却无法对系统资源进行释放。所以,我们需要主动调用close()方法释放java流对象。需要自己close的东西,一般都是用了虚拟机之外的资源,例如端口,显存,文件等,虚拟机无法通过垃圾回收释放这些资源,只能你显式调用close方法来释放。)(JVM中只需要有一个指针,指向打开的文件表,JVM就能操作文件。这个指针也就是文件描述符。只能释放JVM内部文件描述符,而无法释放JVM之外打开的文件表,需要执行close
    许多情况下,如果在一些比较频繁的操作中,不对流进行关闭,很容易出现输入输出流经超越了JVM的边界,所以有时可能无法回收资源。
    这些资源的控制是由操作系统控制的,不是由Java虚拟机控制的。例如,对于数据库连接,它是通过网络连接到数据库服务器的,而网络连接是由操作系统控制的,而不是由Java虚拟机控制的。同样,对于IO流,它也会涉及到文件句柄的占用,而文件句柄也是由操作系统控制的。
    当我们在Java程序中使用数据库连接或IO流等资源时,Java虚拟机会向操作系统申请相应的资源,并将这些资源用于Java程序的执行。但是,在Java程序执行完毕后,Java虚拟机并不能保证这些资源会被自动释放,因为这些资源是由操作系统控制的,而不是由Java虚拟机控制的。因此,我们需要在程序中手动释放这些资源,以避免资源泄露和占用问题的发生。
    所以流操作的时候凡是跨出虚拟机边界的资源都要求程序员自己关闭,不要指望垃圾回收。
  • String的intern方法
  • 使用ThreadLocal
    凡是非JVM内存的释放,都需要调用native方法,执行底层操作系统命令(如:直接内存、IO流、数据库连接、socket等)

AQS 为什么需要双向链表?
双向链表有两个指针,一个指针指向前置节点,一个指针指向后继节点。所以,双向链表可以支持常量 O(1) 时间复杂度的情况下找到前驱节点。因此,双向链表在插入和删除操作的时候,要比单向链表简单、高效。
第1个原因,没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。所以,线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从 Head 节点开始遍历,性能非常低。
第2个原因,在 Lock 接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成 CANCELLED。而被标记为 CANCELLED 状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。
第3个原因,是为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。但是实际上按照公平锁的设计,只有头节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。所以,为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。所以这里会涉及到前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。

这就意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从 Head 节点开始往下逐个遍历,找到并移除异常状态的节点。同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争。

jstack、jmap、jstat主要用途
jstack用于JVM当前时刻的线程快照,又称threaddump文件,它是JVM当前每一条线程正在执行的堆栈信息的集合。生成线程快照的主要目的是为了定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部时长过长导致线程停顿的原因。通过jstack我们就可以知道哪些进程在后台做些什么。
jmap
提取进程内存信息,用于分析OOM
jstat
JDK提供的一个可以监控Java虚拟机各种运行状态信息的命令行工具。它可以显示Java虚拟机中的类加载、内存、垃圾收集、即时编译等运行状态的信息。

重载与重写区别
1、重载实现的是编译时的多态性,而重写实现的是运行时的多态性。
2、重载发生在一个类中,同名的方法的参数列表要不同;而重写发生在子类与父类之间,重写方法的重写方法要相同。
3、重载方法的返回类型可以修改,而重写方法不能。
4、重载方法的异常可以修改,重写方法的异常可以减少或删除,一定不能抛出新的或者更广的异常。
5、重载方法的访问可以修改,而重写方法的访问一定不能做更严格的限制。

ThreadLocal内存泄漏
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ConcurrentHashMap特点
和hashmap数据结构一致,数组配合链表、红黑树。
put、resize操作,数组元素如果为空,先cas操作,若失败分段加锁。支持多线程并发操作

参考链接
volatile关键字?MESI协议?指令重排?内存屏障?
深入解读CompletableFuture源码与原理
史上最全阿里 Java 面试题总结
IO流关闭到底关闭了什么
AQS为什么要用双向链表?
ThreadLocal内存泄露原因

posted @   sahara-随笔  阅读(37)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示