语言细节、并发原语、类加载与 GC——两块拼起来才是完整的 Java 运行时图景。
一、语言与对象
Java 内部类
内部类是定义在另一个类里面的类,与之相对应,包含内部类的类被称为
外部类,
内部类的作用有:(1)内部类提供了更好的封装,可以把内部类隐藏在外
部类之内,不允许同一包中的其他类访问,(2)内部类的方法可以直接访问外
部类的所有数据,包括私有的数据,(3)内部类的种类:成员内部类、静态内
部类、方法内部类、匿名内部类
Java 关键字 final 和 static 是怎么使用的。
final:
1、final 变量即为常量,只能赋值一次。
2、final 方法不能被子类重写。
3、final 类不能被继承。
static:
1、static 变量:对于静态变量在内存中只有一个拷贝(节省内存),JVM 只为静态分配一次
内存,
在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过
对象来访问(但是这是不推荐的)。
Java static
代码块:
static 代码块是类加载时,初始化自动执行的。
static 方法
static 方法可以直接通过类名调用,任何的实例也都可以调用,因此 static 方法中不能用 this
和 super 关键字,
不能直接访问所属类的实例变量和实例方法(就是不带 static 的成员变量和成员成员方法),
只能访问所属类的静态成员变量和成员方法
Java String、StringBuffer、StringBuilder 区别
1、三者在执行速度上:StringBuilder > StringBuffer > String (由于 String 是常量,不可改
变,拼接时会重新创建新的对象)。
2、StringBuffer 是线程安全的,StringBuilder 是线程不安全的。(由于 StringBuffer 有缓冲区)
Java 反射机制
Java 的反射机制是在运行状态中,对于任意一个类,都能够获取到这个
类的所有属性和方法,对于任意一个对象,能够调用它的任意一个方法和属
性, 包括私有的方法和属性,这种动态地获取信息以及动态的调用对象的方
法的功能就称之为 Java 的反射机制。
从对象出发,通过反射(.class 类)可以获取到类的完整信息,(类名、
class 类型、所在包、具有的所有方法 Method[]类型、某个方法的完整信息,
包括修饰符、返回值类型、异常、参数类型、所有属性 Field[ ]、某个属性的完
整信息,构造器 Constructors,调用类的属性或方法。
Java Cglib
CGLIB 用子类拦截方法:Enhancer 生成未实现接口也能代理的类。Spring AOP 在目标无接口时走 CGLIB。final 类/方法无法增强。Objenesis 可绕过构造器。
对比 JDK 动态代理:后者只对接口。CGLIB 调用 super 时要注意两次调用(equals/hashCode)。Java 9+ 模块对非法访问更严,Spring 已转向更高版本的代理实现。
Android 上 ART 对运行时代理不友好,一般不用 CGLIB。排查 AOP 失效先看是否 final、是否自调用绕过代理。生成类会占 Metaspace,注意泄漏 Enhancer 缓存。
二、并发
可重入锁(递归锁)
可重入锁允许同一线程多次获得同一把锁而不死锁。Java 里 synchronized 和 ReentrantLock 都是可重入的:内部记持有线程和重入次数,释放时次数减到零才真正放开。递归调用、同一个类里多个同步方法互相调用,都依赖这个特性。不可重入锁在这种场景会把自己卡住。
可重入不等于可以随便嵌套。嵌套过深会让锁持有时间变长,放大竞争。公平锁能减少饥饿,但吞吐通常更差。写业务时要分清锁保护的是哪块状态,避免一把大锁包住无关代码。排查死锁用 jstack 看等待图。能用并发集合或分段锁就不要把整张表锁住。重入次数异常升高,往往说明调用路径有意外递归。
Java 线程中 sleep 和 wait 的区别
(1)这两个方法来自不同的类,sleep 是来自 Thread,wait 是来自 Object;
(2)sleep 方法没有释放锁,而 wait 方法释放了锁。
(3)wait,notify,notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何
地方使用。
Java JUC:Semaphore控制某个方法允许并发访问线程的个数
在Java 中常使用Semaphore(信号量)来进行并发编程,Semaphore 控
制的是线程并发的数量,实例化一个Semaphore 对象,如Semaphore
semaphore= newSemaphore(5,true) ,其创建了对象 semaphore,并初始化了5 个信号量,
即最多允许5 个线程并发访问,在执行的任务中,调用 semaphore 的acquire
() 方法请求一个信号量,这时信号量个数就减 1,(一旦没有可使用的信号
量,再次请求就会阻塞),来执行任务,执行完任务,调用 semaphore 的
release()方法释放一个信号量此时信号量的个数就会加 1 。
Netty
Netty 是异步事件驱动的网络框架:Boss 接连接,Worker 跑 NIO 事件。ChannelPipeline 串 Handler,入站出站方向相反。ByteBuf 池化减少分配。编解码用 LengthField 或自定义帧。
适合高并发 TCP 网关、MQTT Broker、RPC。零拷贝与堆外内存要配对 leak 检测。心跳 IdleStateHandler。粘包拆包必须在解码器解决,不能假设一次 read 一帧。
线程模型不要在 Handler 里阻塞。业务丢到业务线程池。Android 上较少直接用 Netty,更多在服务端。调优看水位线 WRITE_BUFFER_WATER_MARK 与连接数。
三、JVM
JVM 垃圾回收器及其算法
JVM 垃圾回收先分算法再分回收器。标记-清除、复制清理、分代收集是基础;Serial/Parallel 偏吞吐,CMS 用并发标记降低 STW 但有碎片,G1 按 Region 做混合回收,ZGC 用着色指针追求把停顿压到毫秒级。
选垃算法时要看对象生命周期:短命对象适合复制清理,老年代适合标记整理。分代假设大多数对象朝生夕死,这也是 Eden/Survivor 存在的原因。
读参数前先把各收集器的 STW 点画清楚:Initial Mark、Remark、Cleanup 各占多久。面试常问 CMS 浮动垃圾、G1 Mixed GC 与 ZGC 的 Relocation Set 如何选取。
Java 双亲委派机制
双亲委派让类加载先请示父加载器,避免核心类被替换。启动类加载器、平台/扩展、应用加载器形成链。相同类名由不同加载器加载则类型不相等。
需要打破的场景:Tomcat 隔离 WebApp、JDBC SPI 由启动器加载接口却要找实现、热部署自定义加载器。实现时常重写 loadClass 先找自己再委派。
线程上下文加载器是 SPI 的补丁。排查 ClassNotFound 与 LinkageError 要打印加载器树。Android 的 PathClassLoader/DexClassLoader 类似但委托规则不同。不要随意 setContextClassLoader。
JVM 调优
JVM 调优从业务指标出发:延迟、吞吐量、Full GC 频次与 CPU 。先用 GC 日志、jstat、async-profiler 看清是分配过快、老年代过大还是 STW 过长,再动 -Xms/-Xmx、新生代比例和回收器参数。
G1 下关注 MaxGCPauseMillis、混合回收与 Humongous 对象;ZGC/Shenandoah 更适合大堆低停顿。Metaspace 泄漏要查动态类加载,直接加堆往往只是掩盖问题。
避免一次改一堆参数。每次只改一类因子并对比压测。上线前用同等负载的 GC log 做回湮,把平均停顿与 P99 写进容量标准。
四、工程
Checkstyle
Checkstyle 按规则检查 Java 源码风格:命名、import 顺序、括号位置、行宽和 javadoc。它不替代编译器,只是把团队约定变成可自动执行的门禁,常挂在 Maven / Gradle 和 CI 上。
落地时先选一份接近团队现状的配置(Google 或 Sun),再逐项打开,而不是一次开到最严导致全红。抑制注解要写原因,避免大面积 ignore。和 Spotless / Palantir 格式化工具搭配时,风格类问题交给格式化器,Checkstyle 专注语义约定。规则文件应进版本库,保证本地与流水线一致。
Java-JNI
JNIEnv 和 JavaVM
1.JavaVm
JavaVM 是虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程
共用一个 JavaVM。
2.JNIEnv
JNIEnv 表示 Java 调用 native 语言的环境,是一个封装了几乎全部 JNI 方法
的指针。
JNIEnv 只在创建它的线程生效,不能跨线程传递,不同线程的 JNIEnv 彼此独立。
native 环境中创建的线程,如果需要访问 JNI,必须要调用
AttachCurrentThread 关联,并使用 DetachCurrentThread 解除链接。
Java 与 JVM 速查:反射、类加载、GC 与并发原语
https://lautung.com/archives/java-and-jvm
评论