Java 世界历险记:从一行 System.out.println 到整个 Spring 宇宙
你敲下 System.out.println("Hello"),屏幕上弹出两个字。你觉得自己会 Java 了。直到有一天,你打开了 JVM 的文档,才发现自己连这句话是怎么跑到屏幕上的都不知道。
序章:一个让我重新认识 Java 的瞬间
某天我写了一段再普通不过的代码:
java
ini
public class Main {
public static void main(String[] args) {
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a.equals(c)); // true
}
}
前两个输出我勉强能解释,第三个输出让我彻底懵了。
a == c 是 false,但 a.equals(c) 是 true。一个说"不相等",一个说"相等"。
我盯着屏幕,第一次意识到:我用了这么久 Java,连"相等"是什么意思都没搞明白。
于是,我决定往下挖。这一挖,挖出了 JVM、字节码、内存模型、垃圾回收、Spring 宇宙......一个比我想象中庞大得多的世界。
第一章:Java 代码的一生------从 .java 到机器码
你写的 .java 文件,并不是直接跑的。它经历了一段"变身之旅":
text
css
Main.java
↓ javac 编译
Main.class(字节码)
↓ JVM 解释/编译
机器码
第一步:编译成字节码
bash
css
javac Main.java
这行命令把 .java 编译成 .class,里面是字节码。
字节码不是机器码,也不是源代码,而是一种"中间语言"。想看它长什么样?用 javap:
bash
css
javap -c Main
输出:
text
yaml
0: ldc #2 // String hello
2: astore_1
3: getstatic #3 // Field java/lang/System.out
6: ldc #4 // String hello
8: invokevirtual #5 // Method java/io/PrintStream.println
原来 System.out.println("hello") 不是一行代码,而是一堆指令。
第二步:JVM 负责"翻译"
JVM(Java 虚拟机)就是那个"翻译官"。它有两种模式:
- 解释执行:读一条字节码,执行一条。
- 即时编译(JIT):把"热点代码"编译成机器码,直接跑。
Java 比 C 慢?那是以前。有了 JIT,热点代码跑起来和 C 差不多。
第三步:为什么叫"一次编写,到处运行"
因为字节码是"平台无关"的。同一份 .class,在 Windows 上由 Windows 的 JVM 翻译,在 Linux 上由 Linux 的 JVM 翻译。
JVM 屏蔽了操作系统的差异,这就是 Java 的最大卖点。
第二章:JVM 内存模型------程序运行时的"五脏六腑"
JVM 在运行时,会把自己的内存分成几块:
1. 堆(Heap)------最大的一块
所有对象都在这里。 你 new 出来的东西,全在堆里。
java
sql
User user = new User(); // user 对象在堆里
堆是线程共享的,也是垃圾回收的主战场。
2. 栈(Stack)------每个线程一个
方法调用时的局部变量、方法参数、返回值,都在这。
java
arduino
public void hello() {
int x = 1; // x 在栈里
String s = "hi"; // s 的引用在栈里,对象在堆里
}
栈是线程私有的。每个线程有自己的栈,方法调用就压栈,方法结束就弹栈。
3. 方法区(Method Area)------类信息
类的元信息、常量、静态变量,都在这。
从 JDK 8 开始,方法区的实现叫元空间(Metaspace),它在本地内存里,不在 JVM 堆里。
4. 程序计数器(PC Register)
记录当前线程执行到哪条字节码。
每个线程一个,是 JVM 内存里唯一不会 OOM 的区域。
5. 本地方法栈
给 native 方法用的。
这五块合起来,就是 JVM 的"五脏六腑"。
第三章:垃圾回收------JVM 的"自动清洁工"
Java 最爽的一点:不用手动 free。
但 JVM 怎么知道哪些对象该回收?
引用计数?不行
如果 A 引用 B,B 引用 A,两个都回收不了。
所以 JVM 用的是"可达性分析"。
可达性分析:从 GC Roots 出发
GC Roots 包括:
- 栈里的局部变量。
- 方法区的静态变量。
- 常量引用。
- JNI 引用。
从这些根出发,能走到的对象,就是"活的";走不到的,就是"垃圾"。
分代回收:新对象查得勤,老对象查得少
堆被分成两块:
- 新生代(Young):新对象在这,回收频繁,用复制算法。
- 老年代(Old):活得久的对象,回收少,用标记-整理算法。
为什么这么分?因为大部分对象"朝生夕死"。
垃圾回收器:从 Serial 到 G1 到 ZGC
- Serial:单线程,简单,适合小内存。
- Parallel:多线程,吞吐优先。
- CMS:并发收集,低延迟。
- G1:分区收集,平衡吞吐和延迟,JDK 9 后的默认。
- ZGC:超低延迟,适合大堆。
选哪个?看你的场景:要吞吐选 Parallel,要低延迟选 ZGC。
第四章:Java 语言的"武功秘籍"
Java 语言本身,也有不少"招式"。
1. 面向对象:封装、继承、多态
java
scala
class Animal {
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("汪汪"); }
}
Animal a = new Dog();
a.speak(); // 汪汪
多态的本质是:编译时看类型,运行时看对象。
2. 泛型:类型安全的"模板"
java
ini
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(1); // 编译报错
泛型只在编译期有效,运行时会被"擦除"。
java
ini
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
这就是"类型擦除",也是 Java 泛型最容易被误解的地方。
3. 反射:运行时的"透视眼"
java
ini
Class<?> clazz = Class.forName("com.example.User");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("setName", String.class);
method.invoke(obj, "张三");
反射让你在运行时"看到"类的结构、调用方法、访问字段。
Spring 的依赖注入、MyBatis 的映射,全靠反射。
4. 注解:给代码贴"标签"
java
typescript
@Override
public String toString() { return "hello"; }
@Deprecated
public void oldMethod() {}
@SuppressWarnings("unchecked")
public void unchecked() {}
注解本身不做任何事,是"处理注解的代码"让它有了意义。
比如 @Override 由编译器检查,@Autowired 由 Spring 处理。
5. Lambda 和 Stream:函数式编程
java
rust
List<String> names = Arrays.asList("张三", "李四", "王五");
names.stream()
.filter(n -> n.startsWith("张"))
.map(String::toUpperCase)
.forEach(System.out::println);
Lambda 让 Java 也能写出"函数式"的代码。
Stream 让集合操作像 SQL 一样流畅。
第五章:Spring 宇宙------Java 框架的"复仇者联盟"
如果说 Java 语言是"武功",那 Spring 就是"门派"。
Spring Core:IoC 和 DI
IoC(控制反转):对象的创建权,从你手里交给 Spring。
DI(依赖注入):Spring 把依赖"注入"到你的对象里。
java
kotlin
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
你不用 new UserRepository(),Spring 帮你 new。
Spring AOP:面向切面编程
把"日志、事务、权限"这些横切逻辑,从业务代码里抽出来。
java
less
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint jp) {
System.out.println("调用方法:" + jp.getSignature());
}
}
AOP 的底层是动态代理:JDK 动态代理或 CGLIB。
Spring MVC:Web 层
java
less
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
@RestController + @GetMapping,一个接口就出来了。
Spring Boot:把 Spring 变简单
java
typescript
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
一行代码,启动整个应用。内嵌 Tomcat,自动配置,开箱即用。
Spring Boot 的"自动配置"是它最核心的魔法: 根据你引入的依赖,自动配置相应的 Bean。
Spring Cloud:微服务的"全家桶"
- Eureka / Nacos:服务注册与发现。
- OpenFeign:服务间调用。
- Gateway:网关。
- Hystrix / Sentinel:熔断限流。
- Config / Nacos:配置中心。
Spring Cloud 让微服务不再是"架构师才懂的东西"。
MyBatis / MyBatis-Plus / JPA:持久层
java
kotlin
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
}
MyBatis 让你手写 SQL,灵活;JPA 让你面向对象,简洁。
MyBatis-Plus 在 MyBatis 上加了 CRUD 封装,开发更快。
第六章:Java 的"家族图谱"
Java 不只是语言,它是一个生态。
- Java SE:标准版,基础语言 + 核心库。
- Java EE / Jakarta EE:企业版,Servlet、JPA、EJB。
- Java ME:微型版,嵌入式。
- JavaFX:桌面 GUI。
- Android:移动端(用 Java/Kotlin)。
JDK、JRE、JVM 的区别:
- JVM:虚拟机,执行字节码。
- JRE:JVM + 核心类库,能运行 Java 程序。
- JDK:JRE + 编译器 + 工具,能开发 Java 程序。
一张图记住:JDK > JRE > JVM。
第七章:那些年 Java 被吐槽的"黑点"
1. 啰嗦
java
typescript
// Java
public class Main {
public static void main(String[] args) {
System.out.println("hello");
}
}
python
bash
# Python
print("hello")
Java 的仪式感,确实重了点。
2. 泛型擦除
java
arduino
List<String> list = new ArrayList<>();
list.add("hello");
// 运行时,你不知道 list 里是 String
擦除让泛型失去了运行时信息,也带来了一堆"类型转换警告"。
3. 空指针
java
scss
user.getAddress().getCity().getName();
// 任何一环是 null,就 NPE
NPE 是 Java 程序员的"日常噩梦"。
JDK 14 引入了 Optional,但用起来还是不如 Kotlin 的 ?. 优雅。
4. 启动慢、内存大
JVM 启动要几秒,内存占用动辄几百 MB。
这也是为什么云原生时代,Go 和 Rust 开始抢 Java 的饭碗。
但 Java 也在进化:GraalVM 让 Java 能编译成原生镜像,启动快、内存小。
第八章:Java 的"武功心法"
学 Java 这么多年,我最大的感悟是:
Java 不是一门语言,而是一个"体系"。
- 语言只是表层。
- JVM 是地基。
- 框架是楼房。
- 生态是城市。
学 Java 的路径,大概是这样的:
- 先学语法:类、对象、继承、多态、集合、异常。
- 再学 JVM:内存模型、GC、类加载、字节码。
- 然后学框架:Spring、Spring Boot、MyBatis。
- 最后学架构:微服务、分布式、高并发。
但最重要的,不是学了多少框架,而是理解"为什么这么设计"。
为什么要有 GC?因为手动管理内存太容易出错。 为什么要有 IoC?因为对象依赖太复杂。 为什么要有 AOP?因为横切逻辑太分散。
每一个设计,都是为了解决一个具体问题。
终章:回到最初的问题
现在,我可以回答开头那个问题了。
java
ini
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a.equals(c)); // true
- a == b 是 true:因为 "hello" 在字符串常量池里,a 和 b 指向同一个对象。
- a == c 是 false:因为 new String() 在堆里创建了一个新对象,地址不同。
- a.equals(c) 是 true:因为 equals 比较的是内容,不是地址。
一个"相等",背后是字符串常量池、堆、栈、引用和内容的复杂关系。
这就是 Java 的底层世界:你看到的每一行代码,背后都有一整套机制在运转。
P.S. 写完这篇文章后,我又写了一次 String a = "hello"; String b = "hello";,然后想:如果 JVM 哪天不优化了,这个世界会怎样?
P.P.S. 但 JVM 一直在优化,从解释到 JIT,从 Serial GC 到 ZGC。Java 没老,它一直在进化。