一文看懂JVM核心架构:拆解 “搬运工、仓库、加工厂、对外窗口”

JVM核心功能:
JVM核心功能


一文看懂JVM核心架构:拆解 “搬运工、仓库、加工厂、对外窗口”

想搞懂 Java 为什么能 “一次编写,到处运行”?核心就在 JVM这个 “隐形容器” 里。
为了让复杂的架构更易理解,咱们把 JVM 拆解成 “搬运工”、“仓库”、“加工厂”、“对外窗口” 四大核心模块,带你完整看懂 JVM 的工作逻辑。

1. 类加载器(搬运工:Class Loader)
JVM 要运行代码,首先得把硬盘上的 .class 字节码文件 “搬” 进内存 —— 这就是类加载器的核心任务。
加载策略:不搞 “一次性搬运”,而是按需动态加载,用到哪个类才加载哪个,减少启动时的内存占用。
核心机制:严格遵循 “双亲委派模型”—— 搬运前先问 “上级”(父类加载器)有没有搬过,避免 String 这类核心类被自定义同名类冒充,保证类的唯一性和安全性。
完整流程:加载→验证→准备→解析→初始化,每一步都有严格校验,比如字节码验证会杜绝非法指令,防止恶意代码入侵。
加载器分类:启动类加载器(加载系统核心类)、扩展类加载器(加载扩展库)、应用类加载器(加载项目代码)、自定义类加载器(满足特殊需求),各司其职。

2. 运行时数据区(仓库:Runtime Data Areas)
这是 JVM 存储数据的 “核心仓库”,所有程序运行时的数据都在这里流转,按归属分为 “线程共享” 和 “线程私有” 两类,避免数据混乱。

区域 归属 核心作用 关键特性
堆 (Heap) 线程共享 存放所有 new 出来的对象实例,是最大的内存区域 GC(垃圾回收)的主要战场,所有对象存活与回收都在这里发生
方法区 (Method Area) 线程共享 存储类元数据(类结构、属性、方法信息)、常量池、静态变量 相当于 “类的图纸仓库”,提供对象创建的模板
虚拟机栈 (Stack) 线程私有 存放局部变量,每个方法执行对应一个 “栈帧”(包含参数、返回值、局部变量) 方法执行时入栈,执行完毕后出栈,自动释放内存,不会产生垃圾
程序计数器 (PC) 线程私有 记录当前线程执行的指令位置 像 “导航指针”,CPU 切换线程后能快速恢复执行,避免 “迷路”
本地方法栈 线程私有 为 JNI 调用的本地方法(如 C/C++ 编写的方法)提供内存支持 与虚拟机栈功能类似,专门服务本地方法调用

3. 执行引擎(加工厂:Execution Engine)
内存里的字节码是 “中间指令”,CPU 看不懂 —— 执行引擎就是把字节码翻译成机器码的 “加工厂”,同时负责内存清理,保障运行效率。

双引擎协作:
解释器:逐行翻译字节码,翻译一句执行一句,启动快但执行慢,适合低频代码;
JIT 编译器(即时编译):识别 “热点代码”(频繁执行的代码),一次性整块编译成机器码并缓存,后续直接复用,大幅提升执行速度。
垃圾回收器 (GC):“仓库清洁工”,自动识别堆内存中不再被引用的对象,通过分代收集、标记 – 清除、标记 – 复制、标记 – 整理等算法回收内存,支持 SerialGC、ParallelGC、CMS、G1、ZGC 等多种回收器,适配不同性能需求。
同步与锁机制:为多线程并发保驾护航,提供偏向锁、轻量级锁、重量级锁、自旋锁等多级锁优化,结合 monitor 监视器与 synchronized 底层实现,平衡并发安全与执行效率。

4. 本地接口与跨平台支持(对外窗口:Native & Cross-Platform)
Java 无法直接操作底层硬件和系统,“对外窗口” 负责打通 Java 与外部的连接,同时实现跨平台特性。
JNI(本地方法接口):Java 与底层系统的 “翻译官”,通过调用本地库方法,实现 I/O 操作、硬件交互等 Java 本身无法完成的功能。
I/O 优化机制:支持堆内缓冲区与直接缓冲区(Direct Buffer),搭配 I/O 多路复用、内存映射(mmap)技术,减少数据拷贝,提升读写效率。
跨平台核心:抽象虚拟运行环境,隔离字节码与底层硬件 / 操作系统差异,不管是 Windows、Linux 还是 macOS,都能通过对应的 JVM 解析执行,实现 “一次编写,到处运行”。

5. 异常处理与安全机制(防护盾:Protection)
JVM 内置 “防护盾”,保障程序稳健运行,抵御恶意攻击。
异常处理:通过 athrow 字节码指令触发异常,依托 Throwable 及其子类(Error、Exception)构建异常链,异常表存储捕获 / 处理信息,让程序在出错时能优雅响应,而非直接崩溃。
安全沙箱:通过安全管理器限制系统资源(文件、网络、内存)访问,结合类加载验证、字节码校验,防止核心 API 被篡改,抵御恶意代码入侵。

6. 性能监控与调优(优化器:Optimization)
JVM 提供丰富的监控工具和调优参数,帮你排查性能问题,让程序跑得更快更稳。
监控工具接口:JVM TI(JVM Tool Interface)替代早期的 JVMPI,支持第三方监控工具(如 JConsole、VisualVM)接入,实时采集运行数据。
核心监控指标:GC 日志(回收次数、耗时)、线程状态(运行、阻塞、等待)、内存使用量(堆 / 方法区占用)、类加载统计(加载 / 卸载数量),全面掌握 JVM 运行状态。
常用调优参数:配置堆大小(-Xms 初始堆、-Xmx 最大堆)、选择 GC 收集器(-XX:+UseG1GC)、调整 JIT 编译阈值(-XX:CompileThreshold)、设置直接缓冲区大小(-XX:MaxDirectMemorySize)等,按需优化性能。

总结 JVM 完整工作流程:
类加载器按 “双亲委派模型”,按需加载 .class 文件到方法区;
执行引擎通过解释器 / JIT 编译器,将方法区的字节码翻译成机器码;
程序运行时,虚拟机栈存储局部变量与栈帧,堆创建对象实例,程序计数器维护执行位置;
多线程并发时,锁机制保障数据安全,GC 实时清理堆内无用对象;
需底层操作时,通过 JNI 调用本地方法,I/O 优化机制提升数据传输效率;
异常发生时,异常链与异常表处理错误,安全机制抵御恶意攻击;
借助监控工具与调优参数,持续优化 JVM 运行性能。

使用JDK动态代理时为何必须实现至少一个接口

这个问题,就要去看一下OpenJDK的源码了:

//在Proxy类里中:
//constructorParams的定义如下:
private static final Class<?>[] constructorParams = { InvocationHandler.class };

//newProxyInstance无限精简之后就是:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
        throws IllegalArgumentException {
    //通过ProxyClassFactory调用ProxyGenerator生成了代理类
    Class<?> cl = getProxyClass0(loader, interfaces);
    //找到参数为InvocationHandler.class的构造函数
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    //创建代理类实例
    return cons.newInstance(new Object[]{h});
}

//在ProxyGenerator类中:
public static byte[] generateProxyClass(final String name,Class<?>[] interfaces, int accessFlags)){}
private byte[] generateClassFile() {}

//上面两个方法,做的就是:
//将接口全部进行代理
//并生成其他需要的方法,比如上面用到的构造函数、toString、equals、hashCode等
//生成对应的字节码
//其实这也就说明了,为何JDK的动态代理,必须需要至少一个接口

Jetty源码分析01

一、Jetty的ScopedHandler的doStart方法,最后一步是将线程私有变量__outerScope设置成null,为什么需要这样做呢?

protected void doStart() throws Exception
{
    try{
        _outerScope=__outerScope.get();
        if (_outerScope==null){
           //本次处理的第一个scope handler
           //告知后续scope handler,_outerScope选我
            __outerScope.set(this);
        }
        super.doStart();
        _nextScope= getChildHandlerByClass(ScopedHandler.class);
    }
    finally
    {
        if (_outerScope==null){
           //本次处理结束
           //为了下次同一个线程处理是,
           //还能正常的设置第一个scope handler
           //必须把threadlocal变量设为null
            __outerScope.set(null);
        }
    }
}

二、Jetty中,ScopedHandler中nextHandle调用顺序是如何的?

//此外,这一节里有一个non scoped handler X,一开始没太看懂调阅顺序。
//后来发现是这样的:
public final void nextHandle(String target...)...
{
    if (_nextScope!=null && _nextScope==_handler){
        //上面条件可以保证下一个handler是scope handler
        _nextScope.doHandle(target,baseRequest,request, response);
    }
    else if (_handler!=null){
        //non scpoe handler调用下一个handler的
        super.handle(target,baseRequest,request,response);
    }
}

感觉类成员的命名不太合适,
比如__outerScope和_outerScope
比如_handler其实一直指向的是下一个handler,是不是该为_nextHandler更好一些?

Java的各种容器

对比一下,Servlet容器、Spring容器和SpringMVC容器。

Servlet容器,是用于管理Servlet生命周期的。
Spring容器,是用于管理Spring Bean生命周期的。
SpringMVC容器,适用于管理SpringMVC Bean生命周期的。

Tomcat/Jetty启动,对于每个WebApp,依次进行初始化工作:
1、对每个WebApp,都有一个WebApp ClassLoader,和一个ServletContext
2、ServletContext启动时,会扫描web.xml配置文件,找到Filter、Listener和Servlet配置

3、如果Listener中配有spring的ContextLoaderListener
3.1、ContextLoaderListener就会收到webapp的各种状态信息。
3.3、在ServletContext初始化时,ContextLoaderListener也就会将Spring IOC容器进行初始化,管理Spring相关的Bean。
3.4、ContextLoaderListener会将Spring IOC容器存放到ServletContext中

4、如果Servlet中配有SpringMVC的DispatcherServlet
4.1、DispatcherServlet初始化时(其一次请求到达)。
4.2、其中,DispatcherServlet会初始化自己的SpringMVC容器,用来管理Spring MVC相关的Bean。
4.3、SpringMVC容器可以通过ServletContext获取Spring容器,并将Spring容器设置为自己的根容器。而子容器可以访问父容器,从而在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。
4.2、初始化完毕后,DispatcherServlet开始处理MVC中的请求映射关系。

我面试的时候,有段时间经常会问一个很坑问题,Servlet默认是单例模式的,Spring的Bean默认是单例模式的,那Spring MVC是如何处理并发请求的呢?

Servlet容器、Spring容器、SpringMVC容器之间的关系

之前在极客时间上回答老师的问题:

Servlet容器,是用于管理Servlet生命周期的。
Spring容器,是用于管理Spring Bean生命周期的。
SpringMVC容器,适用于管理SpringMVC Bean生命周期的。

Tomcat/Jetty启动,对于每个WebApp,依次进行初始化工作:
1、对每个WebApp,都有一个WebApp ClassLoader,和一个ServletContext
2、ServletContext启动时,会扫描web.xml配置文件,找到Filter、Listener和Servlet配置

3、如果Listener中配有Spring的ContextLoaderListener
3.1、ContextLoaderListener就会收到webapp的各种状态信息。
3.3、在ServletContext初始化时,ContextLoaderListener也就会将Spring IOC容器进行初始化,管理Spring相关的Bean。
3.4、ContextLoaderListener会将Spring IOC容器存放到ServletContext中

4、如果Servlet中配有SpringMVC的DispatcherServlet
4.1、DispatcherServlet初始化时(其一次请求到达)。
4.2、其中,DispatcherServlet会初始化自己的SpringMVC容器,用来管理Spring MVC相关的Bean。
4.3、SpringMVC容器可以通过ServletContext获取Spring容器,并将Spring容器设置为自己的根容器。而子容器可以访问父容器,从而在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。
4.2、初始化完毕后,DispatcherServlet开始处理MVC中的请求映射关系。

HBase简单通讯代码

首先,就要说一下配置问题了。HBase客户端的配置有两种方式,一种是通过配置文件,另一种是通过代码设置。

1、配置文件方式
配置文件名称为hbase-site.xml,该文件必须放置到CLASS_PATH下面才会有效,文件示例如下:
hbase-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
	<property>
		<name>hbase.rootdir</name>
		<value>hdfs://hadoop-master:9000/hbase</value>
	</property>
	<property>
		<name>hbase.cluster.distributed</name>
		<value>true</value>
	</property>
	<property>
		<name>hbase.master</name>
		<value>hdfs://hadoop-master:60000</value>
	</property>
	<property>
		<name>hbase.zookeeper.quorum</name>
		<value>hadoop-master,hadoop-slave01,hadoop-slave02</value>
	</property>
</configuration>

2、通过代码配置方式

        Configuration hbaseConfig = HBaseConfiguration.create();
        hbaseConfig.setInt("timeout", 120000);
        hbaseConfig.set("hbase.master", "hdfs://hadoop-master:60000");
        hbaseConfig.set("hbase.zookeeper.quorum", "hadoop-master,hadoop-slave01,hadoop-slave02");
        hbaseConfig.setInt("hbase.zookeeper.property.clientPort", 2181);
        hbaseConfig.setInt("hbase.client.retries.number", 1);

Continue reading HBase简单通讯代码

eXistDB简单通讯12(HTTP_SOAP)

  • 保存文件
  • 取回文件
  • 查询

1、QueryFileSOAP.java

package com.neohope.existdb.test;

import org.exist.soap.Query;
import org.exist.soap.QueryResponse;
import org.exist.soap.QueryService;
import org.exist.soap.QueryServiceLocator;

import java.net.URL;
import java.nio.charset.Charset;

public class QueryFileSOAP {
    public static void QueryXML(String xquery, String user, String pwd) throws Exception {
        QueryService service = new QueryServiceLocator();
        Query query = service.getQuery(new URL("http://localhost:8080/exist/services/Query"));
        String sessionId = query.connect("neotest", "neotest");

        byte[] queryData = xquery.getBytes(Charset.forName("UTF-8"));
        QueryResponse resp = query.xquery( sessionId, queryData );
        System.out.println( "found: " + resp.getHits() );
        if(resp.getHits() == 0) {
            return;
        }
        else {
            //get 10 results
            byte[][] hits = query.retrieveData(sessionId, 1, 10,
                    true, false, "elements").getElements();
            for (int i = 0; i < hits.length; i++) {
                System.out.println(new String(hits[i], "UTF-8"));
            }
        }

        query.disconnect(sessionId);
    }

    public static void main(String args[]) throws Exception {
        String user = "neotest";
        String pwd = "neotest";
        String query ="for $name in collection('/db/CDA')/ClinicalDocument/recordTarget/patientRole/patient/name \n" +
                "return \n" +
                "<name>{$name}</name> ";
        QueryXML(query, user, pwd);
    }
}

eXistDB简单通讯11(HTTP_SOAP)

  • 保存文件
  • 取回文件
  • 查询

1、GetFileSOAP.java

package com.neohope.existdb.test;

import org.exist.soap.Query;
import org.exist.soap.QueryService;
import org.exist.soap.QueryServiceLocator;

import java.net.URL;


public class GetFileSOAP {
    public static void GetXML(String fileId, String user, String pwd) throws Exception {
        QueryService service = new QueryServiceLocator();
        Query query = service.getQuery(new URL("http://localhost:8080/exist/services/Query"));
        String session = query.connect(user, pwd);

        byte[] data = query.getResourceData(session,
                "/db/CDA/"+fileId,
                true, false, false);
        System.out.println(new String(data, "UTF-8"));
        query.disconnect(session);
    }

    public static void main(String args[]) throws Exception {
        String user = "neotest";
        String pwd = "neotest";
        GetXML("入院患者护理评估单01.xml",user,pwd);
    }
}

eXistDB简单通讯10(HTTP_SOAP)

  • 保存文件
  • 取回文件
  • 查询

1、SaveFileSOAP.java

package com.neohope.existdb.test;

import org.exist.soap.*;

import java.io.BufferedReader;
import java.io.FileReader;
import java.net.URL;
import java.nio.charset.Charset;

public class SaveFileSOAP {
    public static void SaveXML(String xmlFilePath, String user, String pwd) throws Exception {
        AdminService adminService = new AdminServiceLocator();
        Admin admin = adminService.getAdmin(new URL("http://localhost:8080/exist/services/Admin"));
        String session = admin.connect("neotest", "neotest");

        BufferedReader f = new BufferedReader(new FileReader(xmlFilePath));
        String line;
        StringBuffer xml = new StringBuffer();
        while ((line = f.readLine()) != null)
            xml.append(line);
        f.close();

        admin.store(session, xml.toString().getBytes(Charset.forName("UTF-8")), "UTF-8", "/db/CDA/入院患者护理评估单02.xml", true);
        admin.disconnect(session);
    }

    public static void main( String[] args ) throws Exception {
        String user = "neotest";
        String pwd = "neotest";
        SaveXML("PATH_TO_FILE\\入院患者护理评估单02.xml", user, pwd);
    }
}