Tomcat笔记3(server启动分析)
Tomcat的启动是从Bootstrap类当中的main方法开始的,在bat文件的启动命令脚本最终echo之后,其中的输出是org.apache.catalina.startup.Bootstrap.main start,开始发动tomcat。进入main函数中分析其流程:
public static void main(String args[]) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
t.printStackTrace();
return;
}
daemon = bootstrap;
}
try {
String command = "start";
if (args.length > 0) {
command = args;
}
if (command.equals("startd")) {
args = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
t.printStackTrace();
}
}
实例化Bootstrap之后,进入其init()方法当中, /**
* Initialize daemon.
*/
public void init()
throws Exception
{
// Set Catalina path
setCatalinaHome();
setCatalinaBase();
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class;
paramTypes = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object;
paramValues = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
在init方法当中,首先初始化CatalinaHome和CatalinaBase路径,如果这两个路径都未被设定,则默认设置成当前tomcat的工作路径。
以setCatalinaHome()方法为例:
/**
* Set the <code>catalina.home</code> System property to the current
* working directory if it has not been set.
*/
private void setCatalinaHome() {
if (System.getProperty("catalina.home") != null)
return;
File bootstrapJar =
new File(System.getProperty("user.dir"), "bootstrap.jar");
if (bootstrapJar.exists()) {
try {
System.setProperty
("catalina.home",
(new File(System.getProperty("user.dir"), ".."))
.getCanonicalPath());
} catch (Exception e) {
// Ignore
System.setProperty("catalina.home",
System.getProperty("user.dir"));
}
} else {
System.setProperty("catalina.home",
System.getProperty("user.dir"));
}
}
其中System.getProperty当中的catalina.home参数是在启动tomcat的bat文件时,我们设定好的参数,在catalina.bat文件当中echo了这么一段命令:
echo %_EXECJAVA% %JAVA_OPTS% %CATALINA_OPTS% %JPDA_OPTS% %DEBUG_OPTS% -Djava.endorsed.dirs="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Djava.security.manager -Djava.security.policy=="%SECURITY_POLICY_FILE%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% > command.txt
其中设定了catalina.home和catalina.base路径。
以上步骤获取了系统工作路径,接下来进行类加载器的初始化(setup),initClassLoaders(),方法内容如下:
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
以上代码将分别加载Tomcat的三种包:common,server,shared。分别对应catalina.properties文件中:
common.loader=${catalina.home}/lib,${catalina.home}/lib/*.jar
server.loader=
shared.loader=
即为以上路径下面对应的jar包设置ClassLoaders。
下面继续看tomcat的类加载体系:
首先,涉及到JVM启动时类的加载机制,在OS为JVM进程分配内存空间(可以通过JVM启动参数手工设定)之后,程序入口点位于BootStrap类的main方法,在启动这个JavaApp之前首先实例化一个JVM的实例(而随着这个Java app的终止JVM实例也自动消亡),在一个JVM启动之后,其体系结构大致如下:
三个模块:
Class Loader System
Runtime Data Area
Execution Engine
其中Runtime Data Area 包括5种区域:
Heap & Method Area;
JavaStack & Native Method Stack;
Program Counter
以Bootstrap类启动为例,在JVM启动时将之载入内存的具体流程如下:
路径:org.apache.catalina.startup.Bootstrap,启动之前将Bootstrap类对应的Class文件当中的类型信息提取出来,load到JVM的Method Area当中,这也是被称作RTTI(Run-Time Type Infomation)用来保存运行时类型信息的一种方式。而且,这个Method Area是多线程共享的,所以当存在两个线程需要同时读取一个类的类型信息时,如果该类还没有被载入,则只能由其中一个线程先去将之load进来,另一线只有先等待了。
Method Area的具体内容:
Type Information
The Constant Pool
Field Information
Method Information Class Variables
A Reference to Class ClassLoader
A Reference to Class Class
Method Tables
之后,读取代码当中的静态初始化内容:
private static final Log log = LogFactory.getLog(Bootstrap.class);
protected static final String CATALINA_HOME_TOKEN = "${catalina.home}";
protected static final String CATALINA_BASE_TOKEN = "${catalina.base}";
private static Bootstrap daemon = null;
......
对于第一行代码创建log对象,会在heap当中的Method Area存放当前log对应Class的Type Infomation,之后在线程Stack当中创建log引用(做为Log对象的reference);
在创建String类型时,如果采用上面所述的方式进行,将string的内容存放与heap中Method Area的常量池当中。之后进行对象创建时,首先判定如果当前Method Area当中不包含该类的类型信息,Load该类对应的Class文件,根据Method Area当中的类型信息计算当前对象的所需内存情况,在Heap当中为止分配内存空间,并将该对象的引用压倒栈中。
整个过程中,程序计数器为当前线程所独享,被用于定位下一执行指令的位置。
其中Heap和其中的Method Area是多线程之间共享的,而Java Stack/Program Counter是当前线程独享的,本地方法栈Native Stack是当程序中用到的本地方法时使用的,当JVM调用本地方法时,它就进入了一个不受虚拟机限制的空间,这时可能会产生JVM无法控制的内存问题:
Native Heap OutOfMemory!
另外提一下,在JVM当中压栈和出栈都是以帧为单位的(栈是跟着线程走的,有线程就有栈),每一帧包含的信息包括:
在这个图当中stack frame2对应的方法A被先调用,压栈之后,在其中调用stack frame2对应B方法,frame2被压栈。可以看出在Java 当中,栈并不是跟着方法走的,只是存储线程相关信息的地方,在方法调用时不会新产生stack,当然线程间的调用除外。
JVM在是否通过反射显示加载上将加载分为所谓的两种:显式加载和隐式加载;
从是否及时加载分成两种:pre-loading和lazy-loading。
而ClassLoader本身也有四种类型:BootstrapClassLoader,ExtensionClassLoader,AppClassLoader,URLClassLoader.分别针对一种加载场景,不过可以使用不同的ClassLoader加载一个相同Class,但是该Class对象的内容在启动JVM时通过pre-loading的方式加载系统的基本类库,其顺序大致如下:
BootstrapClassLoader
|
ExtensionClassLoader
|
AppClassLoader
通过New A().getClass().getClassLoader().toString()测试得到:
sun.misc.Launcher$AppClassLoader@1a16869
从上面的加载顺序可以看出,一个ClassLoader的加载路径基本先找JRE的lib目录下面的resource.jar和rt.jar文件(resource.jar里面是很多properties文件用来定义配置项),然后进行load,在最后定位到Test类通过AppClassLoader进行加载。
以上大概描述了下JVM当中的内存处理的理解,下面继续Tomcat的启动。
在JVM启动之后,载入Tomcat相关Class时,首先Setup需要用到的ClassLoaders,分别对应不同路径下的jar包。
在Create Classloader时,tomcat通过批量读取指定路径下的jar包,统一创建一个classloader,最终通过org.apache.catalina.startup.ClassLoaderFactory.createClassLoader方法处理。
在org.apache.catalina.startup.Bootstrap.createClassLoader方法中通过MBeanServer的注册统一管理当前的ClassLoader,代码:
// Retrieving MBean server
MBeanServer mBeanServer = null;
if (MBeanServerFactory.findMBeanServer(null).size() > 0) {
mBeanServer = MBeanServerFactory.findMBeanServer(null).get(0);
} else {
mBeanServer = ManagementFactory.getPlatformMBeanServer();
}
// Register the server classloader
ObjectName objectName =
new ObjectName("Catalina:type=ServerClassLoader,name=" + name);
mBeanServer.registerMBean(classLoader, objectName);
通过MBeanFactory取得一个MBeanServer实例,并将init之后的ClassLoader注册,之后负责针对该MBean管理。
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
接下来设置当前线程的ContextClassLoader为CatalinaLoader,在Thread类当中存在一个contextClassLoader(类型为ClassLoader)的属性,用来在当前线程需要创建新的Class的时候进行加载。加载规则是首先使用父ClassLoader进行载入,如果无法载入,再自己进行Class载入。
然后通过反射方式取得一个Catalina类的实例,并调用它的setParentClassLoader方法将它的ParentClassLoader,在Catalina类当中存在一个parentClassLoader的属性,在类初始化时默认被设置为:
/**
* The shared extensions class loader for this server.
*/
protected ClassLoader parentClassLoader =Catalina.class.getClassLoader();
通过System.out.println(new Catalina().getClass().getClassLoader().toString());得到结果:sun.misc.Launcher$AppClassLoader@18d107f,可见默认的Catalina的ClassLoader是AppClassLoader。那么为什么在这里设置成为sharedLoader(在set这个sharedLoader时候,由于properties当中相应配置为空,则默认被设置成为commonLoader)呢?这个在之后Catalina类在load的时候是要用到的。
在init()方法的最后一步,将反射生成的catalina实例赋值给了bootstrap当中的catalinaDaemon变量。做为之后load、start、stop server的时候被调用的实例。
在接下来通过启动参数进行判定,进入不同的分支:
参数为startd:
args = "start";
daemon.load(args);
daemon.start();
将参数重置为start主要为了调用load方法操作。(注:这个是apache先前的Bug 47881,先前写的是args)。
参数为start:
daemon.setAwait(true);
daemon.load(args);
daemon.start();
这个比startd增加了一个setAwait操作,其实质是在Server启动完成之后,主线程并不退出,而是执行server.setAwait()方法,在该方法中启动一个Socket监听8005端口(timeout为十秒钟),用于监听来自8005端口上的远程关闭命令。
然后是stop和stopd参数,如果以start方式启动tomcat时,在setAwait当中一直while(true),知道发现shutDown的命令为止,则跳出循环,执行stop()方法。
当执行stop方法是,首先查找server,如果不存在,则通过Digest方式进行关闭;查到server则进行socket方式传输shutdown命令进行远程关闭。使用stopd参数则直接调用:
// Shut down the server
try {
getServer().stop();
} catch (LifecycleException e) {
log.error("Catalina.stop", e);
}
所以对应于tomcat的启动方式start和startd,在关闭时应该分别调用stop和stopd,在使用eclipse将tomcat嵌入进去的时候,应该通过startd和stopd方式进行启动和终止,需要tomcat独立运行的时候则通过start和stop进行启动和终止,通过主线程维持连接。
在启动之前的load方式:
在start之前,load当中通过digest的方式进行系统组件的加载。通过反射调用Catalina的load方法,在其中通过createStartDigester();方法创建digester对象,读取系统的server.xml文件将其中的各个组件加入add到digester当中。
本地的启动描述文件的路径:
D:/dev/tomcat7.0/output/build/webapps/docs/architecture/startup/serverStartup.txt
文本:
http://tomcat.apache.org/tomcat-7.0-doc/architecture/startup/serverStartup.txt
UML:
http://tomcat.apache.org/tomcat-7.0-doc/architecture/startup/serverStartup.pdf
页:
[1]