重要【虚拟机、类、web工程启动加载顺序】

java虚拟机启动顺序:

java类加载顺序:

public class SSClass
{
     static
     {
         System.out.println( "SSClass" );
     }
}   
public class SuperClass extends SSClass
{
     static
     {
         System.out.println( "SuperClass init!" );
     }
 
     public static int value = 123 ;
 
     public SuperClass()
     {
         System.out.println( "init SuperClass" );
     }
}
public class SubClass extends SuperClass
{
     static
     {
         System.out.println( "SubClass init" );
     }
 
     static int a;
 
     public SubClass()
     {
         System.out.println( "init SubClass" );
     }
}
public class NotInitialization
{
     public static void main(String[] args)
     {
         System.out.println(SubClass.value);
     }
}

运行结果:

1
2
3
SSClass
SuperClass init!
123

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段

 这里写图片描述

 一加载:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(可以从一个Class文件、网络、动态生成、数据库等中获取);
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

二验证:

三准备:

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

1
public static int value= 123 ;

那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

四解析:

五初始化:

真正开始执行类中定义的java程序代码(准备阶段变量已经赋过一次系统要求的初始值,此阶段是根据程序猿通过程序制定的主观计划去初始化类变量和其他资源)

初始化阶段是执行类构造器<clinit>()方法的过程.编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:

1
2
3
4
5
6
7
8
9
public class Test
{
     static
     {
         i= 0 ;
         System.out.println(i); //这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
     }
     static int i= 1 ;
}

<clinit>()方法与实例构造器<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类<init>()方法执行之前,父类的<clinit>()方法方法已经执行完毕,回到本文开篇的举例代码中,结果会打印输出:SSClass就是这个道理。

由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
<clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有好事很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

package jvm.classload;
 
public class DealLoopTest
{
     static class DeadLoopClass
     {
         static
         {
             if ( true )
             {
                 System.out.println(Thread.currentThread()+ "init DeadLoopClass" );
                 while ( true )
                 {
                 }
             }
         }
     }
 
     public static void main(String[] args)
     {
         Runnable script = new Runnable(){
             public void run()
             {
                 System.out.println(Thread.currentThread()+ " start" );
                 DeadLoopClass dlc = new DeadLoopClass();
                 System.out.println(Thread.currentThread()+ " run over" );
             }
         };
 
         Thread thread1 = new Thread(script);
         Thread thread2 = new Thread(script);
         thread1.start();
         thread2.start();
     }
}

运行结果:(即一条线程在死循环以模拟长时间操作,另一条线程在阻塞等待)

1
2
3
Thread[Thread- 0 , 5 ,main] start
Thread[Thread- 1 , 5 ,main] start
Thread[Thread- 0 , 5 ,main]init DeadLoopClass

需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次
将上面代码中的静态块替换如下:

1
2
3
4
5
6
7
8
9
10
11
12
static
         {
             System.out.println(Thread.currentThread() + "init DeadLoopClass" );
             try
             {
                 TimeUnit.SECONDS.sleep( 10 );
             }
             catch (InterruptedException e)
             {
                 e.printStackTrace();
             }
         }

运行结果:

1
2
3
4
5
Thread[Thread- 0 , 5 ,main] start
Thread[Thread- 1 , 5 ,main] start
Thread[Thread- 1 , 5 ,main]init DeadLoopClass (之后sleep 10s)
Thread[Thread- 1 , 5 ,main] run over
Thread[Thread- 0 , 5 ,main] run over

虚拟机规范严格规定了有且只有5中情况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 1.遇到new,getstatic,putstatic,invokestatic这失调字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 5.当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

开篇已经举了一个范例:通过子类引用赋了的静态字段,不会导致子类初始化。
这里再举两个例子。
1. 通过数组定义来引用类,不会触发此类的初始化:(SuperClass类已在本文开篇定义)

1
2
3
4
5
6
7
public class NotInitialization
{
     public static void main(String[] args)
     {
         SuperClass[] sca = new SuperClass[ 10 ];
     }
}

运行结果:(无)
2. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConstClass
{
     static
     {
         System.out.println( "ConstClass init!" );
     }
     public static  final String HELLOWORLD = "hello world" ;
}
public class NotInitialization
{
     public static void main(String[] args)
     {
         System.out.println(ConstClass.HELLOWORLD);
     }
}

运行结果:hello world

 

特殊顺序

public class StaticTest
{
     public static void main(String[] args)
     {
         staticFunction();
     }
 
     static StaticTest st = new StaticTest();
 
     static
     {
         System.out.println( "1" );
     }
 
     {
         System.out.println( "2" );
     }
 
     StaticTest()
     {
         System.out.println( "3" );
         System.out.println( "a=" +a+ ",b=" +b);
     }
 
     public static void staticFunction(){
         System.out.println( "4" );
     }
 
     int a= 110 ;
     static int b = 112 ;
}

输出结果:

2 3 a=110,b=0 1 

主要的点之一:实例初始化不一定要在类初始化结束之后才开始初始化,类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载,只有在准备阶段和初始化阶段才会涉及类变量的初始化和赋值,因此只针对这两个阶段进行分析;

类的准备阶段需要做是为类变量分配内存并设置默认值,因此类变量st为null、b为0;(需要注意的是如果类变量是final,编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将变量设置为指定的值,如果这里这么定义:static final int b=112,那么在准备阶段b的值就是112,而不再是0了。) 
  类的初始化阶段需要做是执行类构造器(类构造器是编译器收集所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器,对象的构造方法是<init>(),类的构造方法是<clinit>(),可以在堆栈信息中看到),因此先执行第一条静态变量的赋值语句即st = new StaticTest (),此时会进行对象的初始化,对象的初始化是先初始化成员变量再执行构造方法,因此设置a为110->打印2->执行构造方法(打印3,此时a已经赋值为110,但是b只是设置了默认值0,并未完成赋值动作),等对象的初始化完成后继续执行之前的类构造器的语句,接下来就不详细说了,按照语句在源码中的顺序执行即可。 

这里面还牵涉到一个冷知识就是在嵌套初始化时有一个特别的逻辑特别是内嵌的这个变量恰好是个静态成员而且是本类的实例会导致一个有趣的现象:“实例初始化竟然出现在静态初始化之前”其实并没有提前,你要知道java记录初始化与否的时机。 

看一个简化的代码,把关键问题解释清楚:

public class Test {
    public static void main(String[] args) {
        func();
    }
    static Test st = new Test();
    static void func(){}
} 

根据上面的代码,有以下步骤:

  1. 首先在执行此段代码时,首先由main方法的调用触发静态初始化。
  2. 在初始化Test 类的静态部分时,遇到st这个成员。
  3. 但凑巧这个变量引用的是本类的实例。
  4. 那么问题来了,此时静态初始化过程还没完成就要初始化实例部分了。是这样么?
  5. 从人的角度是的。但从java的角度,一旦开始初始化静态部分,无论是否完成,后续都不会再重新触发静态初始化流程了。
  6. 因此在实例化st变量时,实际上是把实例初始化嵌入到了静态初始化流程中,并且在楼主的问题中,嵌入到了静态初始化的起始位置。这就导致了实例初始化完全至于静态初始化之前。这也是导致a有值b没值的原因。
  7. 最后再考虑到文本顺序,结果就显而易见了。

 

最终类的加载执行顺序:(正常流程)

访问 静态内容顺序:父类顺序static-->子类顺序static(从上到下走哪一级结束就在哪一级退出)

访问实例内容顺序:父类顺序static-->子类顺序static---》父类成员变量-->父类块-->父类构造方法-->子类成员变量-->子类块-->子类构造方法

 

 

javaWeb工程启动加载顺序:

 web.xml 的加载顺序是:[context-param -> listener -> filter -> servlet -> spring] ,而同类型节点之间的实际程序调用的时候的顺序是根据对应的 mapping 的顺序进行调用的

如配置多个filter或servlet 时web 容器启动时初始化每个 filter 时,是按照 filter 配置节出现的顺序来初始化的,当请求资源匹配多个 filter-mapping 时,filter 拦截资源是按照 filter-mapping 配置节出现的顺序来依次调用 doFilter() 方法的

一、启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<context-param>结点。
二、容创建一个ServletContext(servlet上下文),这个 web项目的所有部分都将共享这个上下文。 
三、容器将<context-param>转换为键值对,并交给 servletContext。 因为listener, filter 等在初始化时会用到这些上下文中的信息,所以要先加载。 
四、容器创建<listener>中的类实例,创建监听器。
五、加载filter和servlet 
load- on-startup 元素在web应用启动的时候指定了servlet被加载的顺序,它的值必须是一个整数。

如果它的值是一个负整数或是这个元素不存在,那么容器会在该servlet被调用的时候,加载这个servlet。如果值是正整数或零,容器在配置的时候就加载并初始化这个servlet,容器必须保证值小的先被加载。如果值相等,容器可以自动选择先加载谁

【加载Spring】

       比如filter 需要用到 bean ,但加载顺序是: 先加载filter 后加载spring,则filter中初始化操作中的bean为null;

       所以,如果过滤器中要使用到 bean,可以将spring 的加载 改成 Listener的方式 :

<listener>  
        <listener-class>  
             org.springframework.web.context.ContextLoaderListener   
        </listener-class>  
</listener>

要将一个Java类随web容器的启动而启动,需要如下几个步骤:

1:首先让你要自动运行的类继承javax.servlet.http.HttpServlet

2:把你要自动运行的类中写一个init方法。(servlet应用程序启动的入口就是init方法)

public static void init(){

            System.out.println("这样在web容器启动的时候,就会执行这句话了!");

}

3:在web.xml中配置该servlet,如下:

<servlet>
   <servlet-name>GenerateData</servlet-name>
   <servlet-class>com.yq.javaSCADA.business.impl.GenerateData</servlet-class>
   <load-on-startup>1</load-on-startup>
  </servlet>

4:启动的web服务器,tomcat,weblogic,jboss,就会自动运行你类中的init方法了

Spring使用实际问题:

现在新增一些要求,我的整个项目的框架是在spring框架下的,我在这个自动运行的类中的main方法中,主要是产生一些数据,然后将这些数据自动的插入到数据库中,插入数据的时候,需要调用spring容器中的dao相关的bean进行数据库的操作。

        如果我们直接在这个自动启动的类中,增加dao的属性,在spring配置文件中配置这个自动启动的类,并希望它能成功使用spring容器中的bean,那么结果只会让我们失望,因为spring容器相关的context(上下文并没有引入到这个自动类来),当然,我们可以按照Java应用程序启动spring容器的方法,在自动启动类里面再启动一次spring,操作如下:

方法一、

  1. public class BeanManager {  
  2.     private static ApplicationContext context = new ClassPathXmlApplicationContext("appcontext.xml") ;  
  3.   
  4.     public static Object getBean(String beanId) {  
  5.         return context.getBean(beanId);  
  6.     }  

在web.xml中写一个servlet,自动启动,init方法中调用一下BeanManager,为的是在Web应用启动的时候就让Spring加载bean配置文件,否则会在第一次调用BeanManager的时候加载,影响第一次访问速度

  1.   init()  throws ServletException {  
  2.   
  3.        BeanManager bm = new BeanManager();  
  4.   
  5. }

在java代码中使用 BeanManager.getBean(String beanId); 来获得bean实例其实spring也给我们提供了另外两种方法!

方法二、只需要让这个随web容器启动的类实现org.springframework.beans.factory.BeanFactoryAware接口,具体例子如下:

public class GenerateData extends HttpServlet implements BeanFactoryAware{
 
         private static BeanFactory beanFactory ;
        @Override
        public void setBeanFactory(BeanFactory beanfactory) throws BeansException {
                this.beanFactory=beanfactory;
         }

        //这里的insertDataDao,就是spring管理的bean,我们只需取出来用就可以,不用做第二次启动spring容器

        InsertDataDao insertDataDao=beanFactory.getbean("insertDataDao");

        User user=new User("username",“age”,"sex");

        insertDataDao.insert(user);

}

方法三、spring本身还提供了不是用继承HttpServlet的初始化一个类的方式:

让该类实现org.springframework.beans.factory.InitializingBean;和org.springframework.web.context.ServletContextAware;两个接口就可以了。然后将需要随web容器启动的代码写到ServletContextAware接口提供的setServletContext方法即可,例子如下:

public class SpringInitMethodTest implements InitializingBean,ServletContextAware{
 @Override
 public void afterPropertiesSet() throws Exception {
  
 }

 @Override
 public void setServletContext(ServletContext arg0) {
         System.out.println("spring初始化方法成功,oh,yeah");
 }

}

Web容器处理请求的过程:

 当Web容器接收到来自客户端的请求信息之后,会根据URL中的Web元件地址信息到Servlet 队列中查找对应的Servlet对象,如果找到则直接使用,如果没有找到则加载对应的类,并创建对象。

也就是说,Servlet对象是在第一次被使用的时 候才创建的,并且一旦创建就会被反复使用,不再创建新的对象。所有创建出的Servlet对象会在Web服务器停止运行的时候统一进行垃圾回收。

猜你喜欢

转载自2277259257.iteye.com/blog/2328947