J2EE学习笔记

Posted on

J2EE学习笔记

J2EE学习笔记

注:框架可以用Word菜单中的 “视图/文档结构图” 看到
J2EE模式 Value Object(值对象) 用于把数据从某个对象/层传递到其他对象/层的任意Java对象。 通常不包含任何业务方法。 也许设计有公共属性,或者提供可以获取属性值的get方法。 JSP 1.JSP的基础知识

           __  
    _____ |   directive  (指令)
        |     |-- scripting (脚本)

JSP -------| | action (动作) | |_Template data :除JSP语法外,JSP引擎不能解读的东西

1)在JSP中使用的directive(指令)主要有三个: a) page指令 b) include指令 c) taglib指令 在JSP的任何地方,以任何顺序,一个页面可以包含任意数量的page指令 2)Scripting(脚本)包括三种类型 a) <%!declaraction %>; b) <% scriptlet %>; c) <%= expression %>; 3)action(动作) 标准的动作类型有: a) ; b) ; d) ; e) ; f) ; g) ; h) ;

  1. 注释: <% -----jsp comment-------%>;
    <! -----html comment-------%>;
    
  2. <%@ page session = “true” import =”java.util./*” %>; session可以不赋值,默认为true,如果session=”false”,则在JSP页面中,隐含的变量session就不能使用。
  3. 请求控制器结构(Request Controller) 也被称之为JSP Model 2 Architecture 这种途径涉及到使用一个Servlet或一个JSP作为一个应用程序或一组页面的入口点。 为创建可维护的JSP系统,Request Controller是最有用的方式之一。 不是JSP,而是Java类才是放置控制逻辑的正确的地方。 请求控制器的命名模式为: xxxController.jsp 请求控制器类的命名模式为: xxxRequestController 2.JSP中的JavaBean JSP三种bean的类型 1) 页面bean 2) 会话bean 3) 应用bean 大多数的系统会使用一个会话bean来保持状态,而对每一个页面使用一个页面bean 来对复杂的数据进行表示。 页面bean是一个模型,而JSP是一个视图。 3.Custom tag bean是信息的携带者, 而tag更适用于处理信息。 标记库包含一个标记库描述符(TLD)和用于实现Custom tag的Java类 在翻译阶段,JSP容器将使用TLD来验证页面中的所有的tag是否都被正确的使用。 标记处理程序只是一个简单的适配器,而真正的逻辑是在另一个类中实现的,标记处理程序只是提供了一个供其他的可复用的类的JSP接口

Servlet 1.ServletConfig &/#61548; 一个ServletConfig对象是servlet container在servlet initialization的时候传递给servlet的。 ServletConfig包涵 ServletContext 和 一些 Name/Value pair (来自于deployment descriptor) &/#61548; ServletContext接口封装了Web应用程序的上下文概念。 2.会话跟踪 1) Session &/#61548; 当一个Client请求多个Servlets时,一个session可以被多个servlet共享。 &/#61548; 通常情况下,如果server detect到browser支持cookie,那么URL就不会重写。 2) cookie &/#61548; 在Java Servlet中,如果你光 Cookie cookie = new Cookie(name,value) 那么当用户退出Browser时,cookie会被删除掉,而不会被存储在客户端的硬盘上。 如果要存储 cookie,需加一句 cookie.setMaxAge(200) &/#61548; cookie是跟某一个server相关的,运行在同一个server上的servlet共享一个cookie. 3) URL Rewriting 在使用URL Rewriting来维护Session ID的时候,每一次HTTP请求都需要EncodeURL() 典型的用在两个地方 1) out.print(“form action=\” ”); out.print(response.encodeURL(“sessionExample”)); out.print(“form action=\” ”); out.print(“method = GET>;”); 2) out.print(“

;;URL encoded ;”); 3.SingleThreadModel 默认的,每一个servlet definition in a container只有一个servlet class的实例。 只有实现了SingleThreadModel,container才会让servlet有多个实例。 Servlet specification上建议,不要使用synchronized,而使用SingleThreadModel。 SingleThreadModel(没有方法) 保证servlet在同一时刻只处理一个客户的请求。 SingleThreadModel是耗费资源的,特别是当有大量的请求发送给Servlet时,SingleThreadModel的作用是使包容器以同步时钟的方式调用service方法。 这等同于在servlet的service()方法种使用synchronized. Single Thread Model一般使用在需要响应一个heavy request的时候,比如是一个需要和数据库打交道的连接。

  1. 在重载Servlet地init( )方法后,一定要记得调用super.init( );
  2. the client通过发送一个blank line表示它已经结束request 而the server通过关闭the socket来表示response已结束了。
  3. 一个Http Servlet可以送三种东西给Client 1) a single status code 2) any number of http headers 3) a response body
  4. Servlet之间信息共享的一个最简单的方法就是 System.getProperties().put(“key”,”value”);
  5. Post和Get Post:将form内各字段名称和内容放置在html header内传送给server Get: ?之后的查询字符串要使用URLEncode,经过URLEncode后,这个字符串不再带有空格,以后将在server上恢复所带有的空格。

Get是Web上最经常使用的一种请求方法,每个超链接都使用这种方法。

  1. Web.xml就是Web Applicatin 的deployment descriptor 作用有:组织各类元素

     设置init param
     设置安全性
    
  2. Request Dispatcher用来把接收到的request forward processing到另一个servlet 要在一个response里包含另一个servlet的output时,也要用到Request Dispatcher.

  3. Servlet和Jsp在同一个JVM中,可以通过ServeltContext的 setAttribute( ) getAttribute( ) removeAttribute( ) 来共享对象
  4. 利用request.getParameter( )得到的String存在字符集问题。 可以用 strTitle = request.getParameter(“title”);
    strTitle = new String(strTitle.getBytes(“8859-1”),”gb2312”);
    
    如果你希望得到更大得兼容性
    String encoding = response.getCharacterEncoding();     
    
    //确定Application server用什么编码来读取输入的。
    strTitle = new String(strTitle.getBytes(encoding),”gb2312”);
    
    XML 1.XML基础知识
  5. 一个xml文档可以分成两个基本部分: 首部( header ) 内容( content )
  6. xml名字空间规范中指定: xml文档中的每一个元素都处在一个名字空间中;如果没有指定的名字空间,缺省的名字空间就是和该元素相关联的名字空间。
  7. A document that is well-formed obeys all of the rules of XML documents (nested tags, etc.) " If a well-formed document uses a Document Type Definition (more on these in a minute), and it follows all the rules of the DTD, then it is also a valid document
  8. A tag is the text between the ; " An element is the start tag, the end tag,and everything (including other elements) in between
  9. 标签( tags ) 实际上包含了“元素”( elements ) 和 “属性”( attributes )两部分。 用元素( elements )来描述有规律的数据。 用属性( attributes ) 来描述系统数据。 如果你有一些数据要提供给某个应用程序,该数据就可能要用到一个元素。 如果该数据用于分类,或者用于告知应用程序如何处理某部分数据,或者该数据从来没有直接对客户程序公开,那么它就可能成为一种属性。
  10. CDATA (读作:C data ) C是character的缩写。
  11. org.xml.sax.Reader /|\ org.xm.l.sax.XMLReader /|\ org.apche.xerces.parsers.SAXParser 2.WebService 2.1 WebService的基本概念 WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独立的通讯技术。 这种技术允许网络上的所有系统进行交互。随着技术的发展,一个Web服务可以包含额外的指定功能并且可以在多个B2B应用中协作通讯。 Web服务可以理解请求中上下文的关系,并且在每一个特定的情况下产生动态的结果。这些服务会根据用户的身份,地点以及产生请求的原因来改变不同的处理,用以产生一个唯一的,定制的方案。这种协作机制对那些只对最终结果有兴趣的用户来说,是完全透明的。 UDDI 在用户能够调用Web服务之前,必须确定这个服务内包含哪些商务方法,找到被调用的接口定义,还要在服务端来编制软件。所以,我们需要一种方法来发布我们的Web服务。 UDDI (Universal Description, Discovery, and Integration) 是一个主要针对Web服务供应商和使用者的新项目。UDDI 项目中的成员可以通过UDDI Business Registry (UBR) 来操作Web服务的调用,UBR是一个全球性的服务。 Web服务供应商可以在UBR中描述并且注册他们的服务。 用户可以在UBR中查找并定位那些他们需要的服务。 UDDI是一种根据描述文档来引导系统查找相应服务的机制。 UDDI包含标准的“白皮书”类型的商业查询方式, “黄皮书”类型的局部查找,以及 “绿皮书”类型的服务类型查找。 UDDI利用SOAP消息机制(标准的XML/HTTP)来发布,编辑,浏览以及查找注册信息。它采用XML格式来封装各种不同类型的数据,并且发送到注册中心或者由注册中心来返回需要的数据。 WSDL 对于商业用户来说,要找到一个自己需要使用的服务,他必须知道如何来调用。 WSDL (Web Services Description Language) 规范是一个描述接口,语义以及Web服务为了响应请求需要经常处理的工作的XML文档。这将使简单地服务方便,快速地被描述和记录。 以下是一个WSDL的样例: <?xml version="1.0"?>; <definitions name="StockQuote"
        targetNamespace="http://example.com/stockquote.wsdl"
        xmlns:tns="http://example.com/stockquote.wsdl"
        xmlns:xsd1="http://example.com/stockquote.xsd"
        xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
        xmlns="http://schemas.xmlsoap.org/wsdl/">;
    
    ; <schema targetNamespace=http://example.com/stockquote.xsd
         xmlns="http://www.w3.org/2000/10/XMLSchema">;
    
    ; ; ;
        <element name="tickerSymbol" type="string"/>;
      </all>;
    
    ;
    ; ;
    <complexType>;
        <all>;
           <element name="price" type="float"/>;
         </all>;
      </complexType>;
    
    ; ;
    ; ; ; ; ; ; ; ; ;
    <input message="tns:GetLastTradePriceInput"/>;
    <output message="tns:GetLastTradePriceOutput"/>;
    
    ;
    ; <binding name="StockQuoteSoapBinding"
            type="tns:StockQuotePortType">;
    
    <soap:binding style="document"
                         transport="http://schemas.xmlsoap.org/soap/http"/>;
    
    ;
    <soap:operation
                   soapAction="http://example.com/GetLastTradePrice"/>;
    <input>;
       <soap:body use="literal"/>;
    </input>;
    <output>;
        <soap:body use="literal"/>;
    </output>;
    
    ; ; ; ;My first service; ;
    <soap:address location="http://example.com/stockquote"/>;
    
    ;
    ; ; 它包含了以下的关键信息: 消息的描述和格式定义可以通过XML文档中的;和; 标记来传送。 ; 标记中表示了消息传送机制。 (e.g. request-only, request-response, response-only) 。 ; 标记指定了编码的规范 。 ; 标记中表示服务所处的位置 (URL)。 WSDL在UDDI中总是作为一个接口描述文档。因为UDDI是一个通用的用来注册WSDL规范的地方,UDDI的规范并不限制任何类型或者格式描述文档。这些文档可能是一个WSDL文档,或者是一个正规的包含导向文档的Web页面,也可能只是一个包含联系信息的电子邮件地址。 现在Java提供了一个 Java API for WSDL (JWSDL)规范。它提供了一套能快速处理WSDL文档的方法,并且不用直接对XML文档进行操作,它会比JAXP更方便,更快速。 SOAP 当商业用户通过UDDI找到你的WSDL描述文档后,他通过可以Simple Object Access Protocol (SOAP) 调用你建立的Web服务中的一个或多个操作。 SOAP是XML文档形式的调用商业方法的规范,它可以支持不同的底层接口,象HTTP(S)或者SMTP。 之所以使用XML是因为它的独立于编程语言,良好的可扩展性以及强大的工业支持。之所以使用HTTP是因为几乎所有的网络系统都可以用这种协议来通信,由于它是一种简单协议,所以可以与任何系统结合,还有一个原因就是它可以利用80端口来穿越过防火墙。 SOAP的强大是因为它简单。SOAP是一种轻量级的,非常容易理解的技术,并且很容易实现。它有工业支持,可以从各主要的电子商务平台供应商那里获得。 从技术角度来看,SOAP详细指明了如何响应不同的请求以及如何对参数编码。一个SOAP封装了可选的头信息和正文,并且通常使用HTTP POST方法来传送到一个HTTP 服务器,当然其他方法也是可以的,例如SMTP。SOAP同时支持消息传送和远程过程调用。以下是一个SOAP请求。 POST /StockQuote HTTP/1.1 Host: www.stockquoteserver.com Content-Type: text/xml; charset="utf-8" Content-Length: nnnn SOAPAction: "Some-URI" SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/; ; ; 5 ; ; ; ;
    <symbol>;SUNW</symbol>;
    
    ;
    ; ; JAXR 为了支持UDDI在Java平台上的功能,Java APIs for XML Registries (JAXR)允许开发者来访问注册中心。 值得注意的是,JAXR并不是建立Web服务必需的,你可以利用其他常用的XML APIs来直接集成这些协议。 JAXR是一个方便的API,它提供了Java API来发布,查找以及编辑那些注册信息。它的重点在于基于XML的B2B应用,复杂的地址本查找以及对XML消息订阅的支持等Web服务。 它也可以用来访问其他类型的注册中心,象ebXML注册中心。 这些对Web服务的注册信息进行的操作,可以使用当前的一些Web服务工具来完成(例如第三方的SOAP和ebXML消息工具)。另外,当JAXP提供了一致并具有针对性的API来完成这些操作,这将使开发变得更加容易。 JAX/RPC 为了使开发人员专注于建立象SOAP那样的基于XML的请求,JCP正在开发基于RPC (JAX/RPC) 的Java API。JAX/RPC是用来发送和接收方法调用请求的,它基于XML协议,象SOAP,或者其他的象XMLP (XML Protocol,要了解更多可以参考http://www.w3.org/2000/xp/)。JAX/RPC使你不用再关注这些协议的规范,使应用的开发更快速。不久,开发人员就不用直接以XML表示方法调用了。 目前有很多第三方实现了SOAP,开发人员可以在不同的层次上调用SOAP,并选择使用哪一种。将来,JAX/RPC会取代这些APIs并提供一个统一的接口来构造以及处理SOAP RPC请求。 在接收一个从商业伙伴那里过来的SOAP请求的时候,一个Java servlet用JAX/RPC来接收这个基于XML的请求。一旦接收到请求后,servlet会调用商务方法,并且把结果回复给商业伙伴。 JAXM 当从商业合作伙伴那里接收一个Web服务的请求时,我们需要Java API实现一个Servlet来处理ebXML消息,就象我们用JAX/RPC来处理SOAP请求一样。 Java API for XML Messaging (JAXM) 是集成XML消息标准(象ebXML消息或者SOAP消息)的规范。 这个API是用来推动XML消息处理的,它检测那些预定单的消息格式以及约束。它控制了所有的消息封装机制,用一种直观的方式分割了消息中的信息,象路由信息,发货单。这样,开发人员只要关注消息的有效负载,而不用去担心那些消息的重复处理。 目前的开发人员用JAXP来实现JAXM将要提供的功能,JAXM将会提供一套非常具有针对性的API来处理基于XML的消息传送。这将大大简化开发人员的代码,并使它们具有统一的接口。 JAXM和JAX/RPC的差别在于处理消息导向的中间件以及远程过程调用的不同。JAXM注重于消息导向,而JAX/RPC是用来完成远程过程调用的。以下是图解。

请注意,在JAXM 和 JAX/RPC技术成熟之前,开发人员还是依赖于第三方的SOAP APIs,象Apache SOAP, IdooXOAP, 以及 GLUE。当JAXM 和 JAX/RPC正式发布后,它将为当前不同的SOAP和ebXML消息提供统一的接口。就象JDBC位多种不同的数据库提供统一的接口。 JAXB XML绑定技术可以把XML文档和Java对象进行自由转换。 用JAXB,你可以在后台的EJB层,把XML文档转换成Java对象。同样你也可以把从EJB中取出的Java对象转换成XML文档返回给用户。 JAXB接口提供了比SAX和DOM更高级的方法来处理XML文档。它提供的特性可以在XML数据和Java类之间互相映射,提供了一个简单的方法来转换XML数据。它比逐个解析标记更简单。 2.2 建立WeService的步骤 在建立WeService的时候,有三个主要步骤: 1.建立客户端联接 为了允许Applets,Applications,商业合作伙伴,浏览器和PDAs 使用Web服务。 2.实现Web服务 包括工作流,数据传送,商业逻辑以及数据访问。这些功能是隐藏在Web服务后,并且为客户端工作的。 3.联接后台系统 这个系统可能包括一个或多个数据库,现存的企业信息系统,商业合作伙伴自己的系统或者Web服务,以及在多个系统中共享的数据。 基于J2EE的Web服务的核心构架:

RMI

  1. RMI-IIOP
  2. RMI 是在java中使用remote method invocation的最初的方法,RMI使用java.rmi包 RMI-IIOP 是RMI的一个特殊版本,RMI-IIOP可以和CORBA兼容,RMI-IIOP使用java.rmi包和javax.rmi JAF(Java活动构架) 开发者可以使用JAF来决定任意一块数据的类型、封装对数据的访问、寻找合适的操作、实例化相关的bean来执行这些操作等。 例如,JavaMail就是使用JAF根据MIME类型来决定实例化那一个对象。 EJB
  3. EJB组件实现代码的限制 EJB组件的约束 EJB的开发者并不需要在EJB的组件实现代码中编写系统级的服务,EJB提供商/开发 者需知道并且严格地遵守一些限制,这些限制与开发稳定的和可移植的EJB组件的利益有 关。 以下是你应该回避使用的一些Java特色,并且在你的EJB组件的实现代码中要严格限 制它们的使用: 1.使用static,非final 字段。建议你在EJB组件中把所有的static字段都声明为final型的。这样可以保证前后一致的运行期语义,使得EJB容器有可以在多个Java虚拟机之间分发组件实例的灵活性。 2.使用线程同步原语来同步多个组件实例的运行。避免这个问题,你就可以使EJB容器灵活的在多个Java虚拟机之间分发组件实例。 3.使用AWT函数完成键盘的输入和显示输出。约束它的原因是服务器方的商业组件意味着提供商业功能而不包括用户界面和键盘的I/O功能。 4.使用文件访问/java.io 操作。EJB商业组件意味着使用资源管理器如JDBC来存储和检索数据而不是使用文件系统API。同时,部署工具提供了在部署描述器(descriptor)中存储环境实体,以至于EJB组件可以通过环境命名上下文用一种标准的方法进行环境实体查询。所以,使用文件系统的需求基本上是被排除了。 5.监听和接收socket连接,或者用socket进行多路发送。EJB组件并不意味着提供网络socket服务器功能,但是,这个体系结构使得EJB组件可以作为socket客户或是RMI客户并且可以和容器所管理的环境外面的代码进行通讯。 6.使用映象API查询EJB组件由于安全规则所不能访问的类。这个约束加强了Java平台的安全性。 7.欲创建或获得一个类的加载器,设置或创建一个新的安全管理器,停止Java虚拟机,改变输入、输出和出错流。这个约束加强了安全性同时保留了EJB容器管理运行环境的能力。 8.设置socket工厂被URL's ServerSocket,Socket和Stream handler使用。避免这个特点,可以加强安全性同时保留了EJB容器管理运行环境的能力。 9.使用任何方法启动、停止和管理线程。这个约束消除了与EJB容器管理死锁、线程 和并发问题的责任相冲突的可能性。 通过限制使用10-16几个特点,你的目标是堵上一个潜在的安全漏洞: 10.直接读写文件描述符。 11.为一段特定的代码获得安全策略信息。 12.加载原始的类库。 13.访问Java一般角色所不能访问的包和类。 14.在包中定义一个类。 15.访问或修改安全配置对象(策略、安全、提供者、签名者和实体)。 16.使用Java序列化特点中的细分类和对象替代。 17.传递this引用指针作为一个参数或者作为返回值返回this引用指针。你必须使用 SessionContext或EntityContext中的getEJBObject()的结果。 Java2平台的安全策略 以上所列的特点事实上正是Java编程语言和Java2标准版中的标准的、强有力的特色。EJB容器允许从J2SE中使用一些或全部的受限制的特色,尽管对于EJB组件是不可用的,但需通过J2SE的安全机制来使用而不是通过直接使用J2SE的API。 Java2平台为EJB1.1规范中的EJB容器所制定的安全策略定义了安全许可集,这些许可在EJB组件的编程限制中出现。通过这个策略,定义了一些许可诸如:java.io.FilePermission,java.net.NetPermission,java.io.reflect.ReflectPermission,java.lang.security.SecurityPermission,以便加强先前所列出的编程限制。 许多EJB容器没有加强这些限制,他们希望EJB组件开发者能遵守这些编程限制或者是带有冒险想法违背了这些限制。违背这些限制的EJB组件,比标准方法依赖过多或过少的安全许可,都将很少能在多个EJB容器间移植。另外,代码中都将隐藏着一些不确定的、难以预测的问题。所有这些都足以使EJB组件开发者应该知道这些编程限制,同时也应该认真地遵守它们。 任何违背了这些编程限制的EJB组件的实现代码在编译时都不能检查出来,因为这些特点都是Java语言和J2SE中不可缺少的部分。 对于EJB组件的这些限制同样适用于EJB组件所使用的帮助/访问(helper/access)类,J2EE应用程序使用Java文档(jar)文件格式打包到一个带.ear(代表Enterprise Archive)扩展名的文件中,这个ear文件对于发送给文件部署器来说是标准的格式。ear文件中包括在一个或多个ejb-jar文件中的EJB组件,还可能有ejb-jar所依赖的库文件。所有ear文件中的代码都是经过深思熟虑开发的应用程序并且都遵守编程限制和访问许可集。 未来版本的规范可能会指定通过部署工具来定制安全许可的能力,通过这种方法指定了一个合法的组件应授予的许可权限,也指定了一个标准方法的需求:如从文件系统中读文件应有哪些要求。一些EJB容器/服务器目前在它们的部署工具中都提供了比标准权限或多或少的许可权限,这些并不是EJB1.1规范中所需要的。 理解这些约束 EJB容器是EJB组件生存和执行的运行期环境,EJB容器为EJB组件实例提供了一些服务如:事务管理、安全持久化、资源访问、客户端连接。EJB容器也负责EJB组件实例整个生命期的管理、扩展问题以及并发处理。所以,EJB组件就这样寄居在一个被管理的执行环境中--即EJB容器。

    因为EJB容器完全负责EJB组件的生命期、并发处理、资源访问、安全等等,所以与容器本身的锁定和并发管理相冲突的可能性就需要消除,许多限制都需要使用来填上潜在的安全漏洞。除了与EJB容器责任与安全冲突的问题,EJB组件还意味着仅仅聚焦于商务逻辑,它依赖于EJB容器所提供的服务而不是自己来直接解决底层的系统层的问题。 可能的问题 通常,EJB组件在容器之间的移植不可避免地与如下问题相关: 1.它需要依靠的受限制的特点在特定EJB容器中没有得到加强。 2.它需要依靠的非标准的服务从容器中可获得。 为了保证EJB组件的可移植性和一致的行为,你应该使用一个具有与Java2平台安全 策略集相一致的策略集的容器来测试EJB组件,并且其加强了前述的编程限制。 总结 EJB组件开发者应该知道这些推荐的关于EJB组件的编程限制,明白它们的重要性,并且从组件的稳定性和可移植性利益方面考虑来遵循它们。因为这些编程限制能阻止你使用标准的Java语言的特点,违背了这些编程限制在编译时不会知道,并且加强这些限制也不是EJB容器的责任。所有这些原因都使你应很小心地遵守这些编程限制,这些限制在组件的合同中已经成为了一个条款,并且它们对于建造可靠的、可移植的组件是非常重要的。

  4. 优化EJB entity bean为在应用程序和设计中描述持久化商业对象(persistent business objec ts)提供了一个清晰的模型。在java对象模型中,简单对象通常都是以一种简单的方式进行处理但是,很多商业对象所需要的事务化的持久性管理没有得到实现。entity bean将持久化机制封装在容器提供的服务里,并且隐藏了所有的复杂性。entity bean允许应用程序操纵他们就像处理一个一般的java对象应用。除了从调用代码中隐藏持久化的形式和机制外,entity bean还允许EJB容器对对象的持久化进行优化,保证数据存储具有开放性,灵活性,以及可部署性。在一些基于EJB技术的项目中,广泛的使用OO技术导致了对entity bean的大量使用,SUN的工程师们已经积累了很多使用entity Bean的经验,这篇文章就详细阐述的这些卡发经验: /探索各种优化方法 /提供性能优化和提高适用性的法则和建议 /*讨论如何避免一些教训。 法则1:只要可以,尽量使用CMP CMP方式不仅减少了编码的工作量,而且在Container中以及container产生的数据库访问代码中包括了许多优化的可能。Container可以访问内存缓冲中的bean,这就允许它可以监视缓冲中的任何变化。这样的话就在事物没有提交之前,如果缓存的数据没有变化就不用写到数据库中。就可以避免许多不必要的数据库写操作。另外一个优化是在调用find方法的时候。通常情况下find方法需要进行以下数据库操作: 查找数据库中的纪录并且获得主键 将纪录数据装入缓存 CMP允许将这两步操作优化为一步就可以搞定。[具体怎么做我也没弄明白,原文没有具体阐述] 法则2:写代码时尽量保证对BMP和CMP都支持 许多情况下,EJB的开发者可能无法控制他们写的bean怎么样被部署,以及使用的container是不是支持CMP. 一个有效的解决方案是,将商业逻辑的编码完全和持久化机制分离。再CMP类中实现商业逻辑,然后再编写一个BMP类,用该类继承CMP类。这样的话,所有的商业逻辑都在CMP类中,而持久化机制在BMP中实现。[我觉得这种情况在实际工作中很少遇到,但是作者解决问题的思路值得学习] 法则3:把ejbStore中的数据库访问减小到最少。 如果使用BMP,设置一个缓存数据改变标志dirty非常有用。所有改变数据库中底层数据的操作,都要设置dirty,而在ejbStore()中,首先检测dirty的值,如果dirty的值没有改变,表明目前数据库中的数据与缓存的一致,就不必进行数据库操作了,反之,就要把缓存数据写入数据库。 法则4:总是将从lookup和find中获得的引用进行缓存。(cache) 引用缓存对session bean和entity bean 都是适用的。 通过JNDI lookup获得EJB资源。比如DataSource,bean的引用等等都要付出相当大的代价。因此应该避免多余的lookup.可以这样做: 将这些引用定义为实例变量。 从setEntityContext(session Bean使用setSessionContext)方法查找他们。SetEntityContext方法对于一个bean实例只执行一次,所有的相关引用都在这一次中进行查找,这样查找的代价就不是那么昂贵了。应该避免在其他方法中查找引用。尤其是访问数据库的方法:ejbLoad()和ejbStore(),如果在这些频繁调用的方法中进行DataSource的查找,势必造成时间的浪费。 调用其他entity bean的finder方法也是一种重量级的调用。多次调用finder()方法的代价非常高。如果这种引用不适合放在setEntityContext这样的初始化时执行的方法中执行,就应该在适当的时候缓存finder的执行结果。只是要注意的是,如果这个引用只对当前的entity有效,你就需要在bean从缓冲池中取出来代表另外一个实体时清除掉这些引用。,这些操作应该在ejbActivate()中进行。 法则5:总是使用prepare statements 这条优化法则适用于所有访问关系数据库的操作。 数据库在处理每一个SQL Statement的时候,执行前都要对Statement进行编译。一些数据库具有缓存statement和statement的编译后形式的功能。数据库可以把新的Statement和缓存中的进行匹配。然而,如果要使用这一优化特性,新的Statement要必须和缓存中的Statement完全匹配。 对于Non-prepared Statement,数据和Statement本身作为一个字符串传递,这样由于前后调用的数据不同而不能匹配,就导致无法使用这种优化。而对于prepared Statement,数据和Statement是分开传递给数据库的,这样Statement就可以和cache中已编译的Statement进行匹配。Statement就不必每次都进行编译操作。从而使用该优化属性。 这项技术在一些小型的数据库访问中能够减少Statement将近90%的执行时间。 法则6:完全关闭所有的Statement 在编写BMP的数据库访问代码时,记住一定要在数据库访问调用之后关闭Statement,因为每个打开的Statement对应于数据库中的一个打开的游标。 Security 1.加密 对称加密 (1)分组密码 (2)流密码 常用的对称加密算法: DES和TripleDES Blowfish RC4 AES 非对称加密 常用的非对称加密算法 RSA ElGamal 会话密钥加密(对称加密和非对称加密一起使用) 常用的会话密钥加密协议 S/MIME PGP SSL和TLS SSL是在Application level protocal和Transport protocal之间的。 比如:Http和TCP/IP之间 SSL 提供了服务器端认证和可选的客户端认证,保密性和数据完整性。 提供基于SSL方式的传输加密和认证,确保以下三种安全防护: 数据的机密性和准确性、 服务器端认证 客户端认证。 客户端认证比服务器端认证不很普遍的原因是每一个要被认证的客户都必须有一张Verisign这样的CA签发的证书。 通常,在进行身份认证的时候,应当只接受一个CA,这个CA的名字包含在客户证书中。 由于不可能随意创建一个由指定CA签发的证书,所以这可以有效的防御通过伪造证书来进行的攻击尝试。 2.认证(Authentication) 认证就是确定一条消息或一个用户的可靠性的过程。 1.消息摘要 MD5 SHA和SHA-1 2.消息认证码(Message Authientication Codes,MAC) 3.数字签名 用户可以用自己的密钥对信息加以处理,由于密钥仅为本人所有,这样就产生了别人无法生成的文件,也就形成了数字签名 数字签名可以 1)保证数据的完整性 2)验证用户的身份 数字签名采用一个人的私钥计算出来,然后用公钥去检验。
        hash算法                             私钥加密
    
    原报文 ――――――>;报文摘要( Message Digest ) ―――――>;数字签名 原报文和数字签名一起被发送到接受者那里,接受者用同样的hash算法得到报文摘要,然后用发送者的公钥解开数字签名。 比较是否相同,则可以确定报文确定来自发送者。 验证数字签名必须使用公钥,但是,除非你是通过安全的方式直接得到,否则不能保证公钥的正确性。(数字证书可以解决这个问题) 一个接受者在使用公钥(public key)检查数字签名(digital signature)的可信度时,通常先要检查收到的公钥(public key)是否可信的。 因此发送方不是单单地发送公钥(public key),而是发送一个包含公钥(public key)的数字证书(cetificate )。 4.数字证书 数字证书是一个经证书授权中心数字签名的包含公开密钥所有者信息以及公开密钥的文件。 数字证书Cetificate中包括: I. 用户的公钥(public key) II. 用户的一些信息,如姓名,email III. 发行机构的数字签名(digital signature), 用于保证证书的可信度 IV. 发行机构的一些信息 数字证书的格式遵循X.509国际标准。

注意:一个数字证书certificate并不适用于多种browser,甚至一种Browser的多个版本。 数字标识由公用密钥、私人密钥和数字签名三部分组成。 当在邮件中添加数字签名时,您就把数字签名和公用密钥加入到邮件中。数字签名和公用密钥统称为证书。您可以使用 Outlook Express 来指定他人向您发送加密邮件时所需使用的证书。这个证书可以不同于您的签名证书。 收件人可以使用您的数字签名来验证您的身份,并可使用公用密钥给您发送加密邮件,这些邮件必须用您的私人密钥才能阅读。 要发送加密邮件,您的通讯簿必须包含收件人的数字标识。这样,您就可以使用他们的公用密钥来加密邮件了。当收件人收到加密邮件后,用他们的私人密钥来对邮件进行解密才能阅读。 在能够发送带有数字签名的邮件之前,您必须获得数字标识。如果您正在发送加密邮件,您的通讯簿中必须包含每位收件人的数字标识。 数字证书,可以是个人证书或 Web 站点证书,用于将身份与"公开密钥"关联。只有证书的所有者才知道允许所有者"解密"或进行"数字签名"的相应"私人密钥"。当您将自己的证书发送给其他人时,实际上发给他们的是您的公开密钥,这样他们就可以向您发送只能由您使用私人密钥解密和读取的加密信息。  通过浏览器使用数字证书,必须先要设置浏览器软件 Internet Explorer 或 NetScape使用此证书,才能开始发送加密或需要数字签名的信息。访问安全的 Web 站点(以"https"打头的站点)时,该站点将自动向您发送他们的Web站点证书。 3.CA(证书授证中心) CA机构,又称为证书授证(Certificate Authority)中心,作为电子商务交易中受信任的第三方,承担公钥体系中公钥的合法性检验的责任。CA中心为每个使用公开密钥的用户发放一个数字证书,数字证书的作用是证明证书中列出的用户合法拥有证书中列出的公开密钥。CA机构的数字签名使得攻击者不能伪造和篡改证书。在SET交易中,CA不仅对持卡人、商户发放证书,还要对获款的银行、网关发放证书。它负责产生、分配并管理所有参与网上交易的个体所需的数字证书,因此是安全电子交易的核心环节。 对证书的信任基于对根证书的信任. 例如在申请SHECA的个人数字证书前,需要先下载根证书,然后再进行各类证书的申请。 下载根证书的目的: 网络服务器验证(S);安全电子邮件(E) 申请个人数字证书可以为Internet用户提供发送电子邮件的安全和访问需要安全连接(需要客户证书)的站点。 1)个人数字证书 a.个人身份证书
个人身份证书是用来表明和验证个人在网络上的身份的证书,它确保了网上交易和作业的安全性和可靠性。可应用于:网上炒股、网上理财、网上保险、网上缴费、网上购物、网上办公等等。个人身份证书可以存储在软盘或IC卡中。   b.个人安全电子邮件证书
个人安全电子邮件证书可以确保邮件的真实性和保密性。申请后一般是安装在用户的浏览器里。用户可以利用它来发送签名或加密的电子邮件。

用户在申请安装完安全安全电子邮件数字证书后,就可以对要发送的邮件进行数字签名。收信人收到该邮件后,就可以看到数字签名的标记,这样就可以证明邮件肯定来自发信者本人,而不是别人盗用该帐号伪造信件,同时也保证该邮件在传送过程中没被他人篡改过任何数据。 安全电子邮件中使用的数字证书可以实现: 保密性 通过使用收件人的数字证书对电子邮件加密。如此以来,只有收件人才能阅读加密的邮件,在Internet上传递的电子邮件信息不会被人窃取,即使发错邮件,收件人也无法看到邮件内容。 认证身份 在Internet上传递电子邮件的双方互相不能见面,所以必须有方法确定对方的身份。利用发件人数字证书在传送前对电子邮件进行数字签名即可确定发件人身份,而不是他人冒充的。 完整性 利用发件人数字证书在传送前对电子邮件进行数字签名不仅可确定发件人身份,而且传递的电子邮件信息也不能被人在传输过程中修改。 不可否认性 由于发件人的数字证书只有发件人唯一拥有,故发件人利用其数字证书在传送前对电子邮件进行数字签名,发件人就无法否认发过这个电子邮件。 OutLook Express中的个人安全电子邮件证书 签名邮件带有签名邮件图标。 签名邮件可能出现的任何问题都将在本信息之后可能出现的“安全警告”中得到描述。如果存在问题,您应该认为邮件已被篡改,或并非来自所谓的发件人。 当收到一封加密邮件时,您应该可以自信地认为邮件未被任何第三者读过。Outlook Express 会自动对电子邮件解密, 如果在您的计算机上装有正确的数字标识。

2)企业数字证书 a.企业身份证书 企业身份证书是用来表明和验证企业用户在网络上身份的证书,它确保了企业网上交易和作业的安全性和可靠性。可应用于:网上证券、网上办公、网上交税、网上采购、网上资金转帐、网上银行等。企业身份证书可以存储在软盘和IC卡中。    b.企业安全电子邮件证书
企业安全电子邮件证书可以确保邮件的真实性和保密性。申请后一般是安装在用户的浏览器里。企业可以利用它来发送签名或加密的电子邮件。 可使用 Windows 2000 中的证书服务来创建证书颁发机构 (CA),它负责接收证书申请、验证申请中的信息和申请者的身份、颁发证书、吊销证书以及发布证书吊销列表 (CRL)。 通常,当用户发出证书申请时,在其计算机上的加密服务提供程序 (CSP) 为用户生成公钥和私钥对。用户的公钥随同必要的识别信息发送至 CA。如果用户的识别信息符合批准申请的 CA 标准,那么 CA 将生成证书,该证书由客户应用程序检索并就地存储。 4.SET 安全接口层协议——SSL(Se cure SocketsLayer),并且已经几乎成为了目前WWW 世界的事实标准。这一标准使用公共密钥编码方案来对传输数据进行加密,在双方之间建立一个Internet 上的加密通道,从而使第三方无法获得其中的信息,其思路与目前流行的VPN方案大致相同,目的都是要保护数据不被未经授权的第三方所窃听,或即使窃听到也不知所云。但就象VPN 一样,SSL 在认证方面没有任何作为,它们都需要通过另外的手段来确认身份和建立双方彼此间的信任,然后再通过SSL 进行交易。 正是由于SSL 标准在认证方面的缺憾,所以SET 才有存在的必要。SET(Secure Electronic Transactions) 规范由Masterc ard 和Visa 公司于1996 年发布,专家们认为SET 是保证用户与商家在电子商务与在线交易中免受欺骗的重要手段。传统的信用卡交易者总在担心不诚实的店员会将自己的信用卡号码透露给他人,而在线交易也是如此,持卡者总在担心服务器端的管理员会将信用卡号码泄露出去,或者担心黑客会在管理员不知情的情况下盗取信用卡号码。事实上这些担心都是必要的,而SET 标准则可以保证用户的信用卡号码只传送给信用卡公司进行认证,不会被系统管理员看到,也不会留在交易服务器的硬盘上给黑客以可乘之机。 5.PKI PKI是一种易于管理的、集中化的网络安全方案。它可支持多种形式的数字认证: 数据加密、数字签字、不可否认、身份鉴别、密钥管理以及交叉认证等。PKI可通过一个基于认证的框架处理所有的数据加密和数字签字工作。P KI标准与协议的开发迄今已有15年的历史,目前的PKI已完全可以向企业网络提供有效的安全保障。 PKI是一种遵循标准的密钥管理平台,它能够为所有网络应用透明地提供采用加密和数字签名等密码服务所必需的密钥和证书管理。PKI必须具有 1)CA、 2)证书库、 3)密钥备份及恢复系统、 4)证书作废处理系统、 5)客户端证书处理系统 等基本成分,构建PKI也将围绕着这五大系统来构建 一个PKI由众多部件组成,这些部件共同完成两个主要功能: 1)为数据加密 2)创建数字认证。 服务器(即后端)产品是这一系统的核心,这些数据库管理着数字认证、公共密钥及专用密钥( 分别用于数据的加密和解密)。 CA数据库负责发布、废除和修改X.509数字认证信息,它装有用户的公共密钥、证书有效期以及认证功能(例如对数据的加密或对数字签字的验证) 。为了防止对数据签字的篡改,CA在把每一数字签字发送给发出请求的客户机之前,需对每一个数字签字进行认证。一旦数字认证得以创建, 它将会被自动存储于X.500目录中,X.500目录为树形结构。LDAP(Lightweight Directory Access Protocol)协议将响应那些要求提交所存储的公共密钥认证的请求。CA为每一用户或服务器生成两对独立的公共和专用密钥。其中一对用于信息的加密和解密, 另一对由客户机应用程序使用,用于文档或信息传输中数字签字的创建。 大多数PKI均支持证书分布,这是一个把已发布过的或续延生命期的证书加以存储的过程。这一过程使用了一个公共查询机制,X.500目录可自动完成这一存储过程。影响企业普遍接受P KI的一大障碍是不同CA之间的交叉认证。假设有两家公司,每一家企业分别使用来自不同供应商的CA,现在它们希望相互托管一段时间。如果其后援数据库支持交叉认证, 则这两家企业显然可以互相托管它们的CA,因而它们所托管的所有用户均可由两家企业的CA所托管。
* 认证机关
CA是证书的签发机构,它是PKI的核心。众所周知,构建密码服务系统的核心内容是如何实现密钥管理,公钥体制涉及到一对密钥,即私钥和公钥, 私钥只由持有者秘密掌握,无须在网上传送,而公钥是公开的,需要在网上传送,故公钥体制的密钥管理主要是公钥的管理问题,目前较好的解决方案是引进证书(certificate)机制。

证书是公开密钥体制的一种密钥管理媒介。它是一种权威性的电子文档,形同网络计算环境中的一种身份证,用于证明某一主体(如人、服务器等)的身份以及其公开密钥的合法性。在使用公钥体制的网络环境中, 必须向公钥的使用者证明公钥的真实合法性。因此,在公钥体制环境中,必须有一个可信的机构来对任何一个主体的公钥进行公证,证明主体的身份以及他与公钥的匹配关系。C A正是这样的机构,它的职责归纳起来有:  

1、验证并标识证书申请者的身份;
2、确保CA用于签名证书的非对称密钥的质量;
3、确保整个签证过程的安全性,确保签名私钥的安全性;
4、证书材料信息(包括公钥证书序列号、CA标识等)的管理;
5、确定并检查证书的有效期限;
6、确保证书主体标识的唯一性,防止重名;
7、发布并维护作废证书表;
8、对整个证书签发过程做日志记录;
9、向申请人发通知。

其中最为重要的是CA自己的一对密钥的管理,它必须确保其高度的机密性,防止他方伪造证书。CA的公钥在网上公开,整个网络系统必须保证完整性。  

* 证书库
证书库是证书的集中存放地,它与网上"白页”类似,是网上的一种公共信息库,用户可以从此处获得其他用户的证书和公钥。
构造证书库的最佳方法是采用支持LDAP协议的目录系统,用户或相关的应用通过LDAP来访问证书库。系统必须确保证书库的完整性,防止伪造、篡改证书。
* 密钥备份及恢复系统

* 证书作废处理系统

* PKI应用接口系统
PKI的价值在于使用户能够方便地使用加密、数字签名等安全服务,因此一个完整的PKI必须提供良好的应用接口系统,使得各种各样的应用能够以安全、一致、可信的方式与P KI交互,确保所建立起来的网络环境的可信性,同时降低管理维护成本。最后,PKI应用接口系统应该是跨平台的。

许多权威的认证方案供应商(例如VeriSign、Thawte以及GTE)目前都在提供外包的PKI。外包PKI最大的问题是,用户必须把企业托管给某一服务提供商, 即让出对网络安全的控制权。如果不愿这样做,则可建造一个专用的PKI。专用方案通常需把来自Entrust、Baltimore Technologies以及Xcert的多种服务器产品与来自主流应用程序供应商(如Microsoft、Netscape以及Qualcomm)的产品组合在一起。专用PK I还要求企业在准备其基础设施的过程中投入大量的财力与物力。 7.JAAS 扩展JAAS实现类实例级授权
“Java 认证和授权服务”(Java Authentication and Authorization Service,JAAS) 在 JAAS 下,可以给予用户或服务特定的许可权来执行 Java 类中的代码。在本文中,软件工程师 Carlos Fonseca 向您展示如何为企业扩展 JAAS 框架。向 JAAS 框架添加类实例级授权和特定关系使您能够构建更动态、更灵活并且伸缩性更好的企业应用程序。 大多数 Java 应用程序都需要某种类实例级的访问控制。例如,基于 Web 的、自我服务的拍卖应用程序的规范可能有下列要求: 任何已注册(经过认证)的用户都可以创建一个拍卖,但只有创建拍卖的用户才可以修改这个拍卖。 这意味着任何用户都可以执行被编写用来创建 Auction 类实例的代码,但只有拥有该实例的用户可以执行用来修改它的代码。通常情况下,创建 Auction 实例的用户就是所有者。这被称为类实例所有者关系(class instance owner relationship)。 该应用程序的另一个要求可能是: 任何用户都可以为拍卖创建一个投标,拍卖的所有者可以接受或拒绝任何投标。 再一次,任何用户都可以执行被编写用来创建 Bid 类实例的代码,但只有拥有该实例的用户会被授予修改该实例的许可权。而且,Auction 类实例的所有者必须能够修改相关的 Bid 类实例中的接受标志。这意味着在 Auction 实例和相应的 Bid 实例之间有一种被称为特定关系(special relationship)的关系。 不幸的是,“Java 认证和授权服务”(JAAS)— 它是 Java 2 平台的一部分 — 没有考虑到类实例级访问控制或者特定关系。在本文中,我们将扩展 JAAS 框架使其同时包含这两者。推动这种扩展的动力是允许我们将访问控制分离到一个通用的框架,该框架使用基于所有权和特定关系的策略。然后管理员可以在应用程序的生命周期内更改这些策略。 在深入到扩展 JAAS 框架之前,我们将重温一下 Java 2 平台的访问控制机制。我们将讨论策略文件和许可权的使用,并讨论 SecurityManager 和 AccessController 之间的关系。 Java 2 平台中的访问控制 在 Java 2 平台中,所有的代码,不管它是本地代码还是远程代码,都可以由策略来控制。策略(policy)由不同位置上的代码的一组许可权定义,或者由不同的签发者定义、或者由这两者定义。许可权允许对资源进行访问;它通过名称来定义,并且可能与某些操作关联在一起。 抽象类 java.security.Policy 被用于表示应用程序的安全性策略。缺省的实现由 sun.security.provider.PolicyFile 提供,在 sun.security.provider.PolicyFile 中,策略被定义在一个文件中。清单 1 是一个典型策略文件示例: 清单 1. 一个典型的策略文件 // Grant these permissions to code loaded from a sample.jar file // in the C drive and if it is signed by XYZ grant codebase "file:/C:/sample.jar", signedby "XYZ" { // Allow socket actions to any host using port 8080 permission java.net.SocketPermission "/:8080", "accept, connect, listen, resolve"; // Allows file access (read, write, execute, delete) in // the user's home directory. Permission java.io.FilePermission "${user.home}/-", "read, write, execute, delete"; }; SecurityManager 对 AccessController 在标准 JDK 分发版中,控制代码源访问的机制缺省情况下是关闭的。在 Java 2 平台以前,对代码源的访问都是由 SecurityManager 类管理的。SecurityManager 是由 java.security.manager 系统属性启动的,如下所示: java -Djava.security.manager 在 Java 2 平台中,可以将一个应用程序设置为使用 java.lang.SecurityManager 类或者 java.security.AccessController 类管理敏感的操作。AccessController 在 Java 2 平台中是新出现的。为便于向后兼容,SecurityManager 类仍然存在,但把自己的决定提交 AccessController 类裁决。SecurityManager 和 AccessController 都使用应用程序的策略文件确定是否允许一个被请求的操作。清单 2 显示了 AccessController 如何处理 SocketPermission 请求: 清单 2. 保护敏感操作 Public void someMethod() { Permission permission = new java.net.SocketPermission("localhost:8080", "connect"); AccessController.checkPermission(permission); // Sensitive code starts here Socket s = new Socket("localhost", 8080); } 在这个示例中,我们看到 AccessController 检查应用程序的当前策略实现。如果策略文件中定义的任何许可权暗示了被请求的许可权,该方法将只简单地返回;否则抛出一个 AccessControlException 异常。在这个示例中,检查实际上是多余的,因为缺省套接字实现的构造函数也执行相同的检查。 在下一部分,我们将更仔细地看一下 AccessController 如何与 java.security.Policy 实现共同合作安全地处理应用程序请求。 运行中的 AccessController AccessController 类典型的 checkPermission(Permission p) 方法调用可能会导致下面的一系列操作: AccessController 调用 java.security.Policy 类实现的 getPermissions(CodeSource codeSource) 方法。 getPermissions(CodeSource codeSource) 方法返回一个 PermissionCollection 类实例,这个类实例代表一个相同类型许可权的集合。 AccessController 调用 PermissionCollection 类的 implies(Permission p) 方法。 接下来,PermissionCollection 调用集合中包含的单个 Permission 对象的 implies(Permission p) 方法。如果集合中的当前许可权对象暗示指定的许可权,则这些方法返回 true,否则返回 false。 现在,让我们更详细地看一下这个访问控制序列中的重要元素。 PermissionCollection 类 大多数许可权类类型都有一个相应的 PermissionCollection 类。这样一个集合的实例可以通过调用 Permission 子类实现定义的 newPermissionCollection() 方法来创建。java.security.Policy 类实现的 getPermissions() 方法也可以返回 Permissions 类实例 — PermissionCollection 的一个子类。这个类代表由 PermissionCollection 组织的不同类型许可权对象的一个集合。Permissions 类的 implies(Permission p) 方法可以调用单个 PermissionCollection 类的 implies(Permission p) 方法。 CodeSource 和 ProtectionDomain 类 许可权组合与 CodeSource(被用于验证签码(signed code)的代码位置和证书)被封装在 ProtectionDomain 类中。有相同许可权和相同 CodeSource 的类实例被放在相同的域中。带有相同许可权,但不同 CodeSource 的类被放在不同的域中。一个类只可属于一个 ProtectionDomain。要为对象获取 ProtectionDomain,请使用 java.lang.Class 类中定义的 getProtectionDomain() 方法。 许可权 赋予 CodeSource 许可权并不一定意味着允许所暗示的操作。要使操作成功完成,调用栈中的每个类必须有必需的许可权。换句话说,如果您将 java.io.FilePermission 赋给类 B,而类 B 是由类 A 来调用,那么类 A 必须也有相同的许可权或者暗示 java.io.FilePermission 的许可权。 在另一方面,调用类可能需要临时许可权来完成另一个拥有那些许可权的类中的操作。例如,当从另一个位置加载的类访问本地文件系统时,我们可能不信任它。但是,本地加载的类被授予对某个目录的读许可权。这些类可以实现 PrivilegedAction 接口来给予调用类许可权以便完成指定的操作。调用栈的检查在遇到 PrivilegedAction 实例时停止,有效地将执行指定操作所必需的许可权授予所有的后继类调用。 使用 JAAS 顾名思义,JAAS 由两个主要组件组成:认证和授权。我们主要关注扩展 JAAS 的授权组件,但开始我们先简要概述一下 JAAS 认证,紧接着看一下一个简单的 JAAS 授权操作。 JAAS 中的用户认证 JAAS 通过添加基于 subject 的策略加强了 Java 2 中定义的访问控制安全性模型。许可权的授予不仅基于 CodeSource,还基于执行代码的用户。显然,要使这个模型生效,每个用户都必须经过认证。 JAAS 的认证机制建立在一组可插登录模块的基础上。JAAS 分发版包含几个 LoginModule 实现。LoginModules 可以用于提示用户输入用户标识和密码。LoginContext 类使用一个配置文件来确定使用哪个 LoginModule 对用户进行认证。这个配置可以通过系统属性 java.security.auth.login.config 指定。一个示例配置是: java -Djava.security.auth.login.config=login.conf 下面是一个登录配置文件的样子: Example { com.ibm.resource.security.auth.LoginModuleExample required debug=true userFile="users.xml" groupFile="groups.xml"; }; 认识您的主体 Subject 类被用于封装一个被认证实体(比如用户)的凭证。一个 Subject 可能拥有一个被称为主体(principal)的身份分组。例如,如果 Subject 是一个用户,用户的名字和相关的社会保险号可能是 Subject 的某些身份或主体。主体是与身份名关联在一起的。 Principal 实现类及其名称都是在 JAAS 策略文件中指定的。缺省的 JAAS 实现使用的策略文件与 Java 2 实现的策略文件相似 — 除了每个授权语句必须与至少一个主体关联在一起。javax.security.auth.Policy 抽象类被用于表示 JAAS 安全性策略。它的缺省实现由 com.sun.security.auth.PolicyFile 提供,在 com.sun.security.auth.PolicyFile 中策略定义在一个文件中。清单 3 是 JAAS 策略文件的一个示例: 清单 3. 示例 JAAS 策略文件 // Example grant entry grant codeBase "file:/C:/sample.jar", signedby "XYZ", principal com.ibm.resource.security.auth.PrincipalExample "admin" { // Allow socket actions to any host using port 8080 permission java.net.SocketPermission "/:8080", "accept, connect, listen, resolve"; // Allows file access (read, write, execute, delete) in // the user's home directory. Permission java.io.FilePermission "${user.home}/-", "read, write, execute, delete"; }; 这个示例与清单 1 中所示的标准 Java 2 策略文件相似。实际上,唯一的不同是主体语句,该语句声明只有拥有指定主体和主体名字的 subject(用户)被授予指定的许可权。 再一次,使用系统属性 java.security.auth.policy 指出 JAAS 策略文件驻留在何处,如下所示: java -Djava.security.auth.policy=policy.jaas Subject 类包含几个方法来作为特殊 subject 执行工作;这些方法如下所示: public static Object doAs(Subject subject, java.security.PrivilegedAction action) public static Object doAs(Subject subject, java.security.PrivilegedAction action) throws java.security.PrivilegedActionException 注意,用来保护敏感代码的方法与“Java 2 代码源访问控制”(Java 2 CodeSource Access Control)概述中描述的方法相同。请参阅参考资料部分以了解更多关于 JAAS 中代码源访问控制和认证的信息。 JAAS 中的授权 清单 4 显示一个授权请求的结果,该请求使用清单 3 中显示的 JAAS 策略文件。假设已经安装了 SecurityManager,并且 loginContext 已经认证了一个带有名为“admin”的 com.ibm.resource.security.auth.PrincipalExample 主体的 Subject。 清单 4. 一个简单的授权请求 public class JaasExample { public static void main(String[] args) { ... // where authenticatedUser is a Subject with // a PrincipalExample named admin. Subject.doAs(authenticatedUser, new JaasExampleAction()); ... } } public class JaasExampleAction implements PrivilegedAction { public Object run() { FileWriter fw = new FileWriter("hi.txt"); fw.write("Hello, World!"); fw.close(); } } 这里,敏感代码被封装在 JaasExampleAction 类中。还要注意,调用类不要求为 JaasExampleAction 类代码源授予许可权,因为它实现了一个 PrivilegedAction。 扩展 JAAS 大多数应用程序都有定制逻辑,它授权用户不仅仅在类上执行操作,而且还在该类的实例上执行操作。这种授权通常建立在用户和实例之间的关系上。这是 JAAS 的一个小缺点。然而,幸运的是,这样设计 JAAS 使得 JAAS 可以扩展。只要做一点工作,我们将可以扩展 JAAS,使其包含一个通用的、类实例级的授权框架。 在文章开头处我已经说明了,抽象类 javax.security.auth.Policy 被用于代表 JAAS 安全性策略。它的缺省实现是由 com.sun.security.auth.PolicyFile 类提供。PolicyFile 类从 JAAS 格式的文件(象清单 3 中显示的那个一样)中读取策略。 我们需要向这个文件添加一个东西为类实例级授权扩展策略定义:一个与许可权语句相关的可选关系参数。 缺省 JAAS 许可权语句的格式如下: permission ; [name], [actions]; 我们在这个许可权语句的末尾添加一个可选的关系参数来完成策略定义。下面是新许可权语句的格式: permission ; [name], [actions], [relationship]; 在为类实例级授权扩展 JAAS 时要注意的最重要的一点是:许可权实现类必须有一个带三个参数的构造函数。第一个参数是名称参数,第二个是行为参数,最后一个是关系参数。 解析新文件格式 既然文件格式已经改变,就需要一个新的 javax.security.auth.Policy 子类来解析文件。 为简单起见,我们的示例使用了一个新的 javax.security.auth.Policy 子类 com.ibm.resource.security.auth.XMLPolicyFile,来从 XML 文件读取策略。在实际的企业应用程序中,关系数据库更适合执行这个任务。 使用 XMLPolicyFile 类代替缺省的 JAAS 访问控制策略实现的最容易的方法是向 java.security 属性文件添加 auth.policy.provider=com.ibm.resource.security.auth.XMLPolicyFile 条目。java.security 属性文件位于 Java 2 平台运行时的 lib/security 目录下。清单 5 是与 XMLPolicyFile 类一起使用的样本 XML 策略文件: 清单 5. 一个 XML 策略文件 <?xml version="1.0"?>;

; ; ; ; ; ; ; ; ; ; ; ; ; 在这个示例策略文件中,任何与名为 PrincipalExample 的用户有关的用户(Subject)都可以创建并读取一个 Auction.class 实例。但是,只有创建该实例的用户才可以更新(写)它。这是第三个 permission 元素定义的,该元素包含值为 owner 的 relationship 属性。Bid.class 实例也是一样,除了相应 Auction.class 实例的所有者可以更改投标接受标志。 Resource 接口 要求类实例级访问控制的类必须实现 Resource 接口。该接口的 getOwner() 方法返回类实例的所有者。fulfills(Subject subject, String relationship) 方法被用于处理特定关系。另外,这些类使用 com.ibm.resource.security.auth.ResourcePermission 类保护敏感代码。例如,Auction 类拥有下列构造函数: public Auction() { Permission permission = new ResourcePermission("com.ibm.security.sample.Auction", "create"); AccessController.checkPermission(permission); } 所有者关系 ResourcePermission 类的 implies(Permission p) 方法是这个框架的关键。implies() 方法就等同性比较名称和行为属性。如果定义了一个关系,那么必须把受保护的类实例(Resource)传递到 ResourcePermission 构造函数中。ResourcePermission 类理解所有者关系。它将类实例的所有者与执行代码的 subject(用户)进行比较。特定关系被委托给受保护类的 fulfills() 方法。 例如,在清单 5 中所示的 XML 策略文件中,只有 Auction 类实例的所有者可以更新(写)文件。该类的 setter 方法使用清单 6 中显示的保护代码: 清单 6. 运行中的 implies(Permission) 方法 public void setName(String newName) { Permission permission = new ResourcePermission("com.ibm.security.sample.Auction", "write", this); AccessController.checkPermission(permission); // sensitive code this.name = newName; } 被传递到 ResourcePermission 构造函数中的 this 引用代表 Auction 类实现的 Resource 接口。由于策略文件中列出的关系是 owner,所以 ResourcePermission 类使用这个引用检查当前 Subject(用户)是否拥有与实例所有者相匹配的主体。如果指定了另一个关系,那么 ResourcePermission 类调用 Auction 类的 fulfills(Subject subject, String relationship) 方法。由 Resource 实现类提供 fulfills() 方法中的逻辑。 XML 策略文件中列出的 Bid 类拥有清单 7 中所示的方法(假设 Bid 类实例有一个对相应 Auction 类实例的引用 — auction)。 清单 7. 处理特定关系 public void setAccepted(boolean flag) { Permission permission = new ResourcePermission("com.ibm.security.sample.Auction", "accept", this); AccessController.checkPermission(permission); // sensitive code this.accepted = flag; } public boolean fulfills(Subject user, String relationship) { if( relationship.equalsIgnoreCase("auctionOwner") ) { String auctionOwner = auction.getOwner(); Iterator principalIterator = user.getPrincipals().iterator(); while(principalIterator.hasNext()) { Principal principal = (Principal) principalIterator.next(); if( principal.getName().equals(auctionOwner) ) return true; } } return false; } 传递到 fulfills() 方法中的关系字符串是策略文件中列出的关系。在这个案例中,我们使用了“auctionOwner”字符串。 缺省情况下,XMLPolicyFile 类在当前工作目录中查找名为 ResourcePolicy.xml 的文件。系统属性 com.ibm.resource.security.auth.policy 可以用于指定另一个不同的文件名和位置。 WebSphere Application Server 示例 除命令行示例之外,您可能还想运行这个简单的程序,该程序为了 IBM WebSphere Application Server,version 4.0.2 而被优化。

一个可运行的示例 综合这些信息,我们将运行一个简单的命令行示例。该示例程序包含三个 jar 文件: resourceSecurity.jar example.jar exampleActions.jar resourceSecurity.jar 文件包含允许实例级访问控制的 JAAS 扩展框架。它还包含一个 LoginModuleExample 类,这个类从 XML 文件读取用户认证信息。用户标识和密码存储在 users.xml 文件中。用户组存储在 groups.xml 文件中。关于 LoginModuleExample 的更多信息,请参阅参考资料部分。 该示例包含四个附加的文件: login.conf policy resourcePolicy.xml run.bat 在试图运行这个示例程序之前,请确保更新了 run.bat、policy 和 resourcePolicy.xml 文件中的路径。缺省情况下,所有的密码都是“passw0rd”。 示例如何工作 该示例程序提示输入用户标识和密码。它用 users.xml 文件中的条目核对所提供的用户标识和密码。在认证了用户之后,程序设法创建一个 UserProfile 类实例,修改它并从中读取。缺省情况下,UserProfile 类的所有者是 Jane(jane)。当 Jane 登录时,三个操作全部成功。当 John(john)登录时,只有创建操作成功。当 Jane 的经理 Lou(lou)登录时,只有第一个和最后一个操作成功。当系统管理员(admin)登录时,操作全部成功。当然,只有当提供的 ResourcePolicy.xml 文件未被修改时,上述这些才都是真的。 示例安装 下面的安装指导假设您正在使用 JDK 1.3 并且已经把文件解压缩到 d:\JaasExample 目录。通过将文件解压缩到这个目录,您可以省去一些工作;否则您就必须使用正确的路径名修改 policy 和 ResourceSecurity.xml 策略文件。 下面是运行该示例需要做的工作: 下载这个示例的源文件。 把 jaas.jar 和 jaasmod.jar 复制到 JDK jre\lib\ext 目录(即 D:\JDK1.3\jre\lib\ext)。 向位于 JDK 的 jre\lib\security 目录(即 D:\JDK1.3\jre\lib\security)中的 java.security 文件的末尾添加下面的字符串:auth.policy.provider=com.ibm.resource.security.auth.XMLPolicyFile。 执行 run.bat 文件。 结束语 类实例级授权把访问控制分离到一个通用框架(该框架使用基于所有权和特定关系的策略)中。然后管理员可以在应用程序的生命周期内更改这些策略。用这种方法扩展 JAAS 减少了您或另一个程序员必须在应用程序生命周期内业务规则发生更改时重写代码的可能性。 通过将关系字符串抽象为类可以进一步扩展特定关系这个概念。不调用 Resource 实现类的 fulfills(Subject user, String relationship) 方法,而只要调用 Relationship 实现类中定义的新 fulfills(Subject user, Resource resource) 方法。这样就会允许许多 Resource 实现类使用相同的关系逻辑。 6.Java的安全性

  1. the security manager是一个application-wide object ( java.lang.SecurityManager) 每个Java Application都可以有自己地Security Manager,但是默认地Java Application没有一个Security Manager 可以通过下面地代码得到一个Security Manager try { System.setSecurityManager(new SecurityManager(“--”)); } catch( ) {} 2. JDBC 在 JDBC 2 开发的过程中,SQL99 还处在一种变化不定的情况下。现在规范已经完成了,而且数据库厂商已经采用了部分标准。所以自然地,JDBC 规范就跟着将自己与 SQL99 功能的一部分相统一。最新的 JDBC 规范已经采用了 SQL99 标准中那些已经被广泛支持的功能,还有那些在五年内可能会获得支持的功能。
  2. DataSource 在JDBC2.0 Optional Package中,提供了透明的连接池(Connection pooling)。 一旦配置了J2EE应用服务器后,只要用DataSource获取连接(Connection),连接池(Connection pooling)就会自动的工作。 如果用户希望建立一个数据库连接,通过查询在JNDI服务中的DataSource,可以从DataSource中获取相应的数据库连接。 DataSource被认为是从JNDI中获取的网络资源。 DataSource在池中保存的对象都实现了PooledConnection接口。 当应用程序向DataSource请求一个Connection时,它会找到一个可用的PooledConnection对象。 如果连接池空了,它就向ConnectionPoolecDataSource请求一个新的PooledConnection对象 通过使用 DataSource 接口 (JDBC 2.0) 或 DriverManager (JDBC 1.0) 接口,J2EE 组件可以获得物理数据库连接对象(Connection)。要获得逻辑(合用的)连接,J2EE 组件必须使用以下这些 JDBC 2.0 合用管理器接口: javax.sql.ConnectionPoolDataSource 接口,该接口充当合用的 java.sql.Connection 对象的资源管理器连接 factory。每家数据库服务器供应商都提供该接口的实现 (例如,Oracle 实现 oracle.jdbc.pool.OracleConnectionPoolDataSource 类)。 javax.sql.PooledConnection 接口,该接口封装到数据库的物理连接。同样,数据库供应商提供其实现。 对于那些接口和 XA 连接的每一个,都存在一个 XA(X/Open 规范)等价定义。
  3. ResultSet 在JDBC2.0中,为了获得一个Uptatable Result,在Query语句里必须包含Primarykey,并且查询的内容里必须来自一个table ava.sql.ResultSet接口中定义了三种类型的结果集 TYPE_FORWARD_ONLY TYPE_SCROLL_INSENSITIVE 这种类型的结果集支持双向滚动 TYPE_SCROLL_SENSITIVE 如果要建立一个双向滚动的ResultSet,一定要在建立Statement的时候使用如下参数 Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
  4. JDBC驱动程序 连通oracle8.1.6的JDBC 把oracle8.1.6/lib/jdbc//.zip copy 到 %JAVA_HOME%/jre/lib/ext//.jar 如果光copy不ren为.jar是没有用的。
  5. 事务处理 本地事务 java.sql.Connection接口可以控制事务边界(即开始和结束)。 在事务开始的时候调用setAutoCommit( false ), 而在中止事务时调用rollback或commit()方法。这类事务叫本地事务。 分布式事务 但是,在特定的情况下,可能有多个客户(例如两个不同的servlet或EJB组件)参与了同一个事务。 或者,客户在同一个事务中可能会执行跨越多个数据库的数据库操作。 JDBC2.0 Optional Package 同JTA一起来实现分布式样事务。
  6. 一些技巧 检索自动产生的关键字 为了解决对获取自动产生的或自动增加的关键字的值的需求,JDBC 3.0 API 现在将获取这种值变得很轻松。要确定任何所产生的关键字的值,只要简单地在语句的 execute() 方法中指定一个可选的标记,表示您有兴趣获取产生的值。您感兴趣的程度可以是 Statement.RETURN_GENERATED_KEYS,也可以是 Statement.NO_GENERATED_KEYS。在执行这条语句后,所产生的关键字的值就会通过从 Statement 的实例方法 getGeneratedKeys() 来检索 ResultSet 而获得。ResultSet 包含了每个所产生的关键字的列。清单 1 中的示例创建一个新的作者并返回对应的自动产生的关键字。 清单 1. 检索自动产生的关键字 Statement stmt = conn.createStatement(); // Obtain the generated key that results from the query. stmt.executeUpdate("INSERT INTO authors " +
        '(first_name, last_name) " +
        "VALUES ('George', 'Orwell')",
        Statement.RETURN_GENERATED_KEYS);
    
    ResultSet rs = stmt.getGeneratedKeys(); if ( rs.next() ) { // Retrieve the auto generated key(s). int key = rs.getInt(); } JTA/JTS 1.JTA/JTS基本知识 服务器实现JTS是否对应用程序开发人员来说不是很重要的。 对你来说,应该把JTA看作是可用的API。 JTA是用来开发distributed tansaction的 API. 而JTS定义了支持JTA中实现Transaction Manager 的规范。 JavaTransaction Service (JTS) specifies the implementation of a Transaction Manager which supports the Java Transaction API (JTA) 1.0 Specification at the high-level and implements the Java mapping of the OMG Object Transaction Service (OTS) 1.1 Specification at the low-level. JTS uses the standard CORBA ORB/TS interfaces and Internet Inter-ORB Protocol (IIOP) for transaction context propagation between JTS Transaction Managers. A JTS Transaction Manager provides transaction services to the parties involved in distributed transactions: the application server, the resource manager, the standalone transactional application, and the Communication Resource Manager (CRM). 2.JTA 1.1 事务处理的概念 JTA实际上是由两部分组成的:一个高级的事务性客户接口和一个低级的 X/Open XA接口。 我们关心的是高级客户接口,因为bean可以访问它,而且是推荐的客户应用程序的事务性接口。 低级的XA接口是由EJB服务器和容器使用来自动协调事务和资源(如数据库)的 1.1.1事务划分 a.程序划分 使用UserTransaction启动JTA事务 The UserTransaction interface defines the methods that allow an application to explicitly manage transaction boundaries.(from j2ee API document) b.声明划分 EJB容器使用TransactionManager启动JTA事务 The TransactionManager interface defines the methods that allow an application server to manage transaction boundaries. (from j2ee API document) 1.1.2事务上下文及其传播 事务上下文是一种对资源上的事务操作之间和调用操作的组件之间的联系。 1.1.3资源加入 资源加入(resource enlistment)是一个过程,在这个过程中资源管理器通知事务管理器它要参与事务。 1.1.4两阶段提交 两阶段提交是事务管理器和所有加入到事务中的资源之间的协议,确保要么所有的资源管理器都提交了事务,要么都撤销了事务。 如果在一个事务内部只是访问一个单一资源管理器,不需要执行一个两阶段提交。 如果在一个事务内部只是访问多个资源管理器,两阶段提交是有益的。 1.2事务处理系统中的构件模块 应用组件 资源管理器 资源管理器管理持久和稳定的数据存储系统,并且与事务管理器一起参与两阶段提交和恢复协议。典型的资源管理器如数据库系统和消息队列。 事务管理器 3.JTS JTS 是一个组件事务监视器(component transaction monitor)。 这是什么意思?在第 1 部分,我们介绍了事务处理监视器(TPM)这个概念,TPM 是一个程序,它代表应用程序协调分布式事务的执行。 TPM 与数据库出现的时间长短差不多;在 60 年代后期,IBM 首先开发了 CICS,至今人们仍在使用。经典的(或者说程序化)TPM 管理被程序化定义为针对事务性资源(比如数据库)的操作序列的事务。随着分布式对象协议,如 CORBA、DCOM 和 RMI 的出现,人们希望看到事务更面向对象的前景。将事务性语义告知面向对象的组件要求对 TPM 模型进行扩展 — 在这个模型中事务是按照事务性对象的调用方法定义的。 JTS 只是一个组件事务监视器(有时也称为对象事务监视器(object transaction monitor)),或称为 CTM。 JTS 和 J2EE 的事务支持设计受 CORBA 对象事务服务(CORBA Object Transaction Service,OTS)的影响很大。实际上,JTS 实现 OTS 并充当 Java 事务 API(Java Transaction API)— 一种用来定义事务边界的低级 API — 和 OTS 之间的接口。使用 OTS 代替创建一个新对象事务协议遵循了现有标准,并使 J2EE 和 CORBA 能够互相兼容。 乍一看,从程序化事务监视器到 CTM 的转变好像只是术语名称改变了一下。然而,差别不止这一点。当 CTM 中的事务提交或回滚时,与事务相关的对象所做的全部更改都一起被提交或取消。但 CTM 怎么知道对象在事务期间做了什么事?象 EJB 组件之类的事务性组件并没有 commit() 或 rollback() 方法,它们也没向事务监视器注册自己做了什么事。那么 J2EE 组件执行的操作如何变成事务的一部分呢? 透明的资源征用 当应用程序状态被组件操纵时,它仍然存储在事务性资源管理器(例如,数据库和消息队列服务器)中,这些事务性资源管理器可以注册为分布式事务中的资源管理器。在第 1 部分中,我们讨论了如何在单个事务中征用多个资源管理器,事务管理器如何协调这些资源管理器。资源管理器知道如何把应用程序状态中的变化与特定的事务关联起来。 但这只是把问题的焦点从组件转移到了资源管理器 — 容器如何断定什么资源与该事务有关,可以供它征用?请考虑下面的代码,在典型的 EJB 会话 bean 中您可能会发现这样的代码: 清单 1. bean 管理的事务的透明资源征用 InitialContext ic = new InitialContext(); UserTransaction ut = ejbContext.getUserTransaction(); ut.begin(); DataSource db1 = (DataSource) ic.lookup("java:comp/env/OrdersDB"); DataSource db2 = (DataSource) ic.lookup("java:comp/env/InventoryDB"); Connection con1 = db1.getConnection(); Connection con2 = db2.getConnection(); // perform updates to OrdersDB using connection con1 // perform updates to InventoryDB using connection con2 ut.commit(); 注意,这个示例中没有征用当前事务中 JDBC 连接的代码 — 容器会为我们完成这个任务。我们来看一下它是如何发生的。 资源管理器的三种类型 当一个 EJB 组件想访问数据库、消息队列服务器或者其它一些事务性资源时,它需要到资源管理器的连接(通常是使用 JNDI)。而且,J2EE 规范只认可三种类型的事务性资源 — JDBC 数据库、JMS 消息队列服务器和“其它通过 JCA 访问的事务性服务”。后面一种服务(比如 ERP 系统)必须通过 JCA(J2EE Connector Architecture,J2EE 连接器体系结构)访问。对于这些类型资源中的每一种,容器或提供者都会帮我们把资源征调到事务中。 在清单 1 中,con1 和 con2 好象是普通的 JDBC 连接,比如那些从 DriverManager.getConnection() 返回的连接。我们从一个 JDBC DataSource 得到这些连接,JDBC DataSource 可以通过查找 JNDI 中的数据源名称得到。EJB 组件中被用来查找数据源(java:comp/env/OrdersDB)的名称是特定于组件的;组件的部署描述符的 resource-ref 部分将其映射为容器管理的一些应用程序级 DataSource 的 JNDI 名称。 隐藏的 JDBC 驱动器 每个 J2EE 容器都可以创建有事务意识的池态 DataSource 对象,但 J2EE 规范并不向您展示如何创建,因为这不在 J2EE 规范内。浏览 J2EE 文档时,?br>

深入理解JVM

Posted on

深入理解JVM

1 Java技术与Java虚拟机

说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:

图1 Java四个方面的关系

运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件)。最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也可以看出Java平台由Java虚拟机和 Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:

在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。

那么到底什么是Java虚拟机(JVM)呢?通常我们谈论JVM时,我们的意思可能是:

  1. 对JVM规范的的比较抽象的说明;

  2. 对JVM的具体实现;

  3. 在程序运行期间所生成的一个JVM实例。

对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。在本文中我们所讨论的Java虚拟机(JVM)主要针对第三种情况而言。它可以被看成一个想象中的机器,在实际的计算机上通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器等,还有自己相应的指令系统。

JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从JVM的体系结构和它的运行过程这两个方面来对它进行比较深入的研究。

2 Java虚拟机的体系结构

刚才已经提到,JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些不同,然而JVM还是可以实现跨平台的特性,这就要归功于设计JVM时的体系结构了。

我们知道,一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、 Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:

图3 JVM的体系结构

JVM的每个实例都有一个它自己的方法域和一个堆,运行于JVM内的所有的线程都共享这些区域;当虚拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和 Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态;本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。

下面分别对这几个部分进行说明。

执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本的CPU运算,例如,把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。

Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。

虚拟机的内层循环的执行过程如下:

do{

取一个操作符字节;

根据操作符的值执行一个动作;

}while(程序未结束)

由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为:

第一个字节/*256+第二个字节字节码。

指令流一般只是字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。

对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。

Java的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。

Java方法区与传统语言中的编译后代码或是Unix进程中的正文段类似。它保存方法代码(编译后的java代码)和符号表。在当前的Java实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了一个Java类或一个Java界面的编译后的代码。可以说类文件是 Java语言的执行代码文件。为了保证类文件的平台无关性,Java虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考Sun公司的Java 虚拟机规范。

Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java虚拟机的寄存器有四种:

  1. pc: Java程序计数器;

  2. optop: 指向操作数栈顶端的指针;

  3. frame: 指向当前执行方法的执行环境的指针;

  4. vars: 指向当前执行方法的局部变量区第一个变量的指针。

在上述体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。

Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。

局部变量区

每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那么它实际占据了索引n和n+1所代表的存储空间)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。

运行环境区

在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。

动态链接

运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的class文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。

正常的方法返回

如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。

异常捕捉

异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因是:①动态链接错,如无法找到所需的class文件。②运行时错,如对一个空指针的引用。程序使用了throw语句。

当异常发生时,Java虚拟机采取如下措施:

· 检查与当前方法相联系的catch子句表。每个catch子句包含其有效指令范围,能够处理的异常类型,以及处理异常的代码块地址。

· 与异常相匹配的catch子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理的异常类型的子类型。如果找到了匹配的catch子句,那么系统转移到指定的异常处理块处执行;如果没有找到异常处理块,重复寻找匹配的catch子句的过程,直到当前方法的所有嵌套的 catch子句都被检查过。

· 由于虚拟机从第一个匹配的catch子句处继续执行,所以catch子句表中的顺序是很重要的。因为Java代码是结构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,都可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常情况。

· 如果找不到匹配的catch子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。如果错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。

操作数栈区

机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非通用寄存器的机器(如 Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。

每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。

本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现Java虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则完全与C语言的栈相同。

3 Java虚拟机的运行过程

上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。

虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:

class HelloApp

{

public static void main(String[] args)

{

System.out.println("Hello World!");

for (int i = 0; i < args.length; i++ )

{

System.out.println(args[i]);

}

}

}

编译后在命令行模式下键入: java HelloApp run virtual machine

将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行HelloApp时可能采取的步骤。

开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用 ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类 HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下:

图4:虚拟机的运行过程

4 结束语

本文通过对JVM的体系结构的深入研究以及一个Java程序执行时虚拟机的运行过程的详细分析,意在剖析清楚Java虚拟机的机理。

慢慢琢磨JVM

1 JVM简介

JVM是我们Javaer的最基本功底了,刚开始学Java的时候,一般都是从“Hello World”开始的,然后会写个复杂点class,然后再找一些开源框架,比如Spring,Hibernate等等,再然后就开发企业级的应用,比如网站、企业内部应用、实时交易系统等等,直到某一天突然发现做的系统咋就这么慢呢,而且时不时还来个内存溢出什么的,今天是交易系统报了StackOverflowError,明天是网站系统报了个OutOfMemoryError,这种错误又很难重现,只有分析Javacore和dump文件,运气好点还能分析出个结果,运行遭的点,就直接去庙里烧香吧!每天接客户的电话都是战战兢兢的,生怕再出什么幺蛾子了。我想Java做的久一点的都有这样的经历,那这些问题的最终根结是在哪呢?—— JVM。

JVM全称是Java VirtualMachine,Java虚拟机,也就是在计算机上再虚拟一个计算机,这和我们使用 VMWare不一样,那个虚拟的东西你是可以看到的,这个JVM你是看不到的,它存在内存中。我们知道计算机的基本构成是:运算器、控制器、存储器、输入和输出设备,那这个JVM也是有这成套的元素,运算器是当然是交给硬件CPU还处理了,只是为了适应“一次编译,随处运行”的情况,需要做一个翻译动作,于是就用了JVM自己的命令集,这与汇编的命令集有点类似,每一种汇编命令集针对一个系列的CPU,比如8086系列的汇编也是可以用在8088上的,但是就不能跑在8051上,而JVM的命令集则是可以到处运行的,因为JVM做了翻译,根据不同的CPU,翻译成不同的机器语言。

JVM中我们最需要深入理解的就是它的存储部分,存储?硬盘?NO,NO,JVM是一个内存中的虚拟机,那它的存储就是内存了,我们写的所有类、常量、变量、方法都在内存中,这决定着我们程序运行的是否健壮、是否高效,接下来的部分就是重点介绍之。

2 JVM的组成部分

我们先把JVM这个虚拟机画出来,如下图所示:

从这个图中可以看到,JVM是运行在操作系统之上的,它与硬件没有直接的交互。我们再来看下JVM有哪些组成部分,如下图所示:

该图参考了网上广为流传的JVM构成图,大家看这个图,整个JVM分为四部分:

Class Loader类加载器

类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java程序,然后通过javac编译成class文件,那怎么才能加载到内存中被执行呢?Class Loader承担的就是这个责任,那不可能随便建立一个.class文件就能被加载的,Class Loader加载的class文件是有格式要求,在《JVM Specification》中式这样定义Class文件的结构:

ClassFile{

u4magic;

u2minor_version;

u2major_version;

u2constant_pool_count;

cp_infoconstant_pool[constant_pool_count-1];

u2access_flags;

u2this_class;

u2super_class;

u2interfaces_count;

u2interfaces[interfaces_count];

u2fields_count;

field_infofields[fields_count];

u2methods_count;

method_infomethods[methods_count];

u2attributes_count;

attribute_infoattributes[attributes_count];

}

需要详细了解的话,可以仔细阅读《JVM Specification》的第四章“The class File Format”,这里不再详细说明。

友情提示:Class Loader只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由Execution Engine负责的。

Execution Engine执行引擎

执行引擎也叫做解释器(Interpreter),负责解释命令,提交操作系统执行。

Native Interface本地接口

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。

Runtime data area运行数据区

运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治,下一章节详细介绍之。

整个JVM框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行,瞧,一个完整的系统诞生了!

2 JVM的内存管理

所有的数据和程序都是在运行数据区存放,它包括以下几部分:

q Stack 栈

栈也叫栈内存,是Java程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就Over。问题出来了:栈中存的是那些数据呢?又什么是格式呢?

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“先进后出”原则。

那栈帧中到底存在着什么数据呢?栈帧中主要保存3类数据:本地变量(LocalVariables),包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack),记录出栈、入栈的操作;栈帧数据(FrameData),包括类文件、方法等等。光说比较枯燥,我们画个图来理解一下Java栈,如下图所示:

图示在一个栈中有两个栈帧,栈帧2是最先被调用的方法,先入栈,然后方法2又调用了方法1,栈帧1处于栈顶的位置,栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,线程结束,栈释放。

Heap堆内存

一个JVM实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:

Permanent Space永久存储区

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

Young Generation Space 新生区

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。

Tenure generation space养老区

养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。三个区的示意图如下:

Method Area 方法区

方法区是被所有线程共享,该区域保存所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。

PC Register 程序计数器

每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令。

Native Method Stack 本地方法栈

3 JVM相关问题

问:堆和栈有什么区别

答:堆是存放对象的,但是对象内的临时变量是存在栈内存中,如例子中的methodVar是在运行期存放到栈中的。

栈是跟随线程的,有线程就有栈,堆是跟随JVM的,有JVM就有堆内存。

问:堆内存中到底存在着什么东西?

答:对象,包括对象变量以及对象方法。

问:类变量和实例变量有什么区别?

答:静态变量是类变量,非静态变量是实例变量,直白的说,有static修饰的变量是静态变量,没有static修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。

问:我听说类变量是在JVM启动时就初始化好的,和你这说的不同呀!

答:那你是道听途说,信我的,没错。

问:Java的方法(函数)到底是传值还是传址?

答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地址。对于原始数据类型,JVM的处理方法是从Method Area或Heap中拷贝到Stack,然后运行frame中的方法,运行完毕后再把变量指拷贝回去。

问:为什么会产生OutOfMemory产生?

答:一句话:Heap内存中没有足够的可用内存了。这句话要好好理解,不是说Heap没有内存了,是说新申请内存的对象大于Heap空闲内存,比如现在Heap还空闲1M,但是新申请的内存需要1.1M,于是就会报OutOfMemory了,可能以后的对象申请的内存都只要0.9M,于是就只出现一次OutOfMemory,GC也正常了,看起来像偶发事件,就是这么回事。但如果此时GC没有回收就会产生挂起情况,系统不响应了。

问:我产生的对象不多呀,为什么还会产生OutOfMemory?

答:你继承层次忒多了,Heap中产生的对象是先产生父类,然后才产生子类,明白不?

问:OutOfMemory错误分几种?

答:分两种,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen space”,两种都是内存溢出,heap size是说申请不到新的内存了,这个很常见,检查应用或调整堆内存大小。

“PermGen space”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是因为每次发布应用系统都不重启,久而久之永久存储区中的死对象太多导致新对象无法申请内存,一般重新启动一下即可。

问:为什么会产生StackOverflowError?

答:因为一个线程把Stack内存全部耗尽了,一般是递归函数造成的。

问:一个机器上可以看多个JVM吗?JVM之间可以互访吗?

答:可以多个JVM,只要机器承受得了。JVM之间是不可以互访,你不能在A-JVM中访问B-JVM的Heap内存,这是不可能的。在以前老版本的JVM中,会出现A-JVM Crack后影响到B-JVM,现在版本非常少见。

问:为什么Java要采用垃圾回收机制,而不采用C/C++的显式内存管理?

答:为了简单,内存管理不是每个程序员都能折腾好的。

问:为什么你没有详细介绍垃圾回收机制?

答:垃圾回收机制每个JVM都不同,JVM Specification只是定义了要自动释放内存,也就是说它只定义了垃圾回收的抽象方法,具体怎么实现各个厂商都不同,算法各异,这东西实在没必要深入。

问:JVM中到底哪些区域是共享的?哪些是私有的?

答:Heap和Method Area是共享的,其他都是私有的,

问:什么是JIT,你怎么没说?

答:JIT是指Just In Time,有的文档把JIT作为JVM的一个部件来介绍,有的是作为执行引擎的一部分来介绍,这都能理解。Java刚诞生的时候是一个解释性语言,别嘘,即使编译成了字节码(byte code)也是针对JVM的,它需要再次翻译成原生代码(native code)才能被机器执行,于是效率的担忧就提出来了。Sun为了解决该问题提出了一套新的机制,好,你想编译成原生代码,没问题,我在JVM上提供一个工具,把字节码编译成原生码,下次你来访问的时候直接访问原生码就成了,于是JIT就诞生了,就这么回事。

问:JVM还有哪些部分是你没有提到的?

答:JVM是一个异常复杂的东西,写一本砖头书都不为过,还有几个要说明的:

常量池(constant pool):按照顺序存放程序中的常量,并且进行索引编号的区域。比如int i =100,这个100就放在常量池中。

安全管理器(Security Manager):提供Java运行期的安全控制,防止恶意攻击,比如指定读取文件,写入文件权限,网络访问,创建进程等等,Class Loader在Security Manager认证通过后才能加载class文件的。

方法索引表(Methods table),记录的是每个method的地址信息,Stack和Heap中的地址指针其实是指向Methodstable地址。

问:为什么不建议在程序中显式的生命System.gc()?

答:因为显式声明是做堆内存全扫描,也就是Full GC,是需要停止所有的活动的(Stop The World Collection),你的应用能承受这个吗?

问:JVM有哪些调整参数?

答:非常多,自己去找,堆内存、栈内存的大小都可以定义,甚至是堆内存的三个部分、新生代的各个比例都能调整。

转载自:http://wenku.baidu.com/view/70e45e8ba0116c175f0e4840.html

来源: <深入理解JVM - ChinaJane163的专栏 - 博客频道 - CSDN.NET>

虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩

Posted on

虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩

(Disclaimer:如果需要转载请先与我联系;文中图片请不要直接链接 作者:@RednaxelaFX -> http://rednaxelafx.iteye.com) 大前天收到一条PM: 引用

你好,很冒昧的向你发短消息,我现在在看JS引擎,能过看博客发现你对js engine很了解,我想请教一下你 基于栈的解析器与基于寄存器的解析器有什么同,javascriptcore是基于寄存器的,V8是基于栈的,能不能说一下这两者有什么一样吗?能推荐一点资料吗?谢谢。 我刚收到的时候很兴奋,就开始写回复。写啊写发觉已经比我平时发的帖还要长了,想着干脆把回复直接发出来好了。于是下面就是回复: 你好 ^ ^ 很抱歉拖了这么久才回复。码字和画图太耗时间了。 别说冒昧了,我只是个普通的刚毕业的学生而已,担当不起啊 ==|||| 而且我也不敢说“很”了解,只是有所接触而已。很高兴有人来一起讨论JavaScript引擎的设计与实现,总觉得身边对这个有兴趣的人不多,或者是很少冒出来讨论。如果你发个帖或者blog来讨论这方面的内容我也会很感兴趣的~ 想拿出几点来讨论一下。上面提出的问题我希望能够一一给予回答,不过首先得做些铺垫。 另外先提一点:JavaScriptCore从SquirrelFish版开始是“基于寄存器”的,V8则不适合用“基于栈”或者“基于寄存器”的说法来描述。 1、解析器与解释器 解析器是parser,而解释器是interpreter。两者不是同一样东西,不应该混用。 前者是编译器/解释器的重要组成部分,也可以用在IDE之类的地方;其主要作用是进行语法分析,提取出句子的结构。广义来说输入一般是程序的源码,输出一般是语法树(syntax tree,也叫parse tree等)或抽象语法树(abstract syntax tree,AST)。进一步剥开来,广义的解析器里一般会有扫描器(scanner,也叫tokenizer或者lexical analyzer,词法分析器),以及狭义的解析器(parser,也叫syntax analyzer,语法分析器)。扫描器的输入一般是文本,经过词法分析,输出是将文本切割为单词的流。狭义的解析器输入是单词的流,经过语法分析,输出是语法树或者精简过的AST。 (在一些编译器/解释器中,解析也可能与后续的语义分析、代码生成或解释执行等步骤融合在一起,不一定真的会构造出完整的语法树。但概念上说解析器就是用来抽取句子结构用的,而语法树就是表示句子结构的方式。关于边解析边解释执行的例子,可以看看这帖的计算器。) 举例:将i = a + b /* c作为源代码输入到解析器里,则广义上的解析器的工作流程如下图: 其中词法分析由扫描器完成,语法分析由狭义的解析器完成。 (嗯,说来其实“解析器”这词还是按狭义用法比较准确。把扫描器和解析器合起来叫解析器总觉得怪怪的,但不少人这么用,这里就将就下吧 == 不过近来“scannerless parsing”也挺流行的:不区分词法分析与语法分析,没有单独的扫描器,直接用解析器从源码生成语法树。这倒整个就是解析器了,没狭不狭义的问题) 后者则是实现程序执行的一种实现方式,与编译器相对。它直接实现程序源码的语义,输入是程序源码,输出则是执行源码得到的计算结果;编译器的输入与解释器相同,而输出是用别的语言实现了输入源码的语义的程序。通常编译器的输入语言比输出语言高级,但不一定;也有输入输出是同种语言的情况,此时编译器很可能主要用于优化代码。 举例:把同样的源码分别输入到编译器与解释器中,得到的输出不同: 值得留意的是,编译器生成出来的代码执行后的结果应该跟解释器输出的结果一样——它们都应该实现源码所指定的语义。 在很多地方都看到解析器与解释器两个不同的东西被混为一谈,感到十分无奈。 最近某本引起很多关注的书便在开篇给读者们当头一棒,介绍了“JavaScript解析机制”。“编译”和“预处理”也顺带混为一谈了,还有“预编译” 0_0 我一直以为“预编译”应该是ahead-of-time compilation的翻译,是与“即时编译”(just-in-time compilation,JIT)相对的概念。另外就是PCH(precompile header)这种用法,把以前的编译结果缓存下来称为“预编译”。把AOT、PCH跟“预处理”(preprocess)混为一谈真是诡异。算了,我还是不要淌这浑水的好……打住。 2、“解释器”到底是什么?“解释型语言”呢? 很多资料会说,Python、Ruby、JavaScript都是“解释型语言”,是通过解释器来实现的。这么说其实很容易引起误解:语言一般只会定义其抽象语义,而不会强制性要求采用某种实现方式。 例如说C一般被认为是“编译型语言”,但C的解释器也是存在的,例如Ch。同样,C++也有解释器版本的实现,例如Cint。 一般被称为“解释型语言”的是主流实现为解释器的语言,但并不是说它就无法编译。例如说经常被认为是“解释型语言”的Scheme就有好几种编译器实现,其中率先支持R6RS规范的大部分内容的是Ikarus,支持在x86上编译Scheme;它最终不是生成某种虚拟机的字节码,而是直接生成x86机器码。 解释器就是个黑箱,输入是源码,输出就是输入程序的执行结果,对用户来说中间没有独立的“编译”步骤。这非常抽象,内部是怎么实现的都没关系,只要能实现语义就行。你可以写一个C语言的解释器,里面只是先用普通的C编译器把源码编译为in-memory image,然后直接调用那个image去得到运行结果;用户拿过去,发现直接输入源码可以得到源程序对应的运行结果就满足需求了,无需在意解释器这个“黑箱子”里到底是什么。 实际上很多解释器内部是以“编译器+虚拟机”的方式来实现的,先通过编译器将源码转换为AST或者字节码,然后由虚拟机去完成实际的执行。所谓“解释型语言”并不是不用编译,而只是不需要用户显式去使用编译器得到可执行代码而已。 那么虚拟机(virtual machine,VM)又是什么?在许多不同的场合,VM有着不同的意义。如果上下文是Java、Python这类语言,那么一般指的是高级语言虚拟机(high-level language virtual machine,HLL VM),其意义是实现高级语言的语义。VM既然被称为“机器”,一般认为输入是满足某种指令集架构(instruction set architecture,ISA)的指令序列,中间转换为目标ISA的指令序列并加以执行,输出为程序的执行结果的,就是VM。源与目标ISA可以是同一种,这是所谓same-ISA VM。 前面提到解释器中的编译器的输出可能是AST,也可能是字节码之类的指令序列;一般会把执行后者的程序称为VM,而执行前者的还是笼统称为解释器或者树遍历式解释器(tree-walking interpreter)。这只是种习惯而已,并没有多少确凿的依据。只不过线性(相对于树形)的指令序列看起来更像一般真正机器会执行的指令序列而已。 其实我觉得把执行AST的也叫VM也没啥大问题。如果认同这个观点,那么把DLR看作一种VM也就可以接受了——它的“指令集”就是树形的Expression Tree。 VM并不是神奇的就能执行代码了,它也得采用某种方式去实现输入程序的语义,并且同样有几种选择:“编译”,例如微软的.NET中的CLR;“解释”,例如CPython、CRuby 1.9,许多老的JavaScript引擎等;也有介于两者之间的混合式,例如Sun的JVM,HotSpot。如果采用编译方式,VM会把输入的指令先转换为某种能被底下的系统直接执行的形式(一般就是native code),然后再执行之;如果采用解释方式,则VM会把输入的指令逐条直接执行。 换个角度说,我觉得采用编译和解释方式实现虚拟机最大的区别就在于是否存下目标代码:编译的话会把输入的源程序以某种单位(例如基本块/函数/方法/trace等)翻译生成为目标代码,并存下来(无论是存在内存中还是磁盘上,无所谓),后续执行可以复用之;解释的话则是把源程序中的指令逐条解释,不生成也不存下目标代码,后续执行没有多少可复用的信息。有些稍微先进一点的解释器可能会优化输入的源程序,把满足某些模式的指令序列合并为“超级指令”;这么做就是朝着编译的方向推进。后面讲到解释器的演化时再讨论超级指令吧。 如果一种语言的主流实现是解释器,其内部是编译器+虚拟机,而虚拟机又是采用解释方式实现的,或者内部实现是编译器+树遍历解释器,那它就是名副其实的“解释型语言”。如果内部用的虚拟机是用编译方式实现的,其实跟普遍印象中的“解释器”还是挺不同的…… 可以举这样一个例子:ActionScript 3,一般都被认为是“解释型语言”对吧?但这种观点到底是把FlashPlayer整体看成一个解释器,因而AS3是“解释型语言”呢?还是认为FlashPlayer中的虚拟机采用解释执行方案,因而AS3是“解释型语言”呢? 其实Flash或Flex等从AS3生成出来的SWF文件里就包含有AS字节码(ActionScript Byte Code,ABC)。等到FlashPlayer去执行SWF文件,或者说等到AVM2(ActionScript Virtual Machine 2)去执行ABC时,又有解释器和JIT编译器两种实现。这种需要让用户显式进行编译步骤的语言,到底是不是“解释型语言”呢?呵呵。所以我一直觉得“编译型语言”跟“解释型语言”的说法太模糊,不太好。 有兴趣想体验一下从命令行编译“裸”的AS3文件得到ABC文件,再从命令行调用AVM2去执行ABC文件的同学,可以从这帖下载我之前从源码编译出来的AVM2,自己玩玩看。例如说要编译一个名为test.as的文件,用下列命令: Command prompt代码 复制代码 收藏代码

  1. java -jar asc.jar -import builtin.abc -import toplevel.abc test.as

java -jar asc.jar -import builtin.abc -import toplevel.abc test.as 就是用ASC将test.as编译,得到test.abc。接着用: Command prompt代码 复制代码 收藏代码

  1. avmplus test.abc

avmplus test.abc 就是用AVM2去执行程序了。很生动的体现出“编译器+虚拟机”的实现方式。 这个“裸”的AVM2没有带Flash或Flex的类库,能用的函数和类都有限。不过AS3语言实现是完整的。可以用print()函数来向标准输出流写东西。 Well……其实写Java程序不也是这样么?现在也确实还有很多人把Java称为“解释型语言”,完全无视Java代码通常是经过显式编译步骤才得到.class文件,而有些JVM是采用纯JIT编译方式实现的,内部没解释器,例如JRockit、Maxine VMJikes RVM。我愈发感到“解释型语言”是个应该避开的用语 =_= 关于虚拟机,有本很好的书绝对值得一读,《虚拟机——系统与进程的通用平台》(Virtual Machines: Versatile Platforms for Systems and Processes)。国内有影印版也有中文版,我是读了影印版,不太清楚中文版的翻译质量如何。据说翻译得还行,我无法印证。 3、基于栈与基于寄存器的指令集架构 用C的语法来写这么一个语句: C代码 复制代码 收藏代码

  1. a = b + c;

a = b + c; 如果把它变成这种形式: add a, b, c 那看起来就更像机器指令了,对吧?这种就是所谓“三地址指令”(3-address instruction),一般形式为: op dest, src1, src2 许多操作都是二元运算+赋值。三地址指令正好可以指定两个源和一个目标,能非常灵活的支持二元操作与赋值的组合。ARM处理器的主要指令集就是三地址形式的。 C里要是这样写的话: C代码 复制代码 收藏代码

  1. a += b;

a += b; 变成: add a, b 这就是所谓“二地址指令”,一般形式为: op dest, src 它要支持二元操作,就只能把其中一个源同时也作为目标。上面的add a, b在执行过后,就会破坏a原有的值,而b的值保持不变。x86系列的处理器就是二地址形式的。 上面提到的三地址与二地址形式的指令集,一般就是通过“基于寄存器的架构”来实现的。例如典型的RISC架构会要求除load和store以外,其它用于运算的指令的源与目标都要是寄存器。 显然,指令集可以是任意“n地址”的,n属于自然数。那么一地址形式的指令集是怎样的呢? 想像一下这样一组指令序列: add 5 sub 3 这只指定了操作的源,那目标是什么?一般来说,这种运算的目标是被称为“累加器”(accumulator)的专用寄存器,所有运算都靠更新累加器的状态来完成。那么上面两条指令用C来写就类似: C代码 复制代码 收藏代码

  1. acc += 5;
  2. acc -= 3;

acc += 5;

acc -= 3; 只不过acc是“隐藏”的目标。基于累加器的架构近来比较少见了,在很老的机器上繁荣过一段时间。 那“n地址”的n如果是0的话呢? 看这样一段Java字节码: Java bytecode代码 复制代码 收藏代码

  1. iconst_1
  2. iconst_2
  3. iadd
  4. istore_0

iconst_1

iconst_2 iadd

istore_0 注意那个iadd(表示整型加法)指令并没有任何参数。连源都无法指定了,零地址指令有什么用?? 零地址意味着源与目标都是隐含参数,其实现依赖于一种常见的数据结构——没错,就是栈。上面的iconst_1、iconst_2两条指令,分别向一个叫做“求值栈”(evaluation stack,也叫做operand stack“操作数栈”或者expression stack“表达式栈”)的地方压入整型常量1、2。iadd指令则从求值栈顶弹出2个值,将值相加,然后把结果压回到栈顶。istore_0指令从求值栈顶弹出一个值,并将值保存到局部变量区的第一个位置(slot 0)。 零地址形式的指令集一般就是通过“基于栈的架构”来实现的。请一定要注意,这个栈是指“求值栈”,而不是与系统调用栈(system call stack,或者就叫system stack)。千万别弄混了。有些虚拟机把求值栈实现在系统调用栈上,但两者概念上不是一个东西。 由于指令的源与目标都是隐含的,零地址指令的“密度”可以非常高——可以用更少空间放下更多条指令。因此在空间紧缺的环境中,零地址指令是种可取的设计。但零地址指令要完成一件事情,一般会比二地址或者三地址指令许多更多条指令。上面Java字节码做的加法,如果用x86指令两条就能完成了: X86 asm代码 复制代码 收藏代码

  1. mov eax, 1
  2. add eax, 2

mov eax, 1

add eax, 2 (好吧我犯规了,istore0对应的保存我没写。但假如局部变量比较少的话也不必把EAX的值保存(“溢出”,register spilling)到调用栈上,就这样吧 == 其实就算把结果保存到栈上也就是多一条指令而已……) 一些比较老的解释器,例如CRuby在1.9引入YARV作为新的VM之前的解释器,还有SquirrleFish之前的老JavaScriptCore以及它的前身KJS,它们内部是树遍历式解释器;解释器递归遍历树,树的每个节点的操作依赖于解释其各个子节点返回的值。这种解释器里没有所谓的求值栈,也没有所谓的虚拟寄存器,所以不适合以“基于栈”或“基于寄存器”去描述。 而像V8那样直接编译JavaScript生成机器码,而不通过中间的字节码的中间表示的JavaScript引擎,它内部有虚拟寄存器的概念,但那只是普通native编译器的正常组成部分。我觉得也不应该用“基于栈”或“基于寄存器”去描述它。 V8在内部也用了“求值栈”(在V8里具体叫“表达式栈”)的概念来简化生成代码的过程,在编译过程中进行“抽象解释”,使用所谓“虚拟栈帧”来记录局部变量与求值栈的状态;但在真正生成代码的时候会做窥孔优化,消除冗余的push/pop,将许多对求值栈的操作转变为对寄存器的操作,以此提高代码质量。于是最终生成出来的代码看起来就不像是基于栈的代码了。 关于JavaScript引擎的实现方式,下文会再提到。 4、基于栈与基于寄存器架构的VM,用哪个好? 如果是要模拟现有的处理器,那没什么可选的,原本处理器采用了什么架构就只能以它为源。但HLL VM的架构通常可以自由构造,有很大的选择余地。为什么许多主流HLL VM,诸如JVM、CLI、CPython、CRuby 1.9等,都采用了基于栈的架构呢?我觉得这有三个主要原因: ·实现简单 由于指令中不必显式指定源与目标,VM可以设计得很简单,不必考虑为临时变量分配空间的问题,求值过程中的临时数据存储都让求值栈包办就行。 更新:回帖中cscript指出了这句不太准确,应该是针对基于栈架构的指令集生成代码的编译器更容易实现,而不是VM更容易实现。 ·该VM是为某类资源非常匮乏的硬件而设计的 这类硬件的存储器可能很小,每一字节的资源都要节省。零地址指令比其它形式的指令更紧凑,所以是个自然的选择。 ·考虑到可移植性 处理器的特性各个不同:典型的CISC处理器的通用寄存器数量很少,例如32位的x86就只有8个32位通用寄存器(如果不算EBP和ESP那就是6个,现在一般都算上);典型的RISC处理器的各种寄存器数量多一些,例如ARM有16个32位通用寄存器,Sun的SPARC在一个寄存器窗口里则有24个通用寄存器(8 in,8 local,8 out)。 假如一个VM采用基于寄存器的架构(它接受的指令集大概就是二地址或者三地址形式的),为了高效执行,一般会希望能把源架构中的寄存器映射到实际机器上寄存器上。但是VM里有些很重要的辅助数据会经常被访问,例如一些VM会保存源指令序列的程序计数器(program counter,PC),为了效率,这些数据也得放在实际机器的寄存器里。如果源架构中寄存器的数量跟实际机器的一样,或者前者比后者更多,那源架构的寄存器就没办法都映射到实际机器的寄存器上;这样VM实现起来比较麻烦,与能够全部映射相比效率也会大打折扣。像Dalvik VM的解释器实现,就是把虚拟寄存器全部映射到栈帧(内存)里的,这跟把局部变量区与操作数栈都映射到内存里的JVM解释器实现相比实际区别不太大。 如果一个VM采用基于栈的架构,则无论在怎样的实际机器上,都很好实现——它的源架构里没有任何通用寄存器,所以实现VM时可以比较自由的分配实际机器的寄存器。于是这样的VM可移植性就比较高。作为优化,基于栈的VM可以用编译方式实现,“求值栈”实际上也可以由编译器映射到寄存器上,减轻数据移动的开销。 回到主题,基于栈与基于寄存器的架构,谁更快?看看现在的实际处理器,大多都是基于寄存器的架构,从侧面反映出它比基于栈的架构更优秀。 而对于VM来说,源架构的求值栈或者寄存器都可能是用实际机器的内存来模拟的,所以性能特性与实际硬件又有点不同。一般认为基于寄存器的架构对VM来说也是更快的,原因是:虽然零地址指令更紧凑,但完成操作需要更多的load/store指令,也意味着更多的指令分派(instruction dispatch)次数与内存访问次数;访问内存是执行速度的一个重要瓶颈,二地址或三地址指令虽然每条指令占的空间较多,但总体来说可以用更少的指令完成操作,指令分派与内存访问次数都较少。 这方面有篇被引用得很多的论文讲得比较清楚,Virtual Machine Showdown: Stack Versus Registers,是在VEE 2005发表的。VEE是Virtual Execution Environment的缩写,是ACM下SIGPLAN组织的一个会议,专门研讨虚拟机的设计与实现的。可以去找找这个会议往年的论文,很多都值得读。 5、树遍历解释器图解 在演示基于栈与基于寄存器的VM的例子前,先回头看看更原始的解释器形式。 前面提到解析器的时候用了i = a + b / c的例子,现在让我们来看看由解析器生成的AST要是交给一个树遍历解释器,会如何被解释执行呢? 用文字说不够形象,还是看图吧: 这是对AST的后序遍历:假设有一个eval(Node n)函数,用于解释AST上的每个节点;在解释一个节点时如果依赖于子树的操作,则对子节点递归调用eval(Node n),从这些递归调用的返回值获取需要的值(或副作用)——也就是说子节点都eval好了之后,父节点才能进行自己的eval——典型的后序遍历。 (话说,上图中节点左下角有蓝色标记的说明那是节点的“内在属性”。从属性语法的角度看,如果一个节点的某个属性的值只依赖于自身或子节点,则该属性被称为“综合属性”(synthesized attribute);如果一个节点的某个属性只依赖于自身、父节点和兄弟节点,则该属性被称为“继承属性”(inherited attribute)。上图中节点右下角的红色标记都只依赖子节点来计算,显然是综合属性。) SquirrelFish之前的JavaScriptCore、CRuby 1.9之前的CRuby就都是采用这种方式来解释执行的。 可能需要说明的: ·左值与右值 在源代码i = a + b / c中,赋值符号左侧的i是一个标识符,表示一个变量,取的是变量的“左值”(也就是与变量i绑定的存储单元);右侧的a、b、c虽然也是变量,但取的是它们的右值(也就是与变量绑定的存储单元内的值)。在许多编程语言中,左值与右值在语法上没有区别,它们实质的差异容易被忽视。一般来说左值可以作为右值使用,反之则不一定。例如数字1,它自身有值就是1,可以作为右值使用;但它没有与可赋值的存储单元相绑定,所以无法作为左值使用。 左值不一定只是简单的变量,还可以是数组元素或者结构体的域之类,可能由复杂的表达式所描述。因此左值也是需要计算的。 ·优先级、结合性与求值顺序 这三个是不同的概念,却经常被混淆。通过AST来看就很容易理解:(假设源码是从左到右输入的) 所谓优先级,就是不同操作相邻出现时,AST节点与根的距离的关系。优先级高的操作会更远离根,优先级低的操作会更接近根。为什么?因为整棵AST是以后序遍历求值的,显然节点离根越远就越早被求值。 所谓结合性,就是当同类操作相邻出现时,操作的先后顺序同AST节点与根的距离的关系。如果是左结合,则先出现的操作对应的AST节点比后出现的操作的节点离根更远;换句话说,先出现的节点会是后出现节点的子节点。 所谓求值顺序,就是在遍历子节点时的顺序。对二元运算对应的节点来说,先遍历左子节点再遍历右子节点就是左结合,反之则是右结合。 这三个概念与运算的联系都很紧密,但实际描述的是不同的关系。前两者是解析器根据语法生成AST时就已经决定好的,后者则是解释执行或者生成代码而去遍历AST时决定的。 在没有副作用的环境中,给定优先级与结合性,则无论求值顺序是怎样的都能得到同样的结果;而在有副作用的环境中,求值顺序会影响结果。 赋值运算虽然是右结合的,但仍然可以用从左到右的求值顺序;事实上Java、C/#等许多语言都在规范里写明表达式的求值顺序是从左到右的。上面的例子中就先遍历的=的左侧,求得i的左值;再遍历=的右侧,得到表达式的值23;最后执行=自身,完成对i的赋值。 所以如果你要问:赋值在类似C的语言里明明是右结合的运算,为什么你先遍历左子树再遍历右子树?上面的说明应该能让你发现你把结合性与求值顺序混为一谈了。 看看Java从左到右求值顺序的例子: Java代码 复制代码 收藏代码

  1. public class EvalOrderDemo {
  2. public static void main(String[] args) {
  3. int[] arr = new int[1];
  4. int a = 1;
  5. int b = 2;
  6. arr[0] = a + b;
  7. }
  8. }

public class EvalOrderDemo {

public static void main(String[] args) {
    int[] arr = new int[1];

    int a = 1;
    int b = 2;

    arr[0] = a + b;
}

} 由javac编译,得到arr[0] = a + b对应的字节码是: Java bytecode代码 复制代码 收藏代码

  1. // 左子树:数组下标
  2. // a[0]
  3. aload_1
  4. iconst_0
  5. // 右子树:加法
  6. // a
  7. iload_2
  8. // b
  9. iload_3
  10. // +
  11. iadd
  12. // 根节点:赋值
  13. iastore

// 左子树:数组下标

// a[0] aload_1

iconst_0

// 右子树:加法 // a

iload_2 // b

iload_3 // +

iadd

// 根节点:赋值 iastore 6、从树遍历解释器进化为基于栈的字节码解释器的前端 如果你看到树形结构与后序遍历,并且知道后缀记法(或者逆波兰记法,reverse Polish notation)的话,那敏锐的你或许已经察觉了:要解释执行AST,可以先通过后序遍历AST生成对应的后缀记法的操作序列,然后再解释执行该操作序列。这样就把树形结构压扁,成为了线性结构。 树遍历解释器对AST的求值其实隐式依赖于调用栈:eval(Node n)的递归调用关系是靠调用栈来维护的。后缀表达式的求值则通常显式依赖于一个栈,在遇到操作数时将其压入栈中,遇到运算时将合适数量的值从栈顶弹出进行运算,再将结果压回到栈上。这种描述看起来眼熟么?没错,后缀记法的求值中的核心数据结构就是前文提到过的“求值栈”(或者叫操作数栈,现在应该更好理解了)。后缀记法也就与基于栈的架构联系了起来:后者可以很方便的执行前者。同理,零地址指令也与树形结构联系了起来:可以通过一个栈方便的把零地址指令序列再转换回到树的形式。 Java字节码与Java源码联系紧密,前者可以看成后者的后缀记法。如果想在JVM上开发一种语义能直接映射到Java上的语言,那么编译器很好写:秘诀就是后序遍历AST。 那么让我们再来看看,同样是i = a + b / c这段源码对应的AST,生成Java字节码的例子: (假设a、b、c、i分别被分配到局部变量区的slot 0到slot 3) 能看出Java字节码与源码间的对应关系了么? 一个Java编译器的输入是Java源代码,输出是含有Java字节码的.class文件。它里面主要包含扫描器与解析器,语义分析器(包括类型检查器/类型推导器等),代码生成器等几大部分。上图所展示的就是代码生成器的工作。对Java编译器来说,代码生成就到字节码的层次就结束了;而对native编译器来说,这里刚到生成中间表示的部分,接下去是优化与最终的代码生成。 如果你对PythonCRuby 1.9之类有所了解,会发现它们的字节码跟Java字节码在“基于栈”的这一特征上非常相似。其实它们都是由“编译器+VM”构成的,概念上就像是Java编译器与JVM融为一体一般。 从这点看,Java与Python和Ruby可以说是一条船上的。虽说内部具体实现的显著差异使得先进的JVM比简单的JVM快很多,而JVM又普遍比Python和Ruby快很多。 当解释器中用于解释执行的中间代码是树形时,其中能被称为“编译器”的部分基本上就是解析器;中间代码是线性形式(如字节码)时,其中能被称为编译器的部分就包括上述的代码生成器部分,更接近于所谓“完整的编译器”;如果虚拟机是基于寄存器架构的,那么编译器里至少还得有虚拟寄存器分配器,又更接近“完整的编译器”了。 *7、基于栈与基于寄存器架构的VM的一组图解 要是拿两个分别实现了基于栈与基于寄存器架构、但没有直接联系的VM来对比,效果或许不会太好。现在恰巧有两者有紧密联系的例子——JVM与Dalvik VM。JVM的字节码主要是零地址形式的,概念上说JVM是基于栈的架构。Google Android平台上的应用程序的主要开发语言是Java,通过其中的Dalvik VM来运行Java程序。为了能正确实现语义,Dalvik VM的许多设计都考虑到与JVM的兼容性;但它却采用了基于寄存器的架构,其字节码主要是二地址/三地址混合形式的,乍一看可能让人纳闷。考虑到Android有明确的目标:面向移动设备,特别是最初要对ARM提供良好的支持。ARM9有16个32位通用寄存器,Dalvik VM的架构也常用16个虚拟寄存器(一样多……没办法把虚拟寄存器全部直接映射到硬件寄存器上了);这样Dalvik VM就不用太顾虑可移植性的问题,优先考虑在ARM9上以高效的方式实现,发挥基于寄存器架构的优势。 Dalvik VM的主要设计者Dan Bornstein在Google I/O 2008上做过一个关于Dalvik内部实现的演讲;同一演讲也在Google Developer Day 2008 China和Japan等会议上重复过。这个演讲中Dan特别提到了Dalvik VM与JVM在字节码设计上的区别,指出Dalvik VM的字节码可以用更少指令条数、更少内存访问次数来完成操作。(看不到YouTube的请自行想办法) 眼见为实。要自己动手感受一下该例子,请先确保已经正确安装JDK 6,并从官网获取Android SDK 1.6R1。连不上官网的也请自己想办法。 创建Demo.java文件,内容为: Java代码 复制代码 收藏代码

  1. public class Demo {
  2. public static void foo() {
  3. int a = 1;
  4. int b = 2;
  5. int c = (a + b) /* 5;
  6. }
  7. }

public class Demo {

public static void foo() {
    int a = 1;

    int b = 2;
    int c = (a + b) /* 5;

}

} 通过javac编译,得到Demo.class。通过javap可以看到foo()方法的字节码是: Java bytecode代码 复制代码 收藏代码

  1. 0: iconst_1
  2. 1: istore_0
  3. 2: iconst_2
  4. 3: istore_1
  5. 4: iload_0
  6. 5: iload_1
  7. 6: iadd
  8. 7: iconst_5
  9. 8: imul
  10. 9: istore_2
  11. 10: return

0: iconst_1

1: istore_0 2: iconst_2

3: istore_1 4: iload_0

5: iload_1 6: iadd

7: iconst_5 8: imul

9: istore_2 10: return 接着用Android SDK里platforms\android-1.6\tools目录中的dx工具将Demo.class转换为dex格式。转换时可以直接以文本形式dump出dex文件的内容。使用下面的命令: Command prompt代码 复制代码 收藏代码

  1. dx --dex --verbose --dump-to=Demo.dex.txt --dump-method=Demo.foo --verbose-dump Demo.class

dx --dex --verbose --dump-to=Demo.dex.txt --dump-method=Demo.foo --verbose-dump Demo.class 可以看到foo()方法的字节码是: Dalvik bytecode代码 复制代码 收藏代码

  1. 0000: const/4 v0, /#int 1 // /#1
  2. 0001: const/4 v1, /#int 2 // /#2
  3. 0002: add-int/2addr v0, v1
  4. 0003: mul-int/lit8 v0, v0, /#int 5 // /#05
  5. 0005: return-void

0000: const/4 v0, /#int 1 // /#1

0001: const/4 v1, /#int 2 // /#2 0002: add-int/2addr v0, v1

0003: mul-int/lit8 v0, v0, /#int 5 // /#05 0005: return-void (原本的输出里还有些code-address、local-snapshot等,那些不是字节码的部分,可以忽略。) 让我们看看两个版本在概念上是如何工作的。 JVM: (图中数字均以十六进制表示。其中字节码的一列表示的是字节码指令的实际数值,后面跟着的助记符则是其对应的文字形式。标记为红色的值是相对上一条指令的执行状态有所更新的值。下同) 说明:Java字节码以1字节为单元。上面代码中有11条指令,每条都只占1单元,共11单元==11字节。 程序计数器是用于记录程序当前执行的位置用的。对Java程序来说,每个线程都有自己的PC。PC以字节为单位记录当前运行位置里方法开头的偏移量。 每个线程都有一个Java栈,用于记录Java方法调用的“活动记录”(activation record)。Java栈以帧(frame)为单位线程的运行状态,每调用一个方法就会分配一个新的栈帧压入Java栈上,每从一个方法返回则弹出并撤销相应的栈帧。 每个栈帧包括局部变量区、求值栈(JVM规范中将其称为“操作数栈”)和其它一些信息。局部变量区用于存储方法的参数与局部变量,其中参数按源码中从左到右顺序保存在局部变量区开头的几个slot。求值栈用于保存求值的中间结果和调用别的方法的参数等。两者都以字长(32位的字)为单位,每个slot可以保存byte、short、char、int、float、reference和returnAddress等长度小于或等于32位的类型的数据;相邻两项可用于保存long和double类型的数据。每个方法所需要的局部变量区与求值栈大小都能够在编译时确定,并且记录在.class文件里。 在上面的例子中,Demo.foo()方法所需要的局部变量区大小为3个slot,需要的求值栈大小为2个slot。Java源码的a、b、c分别被分配到局部变量区的slot 0、slot 1和slot 2。可以观察到Java字节码是如何指示JVM将数据压入或弹出栈,以及数据是如何在栈与局部变量区之前流动的;可以看到数据移动的次数特别多。动画里可能不太明显,iadd和imul指令都是要从求值栈弹出两个值运算,再把结果压回到栈上的;光这样一条指令就有3次概念上的数据移动了。 对了,想提醒一下:Java的局部变量区并不需要把某个局部变量固定分配在某个slot里;不仅如此,在一个方法内某个slot甚至可能保存不同类型的数据。如何分配slot是编译器的自由。从类型安全的角度看,只要对某个slot的一次load的类型与最近一次对它的store的类型匹配,JVM的字节码校验器就不会抱怨。以后再找时间写写这方面。 Dalvik VM: 说明:Dalvik字节码以16位为单元(或许叫“双字节码”更准确 =_=|||)。上面代码中有5条指令,其中mul-int/lit8指令占2单元,其余每条都只占1单元,共6单元==12字节。 与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。PC记录的是以16位为单位的偏移量而不是以字节为单位的。 与JVM不同的是,Dalvik VM的栈帧中没有局部变量区与求值栈,取而代之的是一组虚拟寄存器。每个方法被调用时都会得到自己的一组虚拟寄存器。常用v0-v15这16个,也有少数指令可以访问v0-v255范围内的256个虚拟寄存器。与JVM相同的是,每个方法所需要的虚拟寄存器个数都能够在编译时确定,并且记录在.dex文件里;每个寄存器都是字长(32位),相邻的一对寄存器可用于保存64位数据。方法的参数按源码中从左到右的顺序保存在末尾的几个虚拟寄存器里。 与JVM版相比,可以发现Dalvik版程序的指令数明显减少了,数据移动次数也明显减少了,用于保存临时结果的存储单元也减少了。 你可能会抱怨:上面两个版本的代码明明不对应:JVM版到return前完好持有a、b、c三个变量的值;而Dalvik版到return-void前只持有b与c的值(分别位于v0与v1),a的值被刷掉了。 但注意到a与b的特征:它们都只在声明时接受过一次赋值,赋值的源是常量。这样就可以对它们应用常量传播,将 Java代码 复制代码 收藏代码

  1. int c = (a + b) /* 5;

int c = (a + b) /* 5; 替换为 Java代码 复制代码 收藏代码

  1. int c = (1 + 2) /* 5;

int c = (1 + 2) /* 5; 然后可以再对c的初始化表达式应用常量折叠,进一步替换为: Java代码 复制代码 收藏代码

  1. int c = 15;

int c = 15; 把变量的每次状态更新(包括初始赋值在内)称为变量的一次“定义”(definition),把每次访问变量(从变量读取值)称为变量的一次“使用”(use),则可以把代码整理为“使用-定义链”(简称UD链,use-define chain)。显然,一个变量的某次定义要被使用过才有意义。上面的例子经过常量传播与折叠后,我们可以分析得知变量a、b、c都只被定义而没有被使用。于是它们的定义就成为了无用代码(dead code),可以安全的被消除。 上面一段的分析用一句话描述就是:由于foo()里没有产生外部可见的副作用,所以foo()的整个方法体都可以被优化为空。经过dx工具处理后,Dalvik版程序相对JVM版确实是稍微优化了一些,不过没有影响程序的语义,程序的正确性是没问题的。这是其一。 其二是Dalvik版代码只要多分配一个虚拟寄存器就能在return-void前同时持有a、b、c三个变量的值,指令几乎没有变化: Dalvik bytecode代码 复制代码 收藏代码

  1. 0000: const/4 v0, /#int 1 // /#1
  2. 0001: const/4 v1, /#int 2 // /#2
  3. 0002: add-int v2, v0, v1
  4. 0004: mul-int/lit8 v2, v2, /#int 5 // /#05
  5. 0006: return-void

0000: const/4 v0, /#int 1 // /#1

0001: const/4 v1, /#int 2 // /#2 0002: add-int v2, v0, v1

0004: mul-int/lit8 v2, v2, /#int 5 // /#05 0006: return-void 这样比原先的版本多使用了一个虚拟寄存器,指令方面也多用了一个单元(add-int指令占2单元);但指令的条数没变,仍然是5条,数据移动的次数也没变。 题外话1:Dalvik VM是基于寄存器的,x86也是基于寄存器的,但两者的“寄存器”却相当不同:前者的寄存器是每个方法被调用时都有自己一组私有的,后者的寄存器则是全局的。也就是说,概念上Dalvik VM字节码中不用担心保护寄存器的问题,某个方法在调用了别的方法返回过来后自己的寄存器的值肯定跟调用前一样。而x86程序在调用函数时要考虑清楚calling convention,调用方在调用前要不要保护某些寄存器的当前状态,还是说被调用方会处理好这些问题,麻烦事不少。Dalvik VM这种虚拟寄存器让人想起一些实际处理器的“寄存器窗口”,例如SPARC的Register Windows也是保证每个函数都觉得自己有“私有的一组寄存器”,减轻了在代码里处理寄存器保护的麻烦——扔给硬件和操作系统解决了。IA-64也有寄存器窗口的概念。 (当然,Dalvik VM与x86的“寄存器”一个是虚拟寄存器一个是真实硬件的ISA提供的寄存器,本来也不在一个级别上…上面这段只是讨论寄存器的语义。) 题外话2:Dalvik的.dex文件在未压缩状态下的体积通常比同等内容的.jar文件在deflate压缩后还要小。但光从字节码看,Java字节码几乎总是比Dalvik的小,那.dex文件的体积是从哪里来减出来的呢?这主要得益与.dex文件对常量池的压缩,一个.dex文件中所有类都共享常量池,使得相同的字符串、相同的数字常量等都只出现一次,自然能大大减小体积。相比之下,.jar文件中每个类都持有自己的常量池,诸如"Ljava/lang/Object;"这种常见的字符串会被重复多次。Sun自己也有进一步压缩JAR的工具,Pack200,对应的标准是JSR 200。它的主要应用场景是作为JAR的网络传输格式,以更高的压缩比来减少文件传输时间。在官方文档提到了Pack200所用到的压缩技巧, JDK 5.0 Documentation 写道

Pack200 works most efficiently on Java class files. It uses several techniques to efficiently reduce the size of JAR files:

  • It merges and sorts the constant-pool data in the class files and co-locates them in the archive.
  • It removes redundant class attributes.
  • It stores internal data structures.
  • It use delta and variable length encoding.
  • It chooses optimum coding types for secondary compression. 可见.dex文件与Pack200采用了一些相似的减小体积的方法。很可惜目前还没有正式发布的JVM支持直接加载Pack200格式的归档,毕竟网络传输才是Pack200最初构想的应用场景。

    再次提醒注意,上面的描述是针对概念上的JVM与Dalvik VM,而不是针对它们的具体实现。实现VM时可以采用许多优化技巧去减少性能损失,使得实际的运行方式与概念中的不完全相符,只要最终的运行结果满足原本概念上的VM所实现的语义就行。

    上面“简单”的提了些讨论点,不过还没具体到JavaScript引擎,抱歉。弄得太长了,只好在这里先拆分一次……有些东西想写的,洗个澡又忘记了。等想起来再补充 orz “简单”是相对于实际应该掌握的信息量而言。上面写的都还没挠上痒痒,心虚。 Anyway。根据拆分的现状,下一篇应该是讨论动态语言与编译的问题,然后再下一篇会看看解释器的演化方法,再接着会看看JavaScript引擎的状况(主要针对V8和Nitro,也会谈谈Tamarin。就不讨论JScript了)。 关于推荐资料,在“我的收藏”的virtual machine标签里就有不少值得一读的资料。如果只是对JavaScript引擎相关感兴趣的话也可以选着读些。我的收藏里还有v8和tamarin等标签的,资料有的是 ^ ^ 能有耐心读到结尾的同学们,欢迎提出意见和建议,以及指出文中的错漏 ^^ 不像抓到虫就给美分的大师,我没那种信心……错漏难免,我也需要进一步学习。拜托大家了~ P.S. 画图真的很辛苦,加上JavaEye的带宽也不是无限的……所以拜托不要直接链接这帖里的图 <( _)> 有需要原始图片的可以跟我联系。我是画成多帧PNG然后转换为GIF发出来的。上面的PNG图片都还保留有原始的图层信息,要拿去再编辑也很方便 ^ ^ 更新1: 原本在树遍历解释器图解的小节中,我用的是这幅图: 其实上图画得不准确,a、b、c的右值不应该画在节点上的;节点应该只保存了它们的左值才对,要获取对应的右值就要查询变量表。我修改了图更新到正文了。原本的图里对i的赋值看起来很奇怪,就像是遍历过程经过了两次i节点一般,而事实不是那样的。

借HSDB来探索HotSpot VM的运行时数据

Posted on

借HSDB来探索HotSpot VM的运行时数据

(未经许可请勿转载。希望转载请与我联系。) (如果打开此页面时浏览器有点卡住的话请耐心等待片刻。大概是ItEye的代码高亮太耗时了…) 几天前在HLLVM群组有人问了个小问题,说 Java代码 复制代码 收藏代码

  1. public class Test {
  2. static Test2 t1 = new Test2();
  3. Test2 t2 = new Test2();
  4. public void fn() {
  5. Test2 t3 = new Test2();
  6. }
  7. }
  8. class Test2 {
  9. }

public class Test {

static Test2 t1 = new Test2();
       Test2 t2 = new Test2();

public void fn() {
    Test2 t3 = new Test2();    

}

}

class Test2 {

} 这个程序的t1、t2、t3三个变量本身(而不是这三个变量所指向的对象)到底在哪里。 TL;DR版回答是:

  • t1在存Java静态变量的地方,概念上在JVM的方法区(method area)里
  • t2在Java堆里,作为Test的一个实例的字段存在
  • t3在Java线程的调用栈里,作为Test.fn()的一个局部变量存在 不过就这么简单的回答大家都会,满足不了对JVM的实现感兴趣的同学们的好奇心。说到底,这“方法区”到底是啥?Java堆在哪里?Java线程的调用栈又是啥样的? 那就让我们跑点例子,借助调试器来看看在一个实际运行中的JVM里是啥状况。

    (下文中代码也传了一份到https://gist.github.com/rednaxelafx/5392451

    写个启动类来跑上面问题中的代码: Java代码 复制代码 收藏代码
  1. public class Main {
  2. public static void main(String[] args) {
  3. Test test = new Test();
  4. test.fn();
  5. }
  6. }

public class Main {

public static void main(String[] args) {
    Test test = new Test();

    test.fn();
}

} (编译这个Main.java和上面的Test.java时最好加上-g参数生成LocalVariableTable等调试信息,以便后面某些情况下可以用到) 接下来如无特别说明本文将使用Windows 7 64-bit, Oracle JDK 1.7.0_09 Server VM, Serial GC的环境中运行所有例子。 之前在GreenTeaJUG在杭州的活动演示Serviceability Agent的时候也讲到过这是个非常便于探索HotSpot VM内部实现的API,而HSDB则是在SA基础上包装起来的一个调试器。这次我们就用HSDB来做实验。 SA的一个限制是它只实现了调试snapshot的功能:要么要让被调试的目标进程完全暂停,要么就调试core dump。所以我们在用HSDB做实验前,得先让我们的Java程序运行到我们关注的点上才行。 理想情况下我们会希望让这Java程序停在Test.java的第6行,也就是Test.fn()中t3局部变量已经进入作用域,而该方法又尚未返回的地方。怎样才能停在这里呢? 其实用个Java层的调试器即可。大家平时可能习惯了在Eclipse、IntelliJ IDEA、NetBeans等Java IDE里使用Java层调试器,但为了减少对外部工具的依赖,本文将使用Oracle JDK自带的jdb工具来完成此任务。 jdb跟上面列举的IDE里包含的调试器底下依赖着同一套调试API,也就是Java Platform Debugger Architecture (JPDA)。功能也类似,只是界面是命令行的,表明上看起来不太一样而已。 为了方便后续步骤,启动jdb的时候可以设定让目标Java程序使用serial GC和10MB的Java heap。 启动jdb之后可以用stop in命令在指定的Java方法入口处设置断点, 然后用run命令指定主类名称来启动Java程序, 等跑到断点看看位置是否已经到满足需求,还没到的话可以用step、next之类的命令来向前进。 对jdb命令不熟悉的同学可以在启动jdb之后使用help命令来查看命令列表和说明。 具体步骤如下: Command prompt代码 复制代码 收藏代码

  1. D:\test>jdb -XX:+UseSerialGC -Xmx10m
  2. Initializing jdb ...
  3. stop in Test.fn

  4. Deferring breakpoint Test.fn.
  5. It will be set after the class is loaded.
  6. run Main

  7. run Main
  8. Set uncaught java.lang.Throwable
  9. Set deferred uncaught java.lang.Throwable
  10. VM Started: Set deferred breakpoint Test.fn
  11. Breakpoint hit: "thread=main", Test.fn(), line=5 bci=0
  12. 5 Test2 t3 = new Test2();
  13. main[1] next
  14. Step completed: > "thread=main", Test.fn(), line=6 bci=8
  15. 6 }
  16. main[1]

D:\test>jdb -XX:+UseSerialGC -Xmx10m

Initializing jdb ...

stop in Test.fn

Deferring breakpoint Test.fn. It will be set after the class is loaded.

run Main run Main

Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable

> VM Started: Set deferred breakpoint Test.fn

Breakpoint hit: "thread=main", Test.fn(), line=5 bci=0

5 Test2 t3 = new Test2();

main[1] next

Step completed: > "thread=main", Test.fn(), line=6 bci=8 6 }

main[1] 按照上述步骤执行完最后一个next命令之后,我们就来到了最初想要的Test.java的第6行,也就是Test.fn()返回前的位置。 接下来把这个jdb窗口放一边,另开一个命令行窗口用jps命令看看我们要调试的Java进程的pid是多少: Command prompt代码 复制代码 收藏代码

  1. D:\test>jps
  2. 4328 Main
  3. 9064 Jps
  4. 7716 TTY

D:\test>jps

4328 Main 9064 Jps

7716 TTY 可以看到是4328。把这个pid记下来待会儿用。 然后启动HSDB: Command prompt代码 复制代码 收藏代码

  1. D:\test>java -cp .;%JAVA_HOME%/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

D:\test>java -cp .;%JAVA_HOME%/lib/sa-jdi.jar sun.jvm.hotspot.HSDB (要留意Linux和Solaris在Oracle/Sun JDK6就可以使用HSDB了,但Windows上要到Oracle JDK7才可以用HSDB) 启动HSDB之后,把它连接到目标进程上。从菜单里选择File -> Attach to HotSpot process: 在弹出的对话框里输入刚才记下的pid然后按OK: 这会儿就连接到目标进程了: 刚开始打开的窗口是Java Threads,里面有个线程列表。双击代表线程的行会打开一个Oop Inspector窗口显示HotSpot VM里记录线程的一些基本信息的C++对象的内容。 不过这里我们更可能会关心的是线程栈的内存数据。先选择main线程,然后点击Java Threads窗口里的工具栏按钮从左数第2个可以打开Stack Memory窗口来显示main线程的栈: Stack Memory窗口的内容有三栏: 左起第1栏是内存地址,请让我提醒一下本文里提到“内存地址”的地方都是指虚拟内存意义上的地址,不是“物理内存地址”,请不要弄混了这俩概念; 第2栏是该地址上存的数据,以字宽为单位,本文例子中我是在Windows 7 64-bit上跑64位的JDK7的HotSpot VM,字宽是64位(8字节); 第3栏是对数据的注释,竖线表示范围,横线或斜线连接范围与注释文字。 现在看不懂这个窗口里的数据没关系,先放一边,后面再回过头来看。 现在让我们打开HSDB里的控制台,以便用命令来了解更多信息。 在菜单里选择Windows -> Console: 然后会得到一个空白的Command Line窗口。在里面敲一下回车就会出现hsdb>提示符。 (用过CLHSDB的同学可能会发现这就是把CLHSDB嵌入在了HSDB的图形界面里) 不知道有什么命令可用的同学可以先用help命令看看命令列表。 可以用universe命令来查看GC堆的地址范围和使用情况: Hsdb代码 复制代码 收藏代码

  1. hsdb> universe
  2. Heap Parameters:
  3. Gen 0: eden [0x00000000fa400000,0x00000000fa4aad68,0x00000000fa6b0000) space capacity = 2818048, 24.831088753633722 used
  4. from [0x00000000fa6b0000,0x00000000fa6b0000,0x00000000fa700000) space capacity = 327680, 0.0 used
  5. to [0x00000000fa700000,0x00000000fa700000,0x00000000fa750000) space capacity = 327680, 0.0 usedInvocations: 0
  6. Gen 1: old [0x00000000fa750000,0x00000000fa750000,0x00000000fae00000) space capacity = 7012352, 0.0 usedInvocations: 0
  7. perm [0x00000000fae00000,0x00000000fb078898,0x00000000fc2c0000) space capacity = 21757952, 11.90770160721009 usedInvocations: 0

hsdb> universe

Heap Parameters: Gen 0: eden [0x00000000fa400000,0x00000000fa4aad68,0x00000000fa6b0000) space capacity = 2818048, 24.831088753633722 used

from [0x00000000fa6b0000,0x00000000fa6b0000,0x00000000fa700000) space capacity = 327680, 0.0 used to [0x00000000fa700000,0x00000000fa700000,0x00000000fa750000) space capacity = 327680, 0.0 usedInvocations: 0

Gen 1: old [0x00000000fa750000,0x00000000fa750000,0x00000000fae00000) space capacity = 7012352, 0.0 usedInvocations: 0

perm 0x00000000fae00000,0x00000000fb078898,0x00000000fc2c0000) space capacity = 21757952, 11.90770160721009 usedInvocations: 0 这里用的是HotSpot VM的serial GC。GC堆由young gen = DefNewGeneration(包括eden和两个survivor space)、old gen = TenuredGeneration和perm gen = PermGen构成。 其中young gen和old gen构成了这种配置下HotSpot VM里的Java堆(Java heap),而perm gen不属于Java heap的一部分,它存储的主要是元数据或者叫反射信息,主要用于实现JVM规范里的“方法区”概念。 在我们的Java代码里,执行到Test.fn()末尾为止应该创建了3个Test2的实例。它们必然在GC堆里,但都在哪里呢?用scanoops命令来看: Hsdb代码 [复制代码 收藏代码

  1. hsdb> scanoops 0x00000000fa400000 0x00000000fc2c0000 Test2
  2. 0x00000000fa49a710 Test2
  3. 0x00000000fa49a730 Test2
  4. 0x00000000fa49a740 Test2

hsdb> scanoops 0x00000000fa400000 0x00000000fc2c0000 Test2

0x00000000fa49a710 Test2 0x00000000fa49a730 Test2

0x00000000fa49a740 Test2 scanoops接受两个必选参数和一个可选参数:必选参数是要扫描的地址范围,一个是起始地址一个是结束地址;可选参数用于指定要扫描什么类型的对象实例。实际扫描的时候会扫出指定的类型及其派生类的实例。 这里可以看到确实扫出了3个Test2的实例。内容有两列:左边是对象的起始地址,右边是对象的实际类型。 从它们所在的地址,对照前面universe命令看到的GC堆的地址范围,可以知道它们都在eden里。 通过whatis命令可以进一步知道它们都在eden之中分配给main线程的thread-local allocation buffer (TLAB)中: Hsdb代码 复制代码 收藏代码

  1. hsdb> whatis 0x00000000fa49a710
  2. Address 0x00000000fa49a710: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)
  3. hsdb> whatis 0x00000000fa49a730
  4. Address 0x00000000fa49a730: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)
  5. hsdb> whatis 0x00000000fa49a740
  6. Address 0x00000000fa49a740: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)
  7. hsdb>

hsdb> whatis 0x00000000fa49a710

Address 0x00000000fa49a710: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)

hsdb> whatis 0x00000000fa49a730 Address 0x00000000fa49a730: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)

hsdb> whatis 0x00000000fa49a740

Address 0x00000000fa49a740: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)

hsdb> 还可以用inspect命令来查看对象的内容: Hsdb代码 复制代码 收藏代码

  1. hsdb> inspect 0x00000000fa49a710
  2. instance of Oop for Test2 @ 0x00000000fa49a710 @ 0x00000000fa49a710 (size = 16)
  3. _mark: 1

hsdb> inspect 0x00000000fa49a710

instance of Oop for Test2 @ 0x00000000fa49a710 @ 0x00000000fa49a710 (size = 16) _mark: 1 可见一个Test2的实例要16字节。因为Test2类没有任何Java层的实例字段,这里就没有任何Java实例字段可显示。不过本来这里还应该显示一行: Hsdb代码 复制代码 收藏代码

  1. _metadata._compressed_klass: InstanceKlass for Test2 @ 0x00000000fb078608

_metadata._compressed_klass: InstanceKlass for Test2 @ 0x00000000fb078608 不幸因为这个版本的HotSpot VM里带的SA有bug所以没显示出来。此bug在新版里已修。 还想看到更裸的数据的同学可以用mem命令来看实际内存里的数据长啥样: Hsdb代码 复制代码 收藏代码

  1. hsdb> mem 0x00000000fa49a710 2
  2. 0x00000000fa49a710: 0x0000000000000001
  3. 0x00000000fa49a718: 0x00000000fb078608

hsdb> mem 0x00000000fa49a710 2

0x00000000fa49a710: 0x0000000000000001 0x00000000fa49a718: 0x00000000fb078608 mem命令接受的两个参数都必选,一个是起始地址,另一个是以字宽为单位的“长度”。我们知道一个Test2实例有16字节,所以给定长度为2来看。 上面的数字都是啥来的呢? Memory代码 复制代码 收藏代码

  1. 0x00000000fa49a710: _mark: 0x0000000000000001
  2. 0x00000000fa49a718: _metadata._compressed_klass: 0xfb078608
  3. 0x00000000fa49a71c: (padding): 0x00000000

0x00000000fa49a710: _mark: 0x0000000000000001

0x00000000fa49a718: _metadata._compressed_klass: 0xfb078608 0x00000000fa49a71c: (padding): 0x00000000 一个Test2的实例包含2个给VM用的隐含字段作为对象头,和0个Java字段。 对象头的第一个字段是mark word,记录该对象的GC状态、同步状态、identity hash code之类的多种信息。 对象头的第二个字段是个类型信息指针,klass pointer。这里因为默认开启了压缩指针,所以本来应该是64位的指针存在了32位字段里。 最后还有4个字节是为了满足对齐需求而做的填充(padding)。 以前在另一帖里也介绍过这部分内容,可以参考:借助HotSpot SA来一窥PermGen上的对象 顺带发张Inspector的截图来展示HotSpot VM里描述Test2类的VM对象长啥样吧。 在菜单里选Tools -> Inspector,在地址里输入前面看到的klass地址: InstanceKlass存着Java类型的名字、继承关系、实现接口关系,字段信息,方法信息,运行时常量池的指针,还有内嵌的虚方法表(vtable)、接口方法表(itable)和记录对象里什么位置上有GC会关心的指针(oop map)等等。 留意到这个InstanceKlass是给VM内部用的,并不直接暴露给Java层;InstanceKlass不是java.lang.Class的实例。 在HotSpot VM里,java.lang.Class的实例被称为“Java mirror”,意思是它是VM内部用的klass对象的“镜像”,把klass对象包装了一层来暴露给Java层使用。 在InstanceKlass里有个_java_mirror字段引用着它对应的Java mirror,而mirror里也有个隐藏字段指向其对应的InstanceKlass。 所以当我们写obj.getClass(),在HotSpot VM里实际上经过了两层间接引用才能找到最终的Class对象: Java代码 复制代码 收藏代码

  1. obj->_klass->_java_mirror

obj->_klass->_java_mirror 在Oracle JDK7之前,Oracle/Sun JDK的HotSpot VM把Java类的静态变量存在InstanceKlass结构的末尾;从Oracle JDK7开始,为了配合PermGen移除的工作,Java类的静态变量被挪到Java mirror(Class对象)的末尾了。

还有就是,在JDK7之前Java mirror存放在PermGen里,而从JDK7开始Java mirror默认也跟普通Java对象一样先从eden开始分配而不放在PermGen里。到JDK8则进一步彻底移除了PermGen,把诸如klass之类的元数据都挪到GC堆之外管理,而Java mirror的处理则跟JDK7一样。

前面对HSDB的操作和HotSpot VM里的一些内部数据结构有了一定的了解,现在让我们回到主题:找指针! HotSpot VM内部使用直接指针来实现Java引用。在64位环境中有可能启用“压缩指针”的功能把64位指针压缩到只用32位来存。压缩指针与非压缩指针直接有非常简单的1对1对应关系,前者可以看作后者的特例。 于是我们要找t1、t2、t3这三个变量,等同于找出存有指向上述3个Test2实例的地址的存储位置。 不嫌麻烦的话手工扫描内存去找也能找到,不过幸好HSDB内建了revptrs命令,可以找出“反向指针”——如果a变量引用着b对象,那么对b对象来说a就是一个“反向指针”。 先拿第一个Test2的实例试试看: Hsdb代码 复制代码 收藏代码

  1. hsdb> revptrs 0x00000000fa49a710
  2. Computing reverse pointers...
  3. Done.
  4. null
  5. Oop for java/lang/Class @ 0x00000000fa499b00

hsdb> revptrs 0x00000000fa49a710

Computing reverse pointers... Done.

null Oop for java/lang/Class @ 0x00000000fa499b00 还真的找到了一个包含指向Test2实例的指针,在一个java.lang.Class的实例里。 用whatis命令来看看这个Class对象在哪里: Hsdb代码 复制代码 收藏代码

  1. hsdb> whatis 0x00000000fa499b00
  2. Address 0x00000000fa499b00: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)

hsdb> whatis 0x00000000fa499b00

Address 0x00000000fa499b00: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)

可以看到这个Class对象也在eden里,具体来说在main线程的TLAB里。 这个Class对象是如何引用到Test2的实例的呢?再用inspect命令: Hsdb代码 复制代码 收藏代码

  1. hsdb> inspect 0x00000000fa499b00
  2. instance of Oop for java/lang/Class @ 0x00000000fa499b00 @ 0x00000000fa499b00 (size = 120)
  3. <>:
  4. t1: Oop for Test2 @ 0x00000000fa49a710 Oop for Test2 @ 0x00000000fa49a710

hsdb> inspect 0x00000000fa499b00

instance of Oop for java/lang/Class @ 0x00000000fa499b00 @ 0x00000000fa499b00 (size = 120) <>:

t1: Oop for Test2 @ 0x00000000fa49a710 Oop for Test2 @ 0x00000000fa49a710 可以看到,这个Class对象里存着Test类的静态变量t1,指向着第一个Test2实例。 成功找到t1了!这个有点特别,本来JVM规范里也没明确规定静态变量要存在哪里,通常认为它应该在概念中的“方法区”里;但现在在JDK7的HotSpot VM里它实质上也被放在Java heap里了。可以把这种特例看作是HotSpot VM把方法区的一部分数据也放在Java heap里了。 前面也已经提过,在JDK7之前的Oracle/Sun JDK里的HotSpot VM把静态变量存在InstanceKlass末尾,存在PermGen里。那个时候的PermGen更接近于完整的方法区一些。 关于PermGen移除计划的一些零星笔记可以参考我以前一老帖。 再接再厉,用revptrs看看第二个Test2实例有谁引用: Hsdb代码 复制代码 收藏代码

  1. hsdb> revptrs 0x00000000fa49a730
  2. Oop for Test @ 0x00000000fa49a720

hsdb> revptrs 0x00000000fa49a730

Oop for Test @ 0x00000000fa49a720 找到了一个Test实例。同样用whatis来看看它在哪儿: Hsdb代码 复制代码 收藏代码

  1. hsdb> whatis 0x00000000fa49a720
  2. Address 0x00000000fa49a720: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)

hsdb> whatis 0x00000000fa49a720

Address 0x00000000fa49a720: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)

果然也在main线程的TLAB里。 然后看这个Test实例的内容: Hsdb代码 复制代码 收藏代码

  1. hsdb> inspect 0x00000000fa49a720
  2. instance of Oop for Test @ 0x00000000fa49a720 @ 0x00000000fa49a720 (size = 16)
  3. <>:
  4. _mark: 1
  5. t2: Oop for Test2 @ 0x00000000fa49a730 Oop for Test2 @ 0x00000000fa49a730

hsdb> inspect 0x00000000fa49a720

instance of Oop for Test @ 0x00000000fa49a720 @ 0x00000000fa49a720 (size = 16) <>:

_mark: 1 t2: Oop for Test2 @ 0x00000000fa49a730 Oop for Test2 @ 0x00000000fa49a730 可以看到这个Test实例里有个成员字段t2,指向了第二个Test2实例。 于是t2也找到了!在Java堆里,作为Test的实例的成员字段存在。 那么赶紧试试用revptrs命令看第三个Test2实例: Hsdb代码 复制代码 收藏代码

  1. hsdb> revptrs 0x00000000fa49a740
  2. null

hsdb> revptrs 0x00000000fa49a740

null 啥?没找到?!SA这也太弱小了吧。明明就在那里… 回头我会做个补丁让新版HotSpot VM的SA能处理这种情况。 这个时候的HSDB界面全貌: 0x00000000fa49a740看起来有没有点眼熟? 回到前面打开的Stack Memory窗口看,仔细看会发现那个窗口里正好就有0x00000000fa49a740这数字,位于0x000000000287f858地址上。 实际情况是,下面这张图里红色框住的部分就是main线程上Test.fn()的调用对应的栈帧: 如果图里看得不清楚的话,我再用文字重新写一遍(两道横线之间的是Test.fn()的栈帧内容,前后的则是别的东西): Memory代码 复制代码 收藏代码

  1. 0x000000000287f7f0: 0x0000000002886298
  2. 0x000000000287f7f8: 0x0000000002893ca5
  3. 0x000000000287f800: 0x0000000002893ca5

  4. Stack frame for Test.fn() @bci=8, line=6, pc=0x0000000002893ca5, methodOop=0x00000000fb077f78 (Interpreted frame)
  5. 0x000000000287f808: 0x000000000287f808 expression stack bottom <- rsp
  6. 0x000000000287f810: 0x00000000fb077f58 bytecode pointer = 0x00000000fb077f50 (base) + 8 (bytecode index) in PermGen
  7. 0x000000000287f818: 0x000000000287f860 pointer to locals
  8. 0x000000000287f820: 0x00000000fb078360 constant pool cache = ConstantPoolCache for Test in PermGen
  9. 0x000000000287f828: 0x0000000000000000 method data oop = null
  10. 0x000000000287f830: 0x00000000fb077f78 method oop = Method for Test.fn()V in PermGen
  11. 0x000000000287f838: 0x0000000000000000 last Java stack pointer (not set)
  12. 0x000000000287f840: 0x000000000287f860 old stack pointer (saved rsp)
  13. 0x000000000287f848: 0x000000000287f8a8 old frame pointer (saved rbp) <- rbp
  14. 0x000000000287f850: 0x0000000002886298 return address = in interpreter codelet "return entry points" [0x00000000028858b8, 0x00000000028876c0) 7688 bytes
  15. 0x000000000287f858: 0x00000000fa49a740 local[1] "t3" = Oop for Test2 in NewGen
  16. 0x000000000287f860: 0x00000000fa49a720 local[0] "this" = Oop for Test in NewGen

  17. 0x000000000287f868: 0x000000000287f868
  18. 0x000000000287f870: 0x00000000fb077039
  19. 0x000000000287f878: 0x000000000287f8c0
  20. 0x000000000287f880: 0x00000000fb077350
  21. 0x000000000287f888: 0x0000000000000000
  22. 0x000000000287f890: 0x00000000fb077060
  23. 0x000000000287f898: 0x000000000287f860
  24. 0x000000000287f8a0: 0x000000000287f8c0
  25. 0x000000000287f8a8: 0x000000000287f9a0
  26. 0x000000000287f8b0: 0x000000000288062a
  27. 0x000000000287f8b8: 0x00000000fa49a720
  28. 0x000000000287f8c0: 0x00000000fa498ea8
  29. 0x000000000287f8c8: 0x0000000000000000
  30. 0x000000000287f8d0: 0x0000000000000000
  31. 0x000000000287f8d8: 0x0000000000000000

0x000000000287f7f0: 0x0000000002886298

0x000000000287f7f8: 0x0000000002893ca5 0x000000000287f800: 0x0000000002893ca5


Stack frame for Test.fn() @bci=8, line=6, pc=0x0000000002893ca5, methodOop=0x00000000fb077f78 (Interpreted frame)

0x000000000287f808: 0x000000000287f808 expression stack bottom <- rsp 0x000000000287f810: 0x00000000fb077f58 bytecode pointer = 0x00000000fb077f50 (base) + 8 (bytecode index) in PermGen

0x000000000287f818: 0x000000000287f860 pointer to locals 0x000000000287f820: 0x00000000fb078360 constant pool cache = ConstantPoolCache for Test in PermGen

0x000000000287f828: 0x0000000000000000 method data oop = null 0x000000000287f830: 0x00000000fb077f78 method oop = Method for Test.fn()V in PermGen

0x000000000287f838: 0x0000000000000000 last Java stack pointer (not set) 0x000000000287f840: 0x000000000287f860 old stack pointer (saved rsp)

0x000000000287f848: 0x000000000287f8a8 old frame pointer (saved rbp) <- rbp 0x000000000287f850: 0x0000000002886298 return address = in interpreter codelet "return entry points" [0x00000000028858b8, 0x00000000028876c0) 7688 bytes

0x000000000287f858: 0x00000000fa49a740 local[1] "t3" = Oop for Test2 in NewGen 0x000000000287f860: 0x00000000fa49a720 local[0] "this" = Oop for Test in NewGen


0x000000000287f868: 0x000000000287f868

0x000000000287f870: 0x00000000fb077039 0x000000000287f878: 0x000000000287f8c0

0x000000000287f880: 0x00000000fb077350 0x000000000287f888: 0x0000000000000000

0x000000000287f890: 0x00000000fb077060 0x000000000287f898: 0x000000000287f860

0x000000000287f8a0: 0x000000000287f8c0 0x000000000287f8a8: 0x000000000287f9a0

0x000000000287f8b0: 0x000000000288062a 0x000000000287f8b8: 0x00000000fa49a720

0x000000000287f8c0: 0x00000000fa498ea8 0x000000000287f8c8: 0x0000000000000000

0x000000000287f8d0: 0x0000000000000000 0x000000000287f8d8: 0x0000000000000000 回顾JVM规范里所描述的Java栈帧结构,包括: [ 操作数栈 (operand stack) ]

[ 栈帧信息 (dynamic linking) ] [ 局部变量区 (local variables) ] 上张我以前做的投影稿里的图: 再跟HotSpot VM的解释器所使用的栈帧布局对比看看,是不是正好能对应上?局部变量区(locals)有了,VM所需的栈帧信息也有了;执行到这个位置operand stack正好是空的所以看不到它。 (HotSpot VM里把operand stack叫做expression stack。这是因为operand stack通常只在表达式求值过程中才有内容) 从Test.fn()的栈帧中我们可以看到t3变量就在locals[1]的位置上。t3变量也找到了!大功告成! 栈帧信息里具体都是些啥,以后有机会再展开讲吧。 都看到这里了,干脆把main方法的栈帧也如法炮制分析一下。先上图: 然后再用文字写一次: Memory代码 复制代码

  1. 0x000000000287f7f0: 0x0000000002886298
  2. 0x000000000287f7f8: 0x0000000002893ca5
  3. 0x000000000287f800: 0x0000000002893ca5
  4. 0x000000000287f808: 0x000000000287f808
  5. 0x000000000287f810: 0x00000000fb077f58
  6. 0x000000000287f818: 0x000000000287f860
  7. 0x000000000287f820: 0x00000000fb078360
  8. 0x000000000287f828: 0x0000000000000000
  9. 0x000000000287f830: 0x00000000fb077f78
  10. 0x000000000287f838: 0x0000000000000000
  11. 0x000000000287f840: 0x000000000287f860
  12. 0x000000000287f848: 0x000000000287f8a8
  13. 0x000000000287f850: 0x0000000002886298
  14. 0x000000000287f858: 0x00000000fa49a740

  15. Stack frame for Main.main(java.lang.String[]) @bci=9, line=4, pc=0x0000000002886298, methodOop=0x00000000fb077060 (Interpreted frame)
  16. 0x000000000287f860: 0x00000000fa49a720 expression stack[0] = Oop for Test in NewGen
  17. 0x000000000287f868: 0x000000000287f868 expression stack bottom
  18. 0x000000000287f870: 0x00000000fb077039 bytecode pointer = 0x00000000fb077030 (base) + 9 (bytecode index) in PermGen
  19. 0x000000000287f878: 0x000000000287f8c0 pointer to locals
  20. 0x000000000287f880: 0x00000000fb077350 constant pool cache = ConstantPoolCache for Main in PermGen
  21. 0x000000000287f888: 0x0000000000000000 method data oop = null
  22. 0x000000000287f890: 0x00000000fb077060 method oop = Method for Main.main([Ljava/lang/String;)V in PermGen
  23. 0x000000000287f898: 0x000000000287f860 last Java stack pointer
  24. 0x000000000287f8a0: 0x000000000287f8c0 old stack pointer
  25. 0x000000000287f8a8: 0x000000000287f9a0 old frame pointer
  26. 0x000000000287f8b0: 0x000000000288062a return address = in StubRoutines
  27. 0x000000000287f8b8: 0x00000000fa49a720 local[1] "test" = Oop for Test in NewGen
  28. 0x000000000287f8c0: 0x00000000fa498ea8 local[0] "args" = Oop for java.lang.String[] in NewGen

  29. 0x000000000287f8c8: 0x0000000000000000
  30. 0x000000000287f8d0: 0x0000000000000000
  31. 0x000000000287f8d8: 0x0000000000000000

0x000000000287f7f0: 0x0000000002886298

0x000000000287f7f8: 0x0000000002893ca5 0x000000000287f800: 0x0000000002893ca5

0x000000000287f808: 0x000000000287f808 0x000000000287f810: 0x00000000fb077f58

0x000000000287f818: 0x000000000287f860 0x000000000287f820: 0x00000000fb078360

0x000000000287f828: 0x0000000000000000 0x000000000287f830: 0x00000000fb077f78

0x000000000287f838: 0x0000000000000000 0x000000000287f840: 0x000000000287f860

0x000000000287f848: 0x000000000287f8a8 0x000000000287f850: 0x0000000002886298

0x000000000287f858: 0x00000000fa49a740

Stack frame for Main.main(java.lang.String[]) @bci=9, line=4, pc=0x0000000002886298, methodOop=0x00000000fb077060 (Interpreted frame) 0x000000000287f860: 0x00000000fa49a720 expression stack[0] = Oop for Test in NewGen

0x000000000287f868: 0x000000000287f868 expression stack bottom 0x000000000287f870: 0x00000000fb077039 bytecode pointer = 0x00000000fb077030 (base) + 9 (bytecode index) in PermGen

0x000000000287f878: 0x000000000287f8c0 pointer to locals 0x000000000287f880: 0x00000000fb077350 constant pool cache = ConstantPoolCache for Main in PermGen

0x000000000287f888: 0x0000000000000000 method data oop = null 0x000000000287f890: 0x00000000fb077060 method oop = Method for Main.main([Ljava/lang/String;)V in PermGen

0x000000000287f898: 0x000000000287f860 last Java stack pointer 0x000000000287f8a0: 0x000000000287f8c0 old stack pointer

0x000000000287f8a8: 0x000000000287f9a0 old frame pointer 0x000000000287f8b0: 0x000000000288062a return address = in StubRoutines

0x000000000287f8b8: 0x00000000fa49a720 local[1] "test" = Oop for Test in NewGen 0x000000000287f8c0: 0x00000000fa498ea8 local[0] "args" = Oop for java.lang.String[] in NewGen


0x000000000287f8c8: 0x0000000000000000

0x000000000287f8d0: 0x0000000000000000 0x000000000287f8d8: 0x0000000000000000 main的栈帧的operand stack就不是空的了,有一个元素,用来传递参数给其调用的Test.fn()方法(作为“this”)。 仔细的同学可能发现了,0x000000000287f860这个地址前面不是说是调用Test.fn()产生的栈帧么?怎么这里又变成调用main()方法的栈帧的一部分了呢? 其实栈帧直接可以有重叠:(再上一张以前做的投影稿里的图) 这样可以减少传递参数所需的数据拷贝,也节省了空间。 回到HSDB,我们换个方式来把t3变量找出来。这里就需要编译Test.java时给的-g参数所生成的LocalVariableTable的信息了: Hsdb代码 复制代码

  1. hsdb> jseval "ts = jvm.threads"
  2. [Thread (address=0x00000000fa48fb38, name=Service Thread), Thread (address=0x00000000fa48fa18, name=C2 CompilerThread1), Thread (address=0x00000000fa48f8f8, name=C2 CompilerThread0), Thread (address=0x00000000fa49d178, name=JDWP Command Reader), Thread (address=0x00000000fa48f820, name=JDWP Event Helper Thread), Thread (address=0x00000000fa48f6d8, name=JDWP Transport Listener: dt_shmem), Thread (address=0x00000000fa48dc88, name=Attach Listener), Thread (address=0x00000000fa48db68, name=Signal Dispatcher), Thread (address=0x00000000fa405828, name=Finalizer), Thread (address=0x00000000fa4053a0, name=Reference Handler), Thread (address=0x00000000fa404860, name=main)]
  3. hsdb> jseval "t = ts[ts.length - 1]"
  4. Thread (address=0x00000000fa404860, name=main)
  5. hsdb> jseval "fs = t.frames"
  6. [Frame (method=Test.fn(), bci=8, line=6), Frame (method=Main.main(java.lang.String[]), bci=9, line=4)]
  7. hsdb> jseval "f0 = fs[0]"
  8. Frame (method=Test.fn(), bci=8, line=6)
  9. hsdb> jseval "f1 = fs[1]"
  10. Frame (method=Main.main(java.lang.String[]), bci=9, line=4)
  11. hsdb> jseval "f0.locals"
  12. {t3=Object 0x00000000fa49a740}
  13. hsdb>

hsdb> jseval "ts = jvm.threads"

[Thread (address=0x00000000fa48fb38, name=Service Thread), Thread (address=0x00000000fa48fa18, name=C2 CompilerThread1), Thread (address=0x00000000fa48f8f8, name=C2 CompilerThread0), Thread (address=0x00000000fa49d178, name=JDWP Command Reader), Thread (address=0x00000000fa48f820, name=JDWP Event Helper Thread), Thread (address=0x00000000fa48f6d8, name=JDWP Transport Listener: dt_shmem), Thread (address=0x00000000fa48dc88, name=Attach Listener), Thread (address=0x00000000fa48db68, name=Signal Dispatcher), Thread (address=0x00000000fa405828, name=Finalizer), Thread (address=0x00000000fa4053a0, name=Reference Handler), Thread (address=0x00000000fa404860, name=main)] hsdb> jseval "t = ts[ts.length - 1]"

Thread (address=0x00000000fa404860, name=main) hsdb> jseval "fs = t.frames"

[Frame (method=Test.fn(), bci=8, line=6), Frame (method=Main.main(java.lang.String[]), bci=9, line=4)] hsdb> jseval "f0 = fs[0]"

Frame (method=Test.fn(), bci=8, line=6) hsdb> jseval "f1 = fs[1]"

Frame (method=Main.main(java.lang.String[]), bci=9, line=4) hsdb> jseval "f0.locals"

{t3=Object 0x00000000fa49a740}

hsdb>

上面讲栈帧布局的时候出现了“bytecode pointer”字眼。既然之前被不少好奇的同学问过“JVM里字节码存在哪里”,这里就一并回答掉好了。 强调一点:“字节码”只是元数据的一部分。它只负责描述运行逻辑,而其它信息像是类型名、成员的个数、类型、名字等等都不是字节码。在Class文件里是如此,到运行时在JVM里仍然是如此。 HotSpot VM里有一套对象专门用来存放元数据,它们包括:

  • Klass系对象。元数据的最主要入口。用于描述类型的总体信息
  • ConstantPool/ConstantPoolCache对象。每个InstanceKlass关联着一个ConstantPool,作为该类型的运行时常量池。这个常量池的结构跟Class文件里的常量池基本上是对应的。可以参考我以前的一个回帖。ConstantPoolCache主要用于存储某些字节码指令所需的解析(resolve)好的常量项,例如给[get|put]static、[get|put]field、invoke[static|special|virtual|interface|dynamic]等指令对应的常量池项用。
  • Method对象,用来描述Java方法的总体信息,像是方法入口地址、调用/循环计数器等等
  • ConstMethod对象,记录着Java方法的不变的描述信息,包括方法名、方法的访问修饰符、字节码、行号表、局部变量表等等。注意了,字节码就嵌在这ConstMethod对象里面。
  • Symbol对象,对应Class文件常量池里的JVM_CONSTANT_Utf8类型的常量。有一个VM全局的SymbolTable管理着所有Symbol。Symbol由所有Java类所共享。
  • MethodData对象,记录着Java方法执行时的profile信息,例如某方法里的某个字节码之类是否从来没遇到过null,某个条件跳转是否总是走同一个分支,等等。这些信息在解释器(多层编译模式下也在低层的编译生成的代码里)收集,然后供给HotSpot Server Compiler用于做激进优化。 在PermGen移除前,上述元数据对象都在PermGen里,直接被GC管理着。 JDK8彻底移除PermGen后,这些对象被挪到GC堆外的一块叫做Metaspace的空间里做特殊管理,仍然间接的受GC管理。 介绍了背景,让我们回到HSDB里。前面不是说“bytecode pointer (bcp)”嘛,从背景介绍可以知道字节码存在ConstMethod对象里,那就让我们用Test.fn()栈帧里存的bcp来验证一下是否真的如此。 还是用whatis命令: Hsdb代码 复制代码
  1. hsdb> whatis 0x00000000fb077f58
  2. Address 0x00000000fb077f58: In perm generation perm [0x00000000fae00000,0x00000000fb078898,0x00000000fc2c0000) space capacity = 21757952, 11.90770160721009 used

hsdb> whatis 0x00000000fb077f58

Address 0x00000000fb077f58: In perm generation perm 0x00000000fae00000,0x00000000fb078898,0x00000000fc2c0000) space capacity = 21757952, 11.90770160721009 used 这地址确实在PermGen里了。那么inspect一下看看? Hsdb代码 [复制代码

  1. hsdb> inspect 0x00000000fb077f58
  2. Error: sun.jvm.hotspot.debugger.UnalignedAddressException: 100011

hsdb> inspect 0x00000000fb077f58

Error: sun.jvm.hotspot.debugger.UnalignedAddressException: 100011 呃,这样不行。inspect命令只能接受对象的起始地址,但字节码是嵌在ConstMethod对象中间的。 那换条路子。栈帧里还有method oop,指向该栈帧对应的Method对象。先从它入手: Hsdb代码 复制代码

  1. hsdb> inspect 0x00000000fb077f78
  2. instance of Method fn()V@0x00000000fb077f78 @ 0x00000000fb077f78 @ 0x00000000fb077f78 (size = 136)
  3. _mark: 1
  4. _constMethod: ConstMethod fn()V@0x00000000fb077f08 @ 0x00000000fb077f08 Oop @ 0x00000000fb077f08
  5. _constants: ConstantPool for Test @ 0x00000000fb077c68 Oop @ 0x00000000fb077c68
  6. _method_size: 17
  7. _max_stack: 2
  8. _max_locals: 2
  9. _size_of_parameters: 1
  10. _access_flags: 1

hsdb> inspect 0x00000000fb077f78

instance of Method fn()V@0x00000000fb077f78 @ 0x00000000fb077f78 @ 0x00000000fb077f78 (size = 136) _mark: 1

_constMethod: ConstMethod fn()V@0x00000000fb077f08 @ 0x00000000fb077f08 Oop @ 0x00000000fb077f08 _constants: ConstantPool for Test @ 0x00000000fb077c68 Oop @ 0x00000000fb077c68

_method_size: 17 _max_stack: 2

_max_locals: 2 _size_of_parameters: 1

_access_flags: 1 这样就找到了Test.fn()的Method对象,看到里面的_constMethod字段所指向的ConstMethod对象: Hsdb代码 复制代码

  1. hsdb> inspect 0x00000000fb077f08
  2. instance of ConstMethod fn()V@0x00000000fb077f08 @ 0x00000000fb077f08 @ 0x00000000fb077f08 (size = 112)
  3. _mark: 1
  4. _method: Method fn()V@0x00000000fb077f78 @ 0x00000000fb077f78 Oop @ 0x00000000fb077f78
  5. _exception_table: [I @ 0x00000000fae01d50 Oop for [I @ 0x00000000fae01d50
  6. _constMethod_size: 14
  7. _flags: 5
  8. _code_size: 9
  9. _name_index: 18
  10. _signature_index: 12
  11. _generic_signature_index: 0
  12. _code_size: 9

hsdb> inspect 0x00000000fb077f08

instance of ConstMethod fn()V@0x00000000fb077f08 @ 0x00000000fb077f08 @ 0x00000000fb077f08 (size = 112) _mark: 1

_method: Method fn()V@0x00000000fb077f78 @ 0x00000000fb077f78 Oop @ 0x00000000fb077f78 _exception_table: [I @ 0x00000000fae01d50 Oop for [I @ 0x00000000fae01d50

_constMethod_size: 14 _flags: 5

_code_size: 9 _name_index: 18

_signature_index: 12 _generic_signature_index: 0

_code_size: 9 这个ConstMethod对象从0x00000000fb077f08开始,长度112字节,也就是这个对象的范围是0x00000000fb077f08, 0x00000000fb077f78)。bcp指向0x00000000fb077f58,确实在这个ConstMethod范围内。 通过经验可以知道实际上这里字节码的起始地址是0x00000000fb077f50。通过ConstMethod的_code_size字段可以知道该方法的字节码有9字节。找出来用mem命令看看内存里的数据: Hsdb代码 [复制代码

  1. hsdb> mem 0x00000000fb077f50 2
  2. 0x00000000fb077f50: 0x4c0001b7590200ca
  3. 0x00000000fb077f58: 0x00000000004105b1

hsdb> mem 0x00000000fb077f50 2

0x00000000fb077f50: 0x4c0001b7590200ca 0x00000000fb077f58: 0x00000000004105b1 这串数字是什么东西呢?展开来写清楚一点就是: Memory代码 复制代码

  1. 0x00000000fb077f50: bb 00 02 new [Class Test2]
  2. 0x00000000fb077f53: 59 dup
  3. 0x00000000fb077f54: b7 01 00 invokespecial [Method Test2.()V]
  4. 0x00000000fb077f57: 4c astore_1
  5. 0x00000000fb077f58: b1 return

0x00000000fb077f50: bb 00 02 new [Class Test2]

0x00000000fb077f53: 59 dup 0x00000000fb077f54: b7 01 00 invokespecial [Method Test2.()V]

0x00000000fb077f57: 4c astore_1 0x00000000fb077f58: b1 return 眼尖的同学要吐槽了:在0x00000000fb077f50的字节不是0xca么,怎么变成0xbb了? 其实0xca是JVM规范里有描述的一个可选字节码指令,breakpoint Memory代码 复制代码

  1. 0x00000000fb077f50: ca 00 02 breakpoint // 00 02 not used

0x00000000fb077f50: ca 00 02 breakpoint // 00 02 not used 还记得本文的实验一开始用了jdb在Test.fn()的入口设置了断点吗?这就是结果——入口处的字节码指令被改写为breakpoint了。当然,原本的字节码指令也还在别的地方存着,等断点解除之后这个位置就会被恢复成原本的0xbb指令。 把ConstMethod里存的字节码跟Class文件里存的比较一下看看。用javap工具来看Class文件的内容: Javap代码 复制代码

  1. public void fn();
  2. Code:
  3. stack=2, locals=2, args_size=1
  4. 0: bb 00 02 new /#2 // class Test2
  5. 3: 59 dup
  6. 4: b7 00 03 invokespecial /#3 // Method Test2."":()V
  7. 7: 4c astore_1
  8. 8: b1 return

    public void fn();

    Code: stack=2, locals=2, args_size=1

    0: bb 00 02 new /#2 // class Test2 3: 59 dup

    4: b7 00 03 invokespecial /#3 // Method Test2."":()V 7: 4c astore_1

    8: b1 return 几乎一模一样。唯一的不同也是个有趣的小细节:invokespecial的参数的常量池号码不一样了。HotSpot VM执行new指令的时候用的还是Class文件里的常量池号和字节序。而在执行invokespecial时,光是ConstantPool里的的常量项不够地方放解析(resolve)出来的信息,所以把这些信息放在ConstantPoolCache里,然后也把invokespecial指令里的参数改写过来,顺带变成了平台相关的字节序。 同样也看看Main.main()方法。内存内容: Memory代码 复制代码

  9. hsdb> mem 0x00000000fb077030 2

  10. 0x00000000fb077030: 0x4c0001b7590200bb
  11. 0x00000000fb077038: 0x214103b10002b62b

hsdb> mem 0x00000000fb077030 2

0x00000000fb077030: 0x4c0001b7590200bb 0x00000000fb077038: 0x214103b10002b62b 展开来注解: Memory代码 复制代码

  1. 0x00000000fb077030: bb 00 02 new [Class Test]
  2. 0x00000000fb077033: 59 dup
  3. 0x00000000fb077034: b7 01 00 invokespecial [Method Test.()V]
  4. 0x00000000fb077037: 4c astore_1
  5. 0x00000000fb077038: 2b aload_1
  6. 0x00000000fb077039: b6 02 00 invokevirtual [Method Test.fn()V]
  7. 0x00000000fb07703c: b1 return

0x00000000fb077030: bb 00 02 new [Class Test]

0x00000000fb077033: 59 dup 0x00000000fb077034: b7 01 00 invokespecial [Method Test.()V]

0x00000000fb077037: 4c astore_1 0x00000000fb077038: 2b aload_1

0x00000000fb077039: b6 02 00 invokevirtual [Method Test.fn()V] 0x00000000fb07703c: b1 return 对应的javap输出: Javap代码 复制代码

  1. public static void main(java.lang.String[]);
  2. Code:
  3. stack=2, locals=2, args_size=1
  4. 0: bb 00 02 new /#2 // class Test
  5. 3: 59 dup
  6. 4: b7 00 03 invokespecial /#3 // Method Test."":()V
  7. 7: 4c astore_1
  8. 8: 2b aload_1
  9. 9: b6 00 04 invokevirtual /#4 // Method Test.fn:()V
  10. 12: b1 return

    public static void main(java.lang.String[]);

    Code: stack=2, locals=2, args_size=1

    0: bb 00 02 new /#2 // class Test 3: 59 dup

    4: b7 00 03 invokespecial /#3 // Method Test."":()V 7: 4c astore_1

    8: 2b aload_1 9: b6 00 04 invokevirtual /#4 // Method Test.fn:()V

    12: b1 return 好,今天就写到这里吧~

学习JVM的References

Posted on

学习JVM的References

BlueDavy之技术blog

{互联网,OSGi,Java, High Scalability, High Performance,HA}

学习JVM的References

Nov 16

bluedavyjvm jvm, references 15 Comments 本blog中列举了我学习JVM的references,会不断的更新,为了避免版权问题,就不在blog上提供references的下载了,感兴趣的同学可自行下载或购买,:) 大多数的论文可从此下载:http://citeseer.ist.psu.edu/index.jsp 同时推荐@rednaxelafx 整理的jvm的参考资料:http://goo.gl/oXmRQ

References |— Towards a Renaissance VM |— Oracle JRockit The Definitive Guide |— JVM Magic |— JAVA虚拟机中文第二版 |— Java Lang Spec 3.0 |— Inside Out A Modern Virtual Machine Revealed |— Hotspot Overview |— Azul’s JVM experiences |— A Crash Course in Modern Hardware |— [ adaptive ] |— Understanding Adaptive Runtimes |— Adaptive Optimization of Java Real-time |— Adaptive Optimization in the Jalapeno JVM |— [ compiler ] |— The Java HotSpotTM Server Compiler |— Tailoring Graph-coloring Register Allocation For Runtime Compilation |— Linear Scan Register Allocation |— Design of the Java HotSpotTM Client Compiler for Java 6 |— [ concurrent ] |— The.Art.of.Multiprocessor.Programming.Mar.2008 |— The Concurrency Revolution The Hardware Story |— Multithreaded Programming Guide |— JVM Continuations |— java.util.concurrent Synchronizer Framework |— Java Concurrency Gotchas |— Groovy and Concurrency |— concurrent programming without locks |— Concurrency Grab Bag |— Alternative Concurrency Paradigms For the JVM |— Accelerating Java Workloads via GPUs |— A Scalable Lockfree Stack Algorithm |— A Concurrent Dynamic Analysis Framework |— [ io ] |— Asynchronous IO Tricks and Tips |— [ memory management ] |— Tuning Java Memory Manager |— The Ghost in the Virtual Machine A Reference to References |— The Garbage Collection Mythbusters |— SuperSizingJava |— Step by Step GC Tuning in the HotSpot Java Virtual Machine |— parallel gc ppt |— Oracle JDBC Memory Management |— NUMA-Aware-Java-Heaps-for-Server-Applications |— memorymanagement_whitepaper |— markcompact_gc ppt |— Leak Pruning |— GC Vs Explicit MM |— GC Tuning in the hotspot |— Garbage Collection and Memory Architecture |— Garbage Collection Algorithms For Automatic Dynamic Memory Management – Richard Jones |— [ Hotspot GC论文 ] |— Parallel Garbage Collection for Shared Memory Multiprocessors |— Garbage First Garbage Collector |— A Generational Mostly-concurrent Garbage Collector |— [ 其他JVM GC ] |— The pauseless gc |— Immix A Mark-Region Garbage Collector |— How to write a distributed gc |— GC Nirvana High Throughput And Low Latency Together |— [ monitoring and profiling ] |— Where Does All the Native Memory Go |— What’s Happening with My Application JVM Monitoring Tool |— Practical Lessons in Memory Analysis |— MonitoringGuide |— Microarchitectural Characterization of Production JVMs and JavaWorkloads |— Going Beyond Memory Leaks Debugging Java from Dumps, Using Memory Analyzer |— Diagnosing and Fixing Memory Leaks in Web Applications Tips from the Front Line |— [ osrelated ] |— poll-epoll_2 |— poll-epoll_1 |— memory systems |— Linux内核源代码情景分析 |— linux_cpu_scheduler |— Linux 内核中断内幕 |— Linux System and Performance Monitoring |— cpumemory |— [ performance ] |— Towards Performance Measurements for the Java Virtual Machine’s invokedynamic |— Thinking clearly about performance |— The Impact of Performance Asymmetry in Emerging Multicore Architectures |— the art of benchmarking |— Techniques for Obtaining High Performance in Java Programs |— Pipelining for Performance |— Performance myths and legends |— Performance Java Versus C |— How to Tune and Write Low-Latency Applications on the Java Virtual Machine |— How to Get the Most Performance from Sun JVM on Intel? Multi-Core Servers |— Comparing the Performance of Web Server Arch |— A Common API for Measuring Performance

BTrace使用简介 JRockit读书笔记I — Java代码的高效执行

15 Comments (+add yours?)

  1. yangwm Nov 16, 2010 @ 20:34:17 顶!
  2. ikbear Nov 16, 2010 @ 21:29:26 顶!收藏了!
  3. ximengbao Nov 27, 2010 @ 17:01:53 没人留言么 那我留一句
  4. alipay_fred Dec 03, 2010 @ 20:17:48 Bohem GC 还是值得一开

另外richard Jones 明年有本新书 关于GC的 期待中。。。

另外 今年ISMM 2011 的一些会议论文也很 insightfull

  1. bluedavy Dec 04, 2010 @ 21:51:35 恩,感谢建议,:)
  2. 如烟 Dec 20, 2010 @ 22:40:44 搜索过来的,这个还是挺有帮助的,谢了
  3. jilen Jan 01, 2011 @ 22:23:49 给力啊,毕玄同学
  4. clark Feb 11, 2011 @ 14:39:54 楼主同学你好厉害啊,对于我们一般人,这些书都看完,人会不会翘掉??
  5. bluedavy Feb 15, 2011 @ 21:34:00 哈哈,只要有兴趣,看完应该还是正常滴…
  6. lorb Mar 22, 2011 @ 11:28:05 赞,发现好多博客的参考链接都指到这儿 在国内这么浮躁的技术氛围中的一方净土,偶像ORZ JVM相关的中文书籍太少,能否写一本
  7. milo Jun 09, 2011 @ 23:18:15 大侠,请教一个很弱智的问题,最近jboss老出现:java.lang.OutOfMemoryError: nativeGetNewTLA 错误,我想请问 nativeGetNewTLA 有什么含义,因为当我看到java.lang.OutOfMemoryError: allocLargeObjectOrArray 时,根据allocLargeObjectOrArray可大概知道是在堆上为大对象或数组对象分配空间时内存不够了,所以我想nativeGetNewTLA 应该能告诉我点什么。google 上看了很多,不理想,想请教你,另外有没有什么文档对这个OutOfMemoryError错误的各种message做个大概的说明。
  8. bluedavy Jun 10, 2011 @ 11:57:18 …这是java crash后出现的错误信息吧? 也许可以看看这个里面关于OOM的说明:http://blog.bluedavy.com/?p=200
  9. cheto Aug 18, 2011 @ 13:05:15 JRocket The Definite Guide 这一类原版书要怎么才能买到?是不是要代购啊
  10. bluedavy Aug 18, 2011 @ 20:30:29 @cheto,恩,是滴,在国内没有引入影像版前,就只有代购了,或者买电子版吧。

    Leave a Reply

Cancel

Name (required)

Mail (required)

Website

CAPTCHA Image

Refresh Image

CAPTCHA Code /*


July 2013 M T W T F S S « Mar 1234567 891011121314 15161718192021 22232425262728 293031

Categories

Tags

btrace c1 c2 Deflater facebook gc gc tuning Grizzly HBase hotspot Inflater interpreter javac java code generation JavaOne javaone general technical session java代码执行 Java 并发 jit jvm memory management Native Memory Leak NoSQL oom oracle keynote pessimism policy references RPC serial gc SOA sun jdk sun jdk oom Web容量规划的艺术 yuanzhuo 书:分布式Java应用 书评 互联网技术 交流 内存管理 分布式Java应用 圆桌交流 容量规划 悲观策略 服务框架 硅谷公司

订阅

推荐书籍

My Book

© BlueDavy之技术blog 2013

Icons & Wordpress Theme by N.Design