向万鹏的独立博客


  • 首页

  • 分类

  • 归档

  • 标签

深入理解@ModelAttribute注解的工作过程

发表于 2016-11-07   |   分类于 SpringMVC   |  

  在SpringMVC中,我们可以通过使用@ModelAttribute注解标记方法,实现类似于Struts2中Preparable拦截器的效果,其使用方法我们已经在《SpringMVC中如何处理模型数据》中讲述过了。
  现在我们仍以上篇文章中的更新操作为例,来讨论@ModelAttribute的工作过程。即:有一个User类,有id、userName、email三个属性。现在要完成一个更新操作,但是其中有一项属性不能被修改,例如id,那么只能修改两项属性,userName和email,所以从form表单传递的信息就只能有这两项,我们是通过@ModelAttribute注解标记方法来实现的:(其中数据库相关的操作仅采用模拟的方式)
  
index.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <!--
模拟修改操作
1. 原始数据为: 1,Jack,Jack@163.com
2. 将username和email修改为Mike、Mike@qq.com,id不能被修改.
3. 表单回显, 模拟操作直接在表单填写对应的属性值
-->
<form action="springmvc/testModelAttribute" method="Post">
username: <input type="text" name="username" value="Jack"/>
<br>
email: <input type="text" name="email" value="Jack@163.com"/>
<br>
<input type="submit" value="Submit"/>
</form>

  
controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ModelAttribute
public void getUser(Map<String, Object> map) {
System.out.println("modelAttribute method");
// 模拟从数据库中获取对象
User user = new User(1,"Jack", "Jack@163.com");
System.out.println("从数据库中获取一个对象: " + user);
map.put("user", user);
}
@RequestMapping("/testModelAttribute")
public String testModelAttribute(User user){
System.out.println("修改: " + user);
return SUCCESS;
}

运行后,在控制台输出:

image_1b0u4nqt51qsb1b4019hq1fme1h5cm.png-21kB

  在这个例子中,SpringMVC通过映射请求调用目标处理方法testModelAttribute方法之前,做了下面三件事情:
  第1步: 执行 @ModelAttribute 注解修饰的方法: 从数据库中取出对象,把对象放入到了 Map 中,键为:user。
  第2步: SpringMVC 从 Map 中取出 User 对象 user, 并把表单的请求参数赋给 user 的对应属性。
  第3步: SpringMVC 把上述对象作为参数传入目标方法testModelAttribute(User user)。
  
  通过阅读SpringMVC的源码,我了解了这三个步骤具体是这样实现的:
  
  第1步: 执行 @ModelAttribute 注解修饰的方法,实际上把 @ModelAttribute 方法中 Map 中的数据user放在了 implicitModel(这是一个BindingAwareModelMap类型的对象,BindingAwareModelMap类型实现了Map接口) 中。
  
  第2步:核心的功能都在第2步实现,这一步主要做了三件事,其中核心步骤是前两件事,即 2.1 和 2.2 :
  
   2.1(第一件事):确定查找键值对所需要的键(key):
   
    2.1.1:若目标方法的 POJO 类型的参数没有使用 @ModelAttribute 作为修饰, 则 key 为 POJO 类名第一个字母的小写;
    2.1.2:若目标方法的 POJO 类型的参数使用了@ModelAttribute 来修饰, 则 key 为 @ModelAttribute 注解的 value 属性值。
  在上面的例子中,目标方法testModelAttribute(User user)中的入参没有被@ModelAttribute修饰,所以key值为POJO 类名第一个字母的小写形式,即“user”。
  
   2.2(第二件事):在 implicitModel(即map) 中查找 key 对应的对象:
   
    2.2.1:若 @ModelAttribute 标记的方法在 Map 中保存过这样一个键值对, 其 key 和 2.1 确定的 key 一致, 则会获取到key对应的键值对的value值;
    2.2.2:若 implicitModel 中不存在 key 对应的对象,则检查当前的控制器类是否被@SessionAttributes注解修饰,如果使用了@SessionAttributes注解修饰,且@SessionAttributes注解的value值中包含了key,则尝试从HttpSession中获取key所对应的value值,如果value值存在则获取到,如果value值不存在则抛出异常。
    2.2.3:如果没有使用@SessionAttributes注解修饰该控制器类,或者使用了,但是@SessionAttributes注解中的value值不包含key,则SpringMVC会通过反射来创建一个POJO类型的对象。
    
   2.3(第三件事):,用表单中传递过来的参数值去更新2.2确定的value值(或通过反射建立的对象)。
    
  第3步:同上面的第3步描述的一样,即SpringMVC 把上述对象作为参数传入目标方法。
  
  以上就是@ModelAttribute注解的工作流程,我们了解了流程之后,就会发现,在2.2.2中有这样一种情况:如果:
  ①在 implicitModel 中不存在 key 对应的对象;
  ②控制器类有标记了@SessionAttributes注解;
  ③@SessionAttributes注解的value值中包含key;
  ④HttpSession域中不存在该key值对应的value值。
  当同时满足这4种情况时,就会抛出异常,解决方法有两种:
  1、通过@ModelAttribute注解修饰目标方法的入参,确定一个key值,使其不被包含在@SessionAttributes注解的value值中。
  2、使用@ModelAttribute方法,在目标方法调用之前,将key对应的键值对放入implicitModel 中。   

SpringMVC中如何处理模型数据

发表于 2016-11-06   |   分类于 SpringMVC   |  

  Spring MVC 提供了以下几种途径输出模型数据:

  • ModelAndView: 处理方法返回值类型为ModelAndView时, 方法体即可通过该对象添加模型数据
  • Map 及Model: 入参为org.springframework.ui.Model、org.springframework.ui.ModelMap或java.util.Map时,处理方法返回时,Map中的数据会自动添加到模型中。
  • @SessionAttributes: 将模型中的某个属性暂存到HttpSession中,以便多个请求之间可以共享这个属性
  • @ModelAttribute: 方法入参标注该注解后, 入参的对象就会放到数据模型中
      
    下面对它们依次进行介绍:

ModelAndView:

  可将控制器处理方法的返回值设为ModelAndView, ModelAndView中既可存放视图信息,也可存放模型数据信息。
  SpringMVC 会把 ModelAndView 的 model 中数据放入到 request 域对象中。
  
ModelAndView设置视图的方法:

  • void setView(View view)
  • void setViewName(String viewName)

ModelAndView添加模型数据的方法:

  • MoelAndView addObject(String attributeName, Object attributeValue)
  • ModelAndView addAllObject(Map modelMap)

下面举例说明:

index.jsp:

1
<a href="springmvc/testModelAndView">Test ModelAndView</a>

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static final String SUCCESS = "success";
/**
* 目标方法的返回值可以是 ModelAndView 类型。
* 其中可以包含视图和模型信息
* SpringMVC 会把 ModelAndView 的 model 中数据放入到 request 域对象中.
* @return
*/
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
String viewName = SUCCESS;
ModelAndView modelAndView = new ModelAndView(viewName);
//添加模型数据到 ModelAndView 中.
modelAndView.addObject("time", new Date());
return modelAndView;
}

success.jsp:

1
time : ${requestScope.time }

运行会成功跳转到success.jsp并显示当前时间:

image_1b0spbdhjovc19ootuc1dgebnp9.png-13kB

Map及Model:

  Spring MVC 在调用方法前会创建一个隐含的模型对象作为模型数据的存储容器(事实上这个隐含的模型对象是一个BindingAwareModelMap 类型的对象,通过后面的例子我们可以验证),如果方法的入参为Map、Model或者ModelMap 类型,Spring MVC 会将隐含模型的引用传递给这些入参(因为BindingAwareModelMap 继承或实现了Map、Model或者ModelMap)。在方法体内,开发者可以通过这个入参对象访问到模型中的所有数据,也可以向模型中添加新的属性数据,SpringMVC 也会把 Map 中数据放入到 request 域对象中。
  通常情况下使用的都是Map类型,下面我们以Map为例说明:
  
index.jsp:

1
<a href="springmvc/testMap">Test Map</a>

controller:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 目标方法可以添加 Map 类型(实际上也可以是 Model 类型或 ModelMap 类型)的参数.
*
* @param map
* @return
*/
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map) {
System.out.println(map.getClass().getName());
map.put("names", Arrays.asList("Tom", "Jerry", "Mike"));
return SUCCESS;
}

success.jsp:

1
names: ${requestScope.names }

运行程序成功跳转到success.jsp:

image_1b0sqenm3154jii6vb21g4a1o1nm.png-12.1kB

并且在控制台输出了:

org.springframework.validation.support.BindingAwareModelMap

这说明Spring MVC在调用方法前创建的隐含的模型对象是 BindingAwareModelMap 类型。

@SessionAttributes:

  上面介绍的两种方式,SpringMVC都是将数据存放在request域对象中,若希望在多个请求之间共用某个模型属性数据,则可以在控制器类上标注一个@SessionAttributes注解(该注解只能放在类的上面, 而不能修饰放方法), Spring MVC 将把模型中对应的属性暂存到HttpSession中。@SessionAttributes除了可以通过属性名指定需要放到会话中的属性外,还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中,下面举例说明(User类代码略):

1
<a href="springmvc/testSessionAttributes">Test SessionAttributes</a>

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @SessionAttributes 除了可以通过属性名指定需要放到会话中的属性外(实际上使用的是 value 属性值),
* 还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中(实际上使用的是 types 属性值)
*
* 注意: 该注解只能放在类的上面. 而不能修饰放方法.
*/
@RequestMapping("/testSessionAttributes")
public String testSessionAttributes(Map<String,Object> map){
User user = new User("Jack","123456","Jack@163.com",20);
map.put("user", user);
map.put("school", "HUST");
return SUCCESS;
}

success.jsp:

1
2
3
4
5
6
7
8
request user : ${requestScope.user }
<br><br>
session user : ${sessionScope.user }
<br><br>
request school : ${requestScope.school }
<br><br>
session school : ${sessionScope.school }
<br><br>

  现在,我们没有在该控制器类上标记@SessionAttributes注解,运行后跳转到success.jsp,页面显示如下:

image_1b0ssdm811mfs1nt3b3g1td6gvr2a.png-21.7kB

  即Map中的数据只被存放到了request域中,而没有被存放到session域中。现在,我们在该控制器类定义处添加@SessionAttributes注解:

1
2
3
4
5
6
@Controller
@RequestMapping("/springmvc")
@SessionAttributes(value="user",types={String.class})
public class SpringMVCTest {
//...
}

@SessionAttributes(value=”user”,types={String.class})注解指定了属性名为user,或者类型为String的属性会被存放到session域中,现在运行程序,发现在success.jsp中既输出了request user和school,也输出了session user和school:

image_1b0ss9u9q1718gft1ctbjgf1lf01t.png-26.4kB

@ModelAttribute:

  @ModelAttribute可以模拟出Struts2中Preparable拦截器的效果,想象这样一种情景:有一个User类,有id、userName、email三个属性。现在要完成一个修改操作,但是其中有一项属性不能被修改,例如id,那么只能修改两项属性,userName和email,所以从form表单传递的信息就只能有这两项,我们通常的处理方法是new一个User对象user,用form表单传递的值给user赋值,然后用user作为参数去更新信息,现在由于form表单无法传递id值,那么user.id此时就为null,便无法满足我们要求的更新操作,如下图:
  image_1b0stovhk9c81hu41fnomvja7k2n.png-61.2kB
  
  解决这种问题的办法是:对于对象user,我们不采取new的方式,而是使用从数据库中直接获取的方式,这样一来,user中的属性值就是原数据库中存储的属性值,那么用form表单传递过来的userName和email值更新user之后,它的id仍然为原来的值,如下图所示:
  image_1b0su1eligi916uo68cdg8rij34.png-71kB
  
  @ModelAttribute注解为我们提供了这种解决方法的实现:使用@ModelAttribute注解的方法,会在调用目标处理方法之前被调用。下面我们根据先前所说的例子来说明,修改数据库中一条user记录,其中id属性不能修改,数据库相关的操作我们仅采用模拟的方式:
  
 index.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <!--
模拟修改操作
1. 原始数据为: 1,Jack,Jack@163.com
2. 将username和email修改为Mike、Mike@qq.com,id不能被修改.
3. 表单回显, 模拟操作直接在表单填写对应的属性值
-->
<form action="springmvc/testModelAttribute" method="Post">
username: <input type="text" name="username" value="Jack"/>
<br>
email: <input type="text" name="email" value="Jack@163.com"/>
<br>
<input type="submit" value="Submit"/>
</form>

  
controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ModelAttribute
public void getUser(Map<String, Object> map) {
System.out.println("modelAttribute method");
// 模拟从数据库中获取对象
User user = new User(1,"Jack", "Jack@163.com");
System.out.println("从数据库中获取一个对象: " + user);
map.put("user", user);
}
@RequestMapping("/testModelAttribute")
public String testModelAttribute(User user){
System.out.println("修改: " + user);
return SUCCESS;
}

运行后,在控制台输出:

image_1b0u4nqt51qsb1b4019hq1fme1h5cm.png-21kB

  值得注意的是:
  1、被@ModelAttribute标记的方法会在SpringMVC调用该控制器中任何目标方法之前被调用,也就是说,getUser()不仅仅只在调用testModelAttribute(User user)之前被调用,当这个控制器中任何目标方法被调用时,getUser()都会在其之前被调用。
  2、getUser()中放置在map中的键值对的键“user”是testModelAttribute(User user)方法中入参类型“User”的首字母小写形式,这是一种最常用的匹配方式,其实还可以通过@ModelAttribute修饰入参,从而使用另一种匹配方式,这些内容我们将在下一篇文章中,通过阅读SpringMVC的源代码,深入理解@ModelAttribute的工作过程,进行更进一步地讨论。

SpringMVC映射请求参数的方式(参数绑定方式)

发表于 2016-11-06   |   分类于 SpringMVC   |  

   SpringMVC通过分析处理方法的签名,可以将HTTP请求信息绑定到处理方法的相应参数中。

使用@RequestParam绑定请求参数值

  在处理方法参数处使用@RequestParam可以把请求参数传递给请求方法,其中:
– value:请求参数的参数名
– required:该参数是否必须,默认为true,
– defaultValue:请求参数的默认值,表示请求参数中必须包含对应的参数,若不存在,将抛出异常。

例如:

jsp:

1
2
3
<form action="springmvc/test?userName=Jack" method="post">
<input type="submit" value="submit">
</form>

控制器:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
@RequestMapping("/springmvc")
public class SpringMVCTest {
private static final String SUCCESS = "success";
@RequestMapping("/test")
public String testRequestParam(@RequestParam(value="userName") String username) {
System.out.println(username);
return SUCCESS;
}
}

  控制器中的方法也可写为:

1
2
3
4
5
@RequestMapping("/test")
public String testRequestParam(String userName) {
System.out.println(userName);
return SUCCESS;
}

  此时方法的参数名必须与页面传来的参数名一致(都为userName),否则会获取不到。   

使用@RequestHeader绑定请求报头的属性值,使用@CookieValue绑定请求中的Cookie值(不常用,了解即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//使用@RequestHeader绑定请求报头的属性值
@RequestMapping("/testRequestHeader")
public String testRequestHeader(
@RequestHeader(value = "Accept-Language") String al) {
System.out.println("testRequestHeader, Accept-Language: " + al);
return SUCCESS;
}
//使用@CookieValue绑定请求中的Cookie值
@RequestMapping("/testCookieValue")
public String testCookieValue(@CookieValue("JSESSIONID") String sessionId) {
System.out.println("testCookieValue: sessionId: " + sessionId);
return SUCCESS;
}

使用POJO对象绑定请求参数值(用于传递对象)

  SpringMVC会按请求参数名和POJO属性名进行自动匹配,自动为该对象填充属性值。支持级联属性。如:dept.deptId、dept.address.tel等,例如:
  
POJO:

1
2
3
4
5
6
7
8
9
10
11
12
public class User {
private String userName;
private int age;
private Address address;
//get,set,toString方法略
}
public class Address{
private String province;
private String city;
//get,set,toString方法略
}

控制器:

1
2
3
4
5
6
7
8
9
10
@Controller
@RequestMapping("springmvc")
public class SpringMVCTest {
private static final String SUCCESS = "success";
@RequestMapping("/testPOJO")
public String testPOJO(User user){
System.out.println(user);
return SUCCESS;
}}

jsp:

1
2
3
4
5
6
7
<form action="springmvc/testPOJO" method="post">
username:<input type="text" name="userName"><br><br>
age:<input type="text" name="age"><br><br>
province:<input type="text" name="address.province"><br><br>
city:<input type="text" name="address.city"><br><br>
<input type="submit" value="Submit">
</form>

  注意,jsp中form表单数据的name值需要与POJO的字段值一致。

使用ServletAPI作为参数

  MVC 的Handler 方法可以接受如下ServletAPI类型的参数:
  
   • HttpServletRequest
   • HttpServletResponse
   • HttpSession
   • Java.security.Principal•Locale
   • InputStream
   • OutputStream
   • Reader
   • Writer
  
  例如:
  
jsp:

1
<a href="springmvc/testServletAPI">Test ServletAPI</a>

控制器:

1
2
3
4
5
6
7
8
@RequestMapping("/testServletAPI")
public void testServletAPI(HttpServletRequest request,
HttpServletResponse response, Writer out) throws IOException {
System.out.println("testServletAPI, " + request + ", " + response);
out.write("hello springmvc");
// return SUCCESS;
}
}

点击jsp页面的Test ServletAPI:

image_1b0rv5de613kekkko9u1kqr5289.png-11.6kB

并在控制台输出:

testServletAPI, org.apache.catalina.connector.RequestFacade@3277a570, org.apache.catalina.connector.ResponseFacade@4d0e75f2

关于请求方法为GET的form表单,action属性后不能带参数的问题

发表于 2016-11-06   |   分类于 BUG解决   |  

  若在页面中有如下代码:

1
2
3
4
5
<form action="XXX?name=Jack&age=10">
<input type="text" name="address" value="Beijing"/>
<input type="hidden" name="sex" value="male"/>
<input type="submit" value="submit"/>
</form>

  那么后台无法获取action属性中的name和age参数。这是因为浏览器会将表单中的数据封装为字符串,例如会将上面的address和sex属性封装为address=Beijing&sex=male,然后将其直接附加在action的URL之后,URL和封装后的字符串之间会有一个‘?’分隔。如果在表单的action属性中已经包含了参数,浏览器会直接将其过滤掉,再附加form表单数据。所以请求方法为GET的表单,其action属性中不能携带参数,如果要实现参数的传递,有如下三种方式:
  1、使用POST方式进行传递,可以在action中添加参数;
  2、如果要用GET方式,那么可以采用上述的方式,通过表单数据进行传递,例如:

1
2
<input type="hidden" name="name" value="Jack"/>
<input type="text" name="age" value="10"/>

  3、使用超链接(超链接默认的请求方式为GET):

1
<a href="XXX?name=Jack&age=10">提交</a>

  

在MyEclipse2014环境下搭建SpringMVC并实现HelloWorld

发表于 2016-11-05   |   分类于 SpringMVC   |  

新建项目:

  File — New — Dynamic Web Project — 输入项目名(这里我命名为MySpringMVC-1) — Finish。

搭建环境:

  右键项目 — MyEclipse — Project Facets[Capabilities] — Install spring Facet:
  
  image_1b0p2k00f1o691q47m26163rfp39.png-26.8kB
  
  点击Next:
  
  image_1b0p2u7atob712do15us13ja17e1m.png-43.6kB
  
  取消所有勾选,点击Finish。   

配置web.xml:

打开WebRoot/WEB-INF/web.xml,配置为如下形式:

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    id="WebApp_ID" version="2.5">

    <!-- 配置 DispatcherServlet -->
    <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 该servlet在项目加载时就被创建,而不是等第一次请求时才创建 -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>springDispatcherServlet</servlet-name>
        <!-- 可以应答所有请求 -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

配置SpringMVC的配置文件:

  在WebRoot/WEB-INF目录下新建一个Spring Bean Definition,命名为<servlet-name>-servlet.xml,在我项目的web.xml中,<servlet-name>为springDispatcherServlet,所以此处配置文件名为springDispatcherServlet-servlet.xml:
  
  image_1b0p4h55ug85qepacf4kh1ats1t.png-25.5kB
  
  点击Next:
  
  image_1b0p49b981gtkugr3rh1ah5114j1g.png-50.4kB

  勾选context和mvc,点击Finish,然后打开springDispatcherServlet-servlet.xml,配置如下:
  
springDispatcherServlet-servlet.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">


    <!-- 指定要自动扫描的包 -->
    <context:component-scan base-package="com.MySpringMVC"></context:component-scan>

    <!-- 配置视图解析器: 如何把 handler 方法返回值解析为实际的物理视图 -->
    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

</beans>

  其中指定com.MySpringMVC下的子包和类中的注解将会被扫描到。   

添加控制器类:

  新建包com.MySpringMVC.controller,并在包下新建类Hello.java,代码如下:

Hello.java:

package com.MySpringMVC.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class Hello {

    /**
     * 1. 使用 @RequestMapping 注解来映射请求的 URL
     * 2. 返回值会通过视图解析器解析为实际的物理视图, 对于 InternalResourceViewResolver 视图解析器, 会做如下的解析:
     * 通过 prefix + returnVal + 后缀 这样的方式得到实际的物理视图, 然会做转发操作
     * 
     * /WEB-INF/views/success.jsp
     * 
     * @return
     */
    @RequestMapping("/helloworld")
    public String hello(){
        System.out.println("hello world!");
        return "success";
    }
}

新建页面:

  现在我们来建立页面测试一下:
  
index.jsp: (其中超链接地址“helloworld”对应Hello.java中的“@RequestMapping(“/helloworld”)”)

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <base href="<%=basePath%>">

    <title>My JSP 'index.jsp' starting page</title>
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="This is my page">
  </head>

  <body>
    <a href="helloworld">Hello World!</a>
  </body>
</html>

/WEB-INF/views/success.jsp:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <base href="<%=basePath%>">

    <title>My JSP 'success.jsp' starting page</title>

    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="This is my page">

  </head>

  <body>
    <h4>Hello world!</h4>
  </body>
</html>

运行测试:

  运行项目:
  
  image_1b0p616ha1lc1qgrum51eum18h82a.png-9.9kB
  
  点击,跳转到了WEB-INF/views/success.jsp:
  
  image_1b0p62p3i1kp41em91e71s3vgrf2n.png-11.2kB
  
  并且在控制台输出了hello world!:
  
  image_1b0p63m9m7ce15v71feg1nsn1ls734.png-28.5kB      

     

JVM的类加载机制

发表于 2016-11-03   |   分类于 JVM   |  

  虚拟机把描述类的数据从Class文件(这里所说的Class文件指一串二进制的字节流,无论以何种形式出现都可以)加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。   

类加载的时机:

  从类被加载到虚拟机内存中开始,到卸载出内存为止,类的生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段,这里的“加载”是“类加载”过程的一个阶段,注意区分。其中验证、准备和解析三部分称为连接。这7个阶段顺序如下图所示:
  image_1b0juhg751g961i6p7mf3hkmmi9.png-59.9kB

  其中加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的(即:加载阶段必须在验证阶段开始之前开始,验证阶段必须在准备阶段开始之前开始等。这些阶段都是互相交叉地混合式进行的,通常会在一个阶段的执行过程中调用或激活另一个阶段),解析阶段则不一定,在某些情况下,解析阶段有可能在初始化阶段结束后开始,以支持Java的动态绑定。
  什么时候开始第一个阶段——“加载”,JVM规范没有规定,可由不同的JVM实现自己把握,但是规定了“初始化”阶段的开始时机,下面5种情况下必须立刻进行类的“初始化”:

  1. 遇到new、getstatic、putstatic或invokestatic四条字节码指令时。如果类没有进行过初始化,则需先触发其初始化。这四条指令对应于Java代码就是:使用new实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的字段除外)的时候,及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。
  3. 初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。
  4. 当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。
  5. 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

  对于上述5种情况,JVM规范限定为“有且只有”,这5种场景中的行为成为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。下面举3个例子来说明何为被动引用:(包名统一为classLoading)

例1:

public class SuperClass {
       public static int value = 123;
       static {
           System.out.println("super class init.");
       }
   }

   public class SubClass extends SuperClass {
       static {
           System.out.println("sub class init.");
       }
   }

   public static void main(String[] args) {    
       System.out.println(SubClass.value);
   }

执行后输出的结果如下:

super class init.
123

  程序并不会输出“sub class init.”。通过子类引用父类中的静态字段,不会初始化子类,只会初始化父类。对于静态字段,只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
  
例2:

public static void main(String[] args) {
        SuperClass[] arr = new SuperClass[10];
}

public class SuperClass {
    public static int value = 123;
    static {
        System.out.println("super class init.");
    }
}

  运行程序发现没有输出“super class init.”,说明没有触发SuperClass类的初始化阶段,但这段代码里触发了另一个名为“[LclassLoading.SuperClass”的类的初始化,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。
  
例3:

public class NotInitialization{ 
public static void main(String[] args) {
        System.out.println(ConstClass.Test);
        }
}

public class ConstClass {
    public static final String Test = "Hello world!";

    static {
        System.out.println("const class init.");
    }
}

程序运行结果:
Hello world!

  运行程序只会输出“Hello world!”,而不会输出“const class init.”,证明没有初始化ConstClass类。虽然程序中引用了ConstClass类的常量Test,但是在编译阶段将此常量的值“Hello world!”存储到了NotInitialization类的常量池中,以后NotInitialization对常量Test的引用实际上转化为了NotInitialization类对自身常量池的引用。也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class文件后就不存在任何联系了。

类加载的过程

  JVM中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
  
一、加载:

   “加载”是“类加载”中的一个阶段,在加载阶段,虚拟机需要完成以下三件事。

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

二、验证:

  验证是连接阶段的第一步,这一步主要的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。验证阶段大致分为下面四个阶段:
1、文件格式验证:验证字节流是否符合Class文件格式的规范,并且是否能被当前版本的虚拟机处理(如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内等)。
2、元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范要求。主要目的是对类的元数据信息进行语义检验,保证不存在不符合Java语言规范的元数据信息。
3、字节码验证:整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完全检验后,这个阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
4、符号引用验证:可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

三、准备:

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易混淆的点:
(1)这个时候进行内存分配的仅包含类变量(被Static修饰的变量),则不包括实例变量(实例变量将会在对象实例化时随着对象一起体在Java堆中)。
(2)这里所说的初始值“通常情况”下是数据类型的零值。假设一个类变量定义为:
public static int value = 123;
那么变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会被执行。

四、解析:

  解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
  
符号引用(Symbolic References):符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因此符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用(Direct References):直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

  虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast等16个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
  解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。
  
五、初始化:

  类的初始化阶段是类加载过程的最后一步,在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划区初始化类变量和其他资源,或者可以说,初始化阶段是执行类构造器()方法的过程。

JVM中的内存分配策略

发表于 2016-11-02   |   分类于 JVM   |  

  对象的内存分配,从大方向上将,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况也可能直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
  下面讲解集中最普遍的内存分配规则,并通过代码去验证这些规则。本文中的代码在测试时使用Client模式虚拟机运行,没有手工指定收集器组合,换句话说,验证的是使用Serial/Serial Old收集器下(ParNew/Serial Old组合的规则也基本一致)的内存分配策略。

1、对象优先在Eden分配

  大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。

虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前内存各区域的分配情况。在实际应用中,内存回收日志一般都是打印到文件后通过日志工具进行分析,不过本实验的日志并不多,直接阅读就能看得很清除。代码及运行结果如下:

private static final int _1MB = 1024*1024;

/**
 * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
 */
public static void testAllocation(){
    byte[] allocation1,allocation2,allocation3,allocation4;
    allocation1 = new byte[2*_1MB];
    allocation2 = new byte[2*_1MB];
    allocation3 = new byte[2*_1MB];
    allocation4 = new byte[4*_1MB];//出现一次Minor GC
}

运行结果:

[GC [DefNew: 6487K->152K(9216K), 0.0041641 secs] 6487K->6296K(19456K), 0.0042253 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4412K [0x326e0000, 0x330e0000, 0x330e0000)
eden space 8192K, 52% used [0x326e0000, 0x32b08fe0, 0x32ee0000)
from space 1024K, 14% used [0x32fe0000, 0x330062b0, 0x330e0000)
to space 1024K, 0% used [0x32ee0000, 0x32ee0000, 0x32fe0000)
tenured generation total 10240K, used 6144K [0x330e0000, 0x33ae0000, 0x33ae0000)
the space 10240K, 60% used [0x330e0000, 0x336e0030, 0x336e0200, 0x33ae0000)
compacting perm gen total 12288K, used 375K [0x33ae0000, 0x346e0000, 0x37ae0000)
the space 12288K, 3% used [0x33ae0000, 0x33b3dcb0, 0x33b3de00, 0x346e0000)
ro space 10240K, 54% used [0x37ae0000, 0x3805d9f8, 0x3805da00, 0x384e0000)
rw space 12288K, 55% used [0x384e0000, 0x38b813f8, 0x38b81400, 0x390e0000)

  上述程序的testAllocation()方法中,尝试分配3个2MB和1个4MB的对象,在运行时通过-Xms20M、-Xmx20M和-Xmn10M这3个参数限制Java堆大小为20M,且不可扩展,其中10MB分配给新生代,剩下的10M就分配给老年代了。-XX:SurvivorRatio=8决定了新生代中Eden和Survivor区的空间比例为8:1,从运行结果可以看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)。

执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代由6487K变为152K,而总内存占用了几乎没有减少(因为allocation1,2,3三个对象都是存活的虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配所需的4MB内存时,发现Eden区已经被占用了6MB,剩余空间不足以分配4MB,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB对象无法全部放入Survivor空间(Survivor只有1MB),所以只好通过分配担保机制提前转移到老年代。

这次GC结束后,4MB的allocation4对象被顺利分配到Eden中。因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(allocation1,2,3占用)。

在这里补充一点,Minor GC和Full GC的区别:

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次的Minor GC(但并非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

2、大对象直接进入老年代

  所谓大对象,就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(更加坏的情况就是遇到一群朝生夕死的短命大对象,写程序时应该避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置大对象。

  虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接进入老年代中分配。这样避免在Eden区及两个Survivor区之间发生大量的内存拷贝。我们来看下面的代码和运行结果:

/**
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
     * -XX:PretenureSizeThreshold=3145728
     */
    public static void testPretenureSizeThreshold(){
        byte[] allocation;
        allocation = new byte[4*_1MB];
    }

运行结果:

Heap
def new generation total 9216K, used 671K [0x046a0000, 0x050a0000, 0x050a0000)
eden space 8192K, 8% used [0x046a0000, 0x04747e88, 0x04ea0000)
from space 1024K, 0% used [0x04ea0000, 0x04ea0000, 0x04fa0000)
to space 1024K, 0% used [0x04fa0000, 0x04fa0000, 0x050a0000)
tenured generation total 10240K, used 4096K [0x050a0000, 0x05aa0000, 0x05aa0000)
the space 10240K, 40% used [0x050a0000, 0x054a0010, 0x054a0200, 0x05aa0000)
compacting perm gen total 12288K, used 2130K [0x05aa0000, 0x066a0000, 0x09aa0000)
the space 12288K, 17% used [0x05aa0000, 0x05cb49b8, 0x05cb4a00, 0x066a0000)
No shared spaces configured.
我们可以看到Eden空间几乎没有被利用,,而老年代10MB空间被使用40%,也就是4MB的allocation对象被直接分配到老年代中,这是因为PretenureSizeThreshold被设置为3MB,因此超过3MB的对象都会直接在老年代中进行分配。

3、长期存活的对象将进入老年代

虚拟机采用了分代收集的思想来管理内存,那内存回收时就必须识别哪些对象应该放在新生代,哪些对象应该放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中没熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15)时,就会被晋升到老年代中。对象晋升到老年代的年龄阀值,可以通过参数-XX:MaxTenuringThreshold来设置。我们来看下面的代码和运行结果:

/**
* VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* -XX:MaxTenuringThreshold=1
*/
public static void testTenuringThreshold(){
    byte[] allocation1,allocation2,allocation3;
    allocation1 = new byte[_1MB/4];
    allocation2 = new byte[4*_1MB];
    allocation3 = new byte[4*_1MB];
    allocation3 = null;
    allocation3 = new byte[4*_1MB];
}

当-XX:MaxTenuringThreshold=1时,运行结果:

[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)

  • age 1: 420200 bytes, 420200 total

: 4859K->410K(9216K), 0.0042347 secs] 4859K->4506K(19456K), 0.0042967 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4506K->0K(9216K), 0.0008751 secs] 8602K->4506K(19456K), 0.0009284 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4178K [0x04540000, 0x04f40000, 0x04f40000)
eden space 8192K, 51% used [0x04540000, 0x04954830, 0x04d40000)
from space 1024K, 0% used [0x04d40000, 0x04d40000, 0x04e40000)
to space 1024K, 0% used [0x04e40000, 0x04e40000, 0x04f40000)
tenured generation total 10240K, used 4506K [0x04f40000, 0x05940000, 0x05940000)
the space 10240K, 44% used [0x04f40000, 0x053a6978, 0x053a6a00, 0x05940000)
compacting perm gen total 12288K, used 2137K [0x05940000, 0x06540000, 0x09940000)
the space 12288K, 17% used [0x05940000, 0x05b56580, 0x05b56600, 0x06540000)
No shared spaces configured.

当-XX:MaxTenuringThreshold=15时,运行结果:

[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)

  • age 1: 420200 bytes, 420200 total

: 4859K->410K(9216K), 0.0069588 secs] 4859K->4506K(19456K), 0.0070540 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)

  • age 2: 420056 bytes, 420056 total
    : 4506K->410K(9216K), 0.0012592 secs] 8602K->4506K(19456K), 0.0013433 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    Heap
    def new generation total 9216K, used 4588K [0x044e0000, 0x04ee0000, 0x04ee0000)
    eden space 8192K, 51% used [0x044e0000, 0x048f4830, 0x04ce0000)
    from space 1024K, 40% used [0x04ce0000, 0x04d468d8, 0x04de0000)
    to space 1024K, 0% used [0x04de0000, 0x04de0000, 0x04ee0000)
    tenured generation total 10240K, used 4096K [0x04ee0000, 0x058e0000, 0x058e0000)
    the space 10240K, 40% used [0x04ee0000, 0x052e0010, 0x052e0200, 0x058e0000)
    compacting perm gen total 12288K, used 2137K [0x058e0000, 0x064e0000, 0x098e0000)
    the space 12288K, 17% used [0x058e0000, 0x05af6580, 0x05af6600, 0x064e0000)
    No shared spaces configured.

  此方法中allocation1对象需要256KB的内存空间,Survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后会非常干净地变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时候新生代仍然有410KB的空间被占用。

4、动态对象年龄判定

  为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

/**

 * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 * -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
 */
public static void testTenuringThreshold2(){
    byte[] allocation1,allocation2,allocation3,allocation4;
    allocation1 = new byte[_1MB/4];
    allocation2 = new byte[_1MB/4];
    allocation3 = new byte[4*_1MB];
    allocation4 = new byte[4*_1MB];
    allocation4 = null;
    allocation4 = new byte[4*_1MB];
}

运行结果:

[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)

  • age 1: 682360 bytes, 682360 total

: 5115K->666K(9216K), 0.0068333 secs] 5115K->4762K(19456K), 0.0069434 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 4762K->0K(9216K), 0.0015284 secs] 8858K->4762K(19456K), 0.0016157 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4178K [0x04470000, 0x04e70000, 0x04e70000)
eden space 8192K, 51% used [0x04470000, 0x04884830, 0x04c70000)
from space 1024K, 0% used [0x04c70000, 0x04c70000, 0x04d70000)
to space 1024K, 0% used [0x04d70000, 0x04d70000, 0x04e70000)
tenured generation total 10240K, used 4762K [0x04e70000, 0x05870000, 0x05870000)
the space 10240K, 46% used [0x04e70000, 0x053168f8, 0x05316a00, 0x05870000)
compacting perm gen total 12288K, used 2137K [0x05870000, 0x06470000, 0x09870000)
the space 12288K, 17% used [0x05870000, 0x05a86580, 0x05a86600, 0x06470000)
No shared spaces configured.

  发现运行结果中Survivor占用仍然为0%,而老年代比预期增加了6%,也就是说allocation1,allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来达到了512KB,并且它们是同年的,满足同年对象达到Survivor空间的一半规则。我们只要注释一个对象的new操作,就会发现另外一个不会晋升到老年代了。

5、空间分配担保

  当发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。

  新生代使用复制收集算法,但为了内存利用率,值使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时(最极端就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下去,在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验,与老年代的剩余空间进行对比,决定是否进行Full GC来让老年代腾出更多空间。

  取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存活后的对象突增,远远高于平均值时,依然会导致担保失败(Handle Promotion Failure)。如果出现HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

/**
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
     * -XX:-HandlePromotionFailure -XX:+PrintGCDetails
     */
    public static void testHandlePromotion(){
        byte[] allocation1,allocation2,allocation3,allocation4,allocation5,
            allocation6,allocation7;
        allocation1 = new byte[2*_1MB];
        allocation2 = new byte[2*_1MB];
        allocation3 = new byte[2*_1MB];
        allocation1 = null;
        allocation4 = new byte[2*_1MB];
        allocation5 = new byte[2*_1MB];
        allocation6 = new byte[2*_1MB];
        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        allocation7 = new byte[2*_1MB];
    }

以HandlePromotionFailure=false运行结果:

[GC [DefNew: 6651K->154K(9216K), 0.0033102 secs] 6651K->4250K(19456K), 0.0033813 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 6384K->6384K(9216K), 0.0000288 secs][Tenured: 4096K->4250K(10240K), 0.0039019 secs] 10480K->4250K(19456K), [Perm : 2132K->2132K(12288K)], 0.0040460 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 2211K [0x044e0000, 0x04ee0000, 0x04ee0000)
eden space 8192K, 27% used [0x044e0000, 0x04708fe0, 0x04ce0000)
from space 1024K, 0% used [0x04de0000, 0x04de0000, 0x04ee0000)
to space 1024K, 0% used [0x04ce0000, 0x04ce0000, 0x04de0000)
tenured generation total 10240K, used 4250K [0x04ee0000, 0x058e0000, 0x058e0000)
the space 10240K, 41% used [0x04ee0000, 0x053068a8, 0x05306a00, 0x058e0000)
compacting perm gen total 12288K, used 2137K [0x058e0000, 0x064e0000, 0x098e0000)
the space 12288K, 17% used [0x058e0000, 0x05af6710, 0x05af6800, 0x064e0000)
No shared spaces configured.

以HandlePromotionFailure=true运行结果:

[GC [DefNew: 6651K->154K(9216K), 0.0038289 secs] 6651K->4250K(19456K), 0.0038877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 6384K->154K(9216K), 0.0006008 secs] 10480K->4250K(19456K), 0.0006525 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 2366K [0x043d0000, 0x04dd0000, 0x04dd0000)
eden space 8192K, 27% used [0x043d0000, 0x045f8fe0, 0x04bd0000)
from space 1024K, 15% used [0x04bd0000, 0x04bf6888, 0x04cd0000)
to space 1024K, 0% used [0x04cd0000, 0x04cd0000, 0x04dd0000)
tenured generation total 10240K, used 4096K [0x04dd0000, 0x057d0000, 0x057d0000)
the space 10240K, 40% used [0x04dd0000, 0x051d0020, 0x051d0200, 0x057d0000)
compacting perm gen total 12288K, used 2137K [0x057d0000, 0x063d0000, 0x097d0000)
the space 12288K, 17% used [0x057d0000, 0x059e6710, 0x059e6800, 0x063d0000)
No shared spaces configured.

JVM垃圾收集器总结

发表于 2016-11-02   |   分类于 JVM   |  

  我们在《JVM中的垃圾收集算法》中介绍了垃圾收集算法,如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,所以不同的厂商、不同版本的虚拟机可以根据需要提供不同的收集器。我们这里讨论的收集器是基于JDK1.7 Update 14之后的HotSpot虚拟机,其包含的所有收集器如图1所示:

image_1b0ig2cvncat1l5s96ahaj11e89.png-71.7kB

          图1 HotSpot虚拟机的垃圾收集器
          
  图中上下两个区域分别代表收集器是属于新生代收集器还是老年代收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
  
  另外,在开始下面的介绍之前,我们有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,他们可以解释为:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),
用户程序继续运行,而垃圾收集程序运行于另一个CPU上。

Serial(串行GC)收集器:

  Serial收集器是一个新生代收集器,单线程执行,使用复制算法。其中“单线程”的意义不仅仅说明它只会使用一个CPU或者一条收集线程区完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,即“Stop The World”,这好比你妈妈在给你打扫房间的时候,你必须老老实实地坐在椅子上,而不能她一边打扫,你还一边乱扔纸屑。Serial收集器是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。其工作过程如图2所示:

image_1b0ih2iq21lf51mk8fhvbfi8713.png-53.5kB

          图2 Serial/Serial Old收集器运行示意图

ParNew(并行GC)收集器:

  ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。其工作过程如图3所示:
  
  image_1b0ih6vgtfsq1shuokhgnjkc81g.png-60.3kB

          图3 ParNew/Serial Old收集器运行示意图           

Parallel Scavenge(并行GC)收集器:

  Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。
  parallel Scavenge收集器看上去与ParNew一样,其特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Serial Old(串行GC)收集器:

  Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法,其工作过程如上面图2所示。

Parallel Old(并行GC)收集器:

  Parallel Old是Parallel
Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS(并发GC)收集器:

  CMS(Concurrent Mark Sweep)收集器是基于“标记 - 清除”算法的并发收集器,其设计目标为获取最短回收停顿时间。它的运作过程比上面介绍的收集器都要复杂一些,整个过程分为四个步骤,包括:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

  初始标记和重新标记仍需暂停所有用户线程,即“Stop the World”。初始标记只是标记GC Roots能直接关联的对象,速度很快。并发标记阶段,用户线程与标记线程并发,就是进行GC Roots Tracing的过程。而重新标记则只是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。整个收集过程中耗时最久的并发标记和并发清除则和用户线程一起工作,所以总地来讲,CMS中GC线程是和用户线程一起并发执行的。其工作过程如图4所示:
  
  image_1b0ii8hdh1qlp1ob81kdg1fgh12nu2n.png-98.7kB
  
          图4:CMS收集器运行示意图

  CMS是一款突破性的收集器,它采用并发收集,极大地缩短了用户线程停顿时间。但同时,CMS还是具有一些缺陷:
1、对CPU资源非常敏感。几乎所有的并行/并发系统都对CPU敏感。虽然它很少导致用户卡顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量变低。
2、无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在同时执行,因此此时这些线程产生的这部分垃圾CMS无法处理,只好留在下一次GC时再清理,这一部分垃圾 就被称为“浮动垃圾”。因为在垃圾收集阶段用户线程还在运行,因此CMS需要预留足够的空间供这些线程使用,而不能向其他收集器那样等老年代几乎被完全充 满时再进行回收。默认CMS收集器在老年代使用68%之后就被激活。
3、这个缺点来自于CMS所采用的“标记 - 清除”算法。这种方式容易产生大量碎片,当碎片过多时,容易出现老年代空间有很大剩余,但找不到连续空间进行分配给大对象,从而不得不提前触发一次GC。

G1 收集器:

  G1(Garbage First)收集器是当前收集器技术发展的最前沿成果,它与其他GC收集器相比,具有如下特点:
1、并行+并发。可充分利用CPU资源;
2、分代收集;
3、G1从整体看是“标记-整理”算法,从局部(两个Region之间)看,是“复制”算法。 不会产生空间碎片;
4、可预测的停顿。建立可预测的态度时间模型,能让使用者明确指定在一个长度为M毫秒的时间内,消耗在垃圾收集的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

  G1之前的收集器是进行收集的范围是整个新生代或 老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region)。G1跟踪这些Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据优先级从列表中挑选回收价值最大的Region进行回收(这也就是Garbage First名称的由来)。其工作过程类似于CMS,分为如下四个步骤:   

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

  前三个步骤与CMS类似,最后的筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

JVM中的垃圾收集算法

发表于 2016-11-02   |   分类于 JVM   |  

1、标记-清除算法:

  标记-清除算法是最基础的垃圾收集算法,它分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。关于标记过程,我们在《对象死亡的过程》中已经叙述过。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。此种算法有两个不足之处:一是效率问题,标记和清除两个过程效率都不高;二是空间问题,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除算法的执行过程如图1所示:
  image_1b0i6md5a1vavd01akg1vkso9b9.png-39.1kB
  
               图1:标记-清除算法                

2、复制算法:

  为了提高效率,复制算法便出现了,它的基本思想是:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。由于每次只对一个半区进行回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。这种算法实现简单,运行高效,缺点也显而易见:可使用的内存降为了原来的一半。它的执行流程如图2所示:
  image_1b0ie1a66ehl1cgvme256b12fb1g.png-36.2kB
  
               图2:复制算法
               
  复制算法主要用于新生代对象的回收,这是因为新生代中的对象98%是朝生夕死的,所以我们并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当然,我们无法保证每次回收都只有不多于10%的对象存活,这就需要其他内存(这里指老年代)在Survivor空间不够用的情况下进行分配担保。内存的分配担保是指:如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。   

3、标记-整理算法:

  当对象存活率较高时,复制算法便需要进行较多次的复制操作,效率会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,有人提出了另外一种“标记-整理”算法,其主要思想是:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法的执行流程如图3所示:
  image_1b0idfjif1vi31ug617nk140eb1d13.png-40.2kB
  
               图3:标记-整理算法
               
  补充一点,为什么新生代不使用标记-整理算法呢?这是因为一致性问题,由于在整理的过程中,标记不能变动,这就相当于虚拟机要暂停,等待内存整理完毕再运行,而整理过程还是挺耗时的。但是,如果对象存活的时间很长,存活率很高,每次清理都只有少部分对象死亡(这正是老年代的特点),那么,这种算法消耗的时间会大大减少。   

4、分代收集算法:

  分代收集算法不是什么新的思想,只是根据我们上面的叙述,对新生代和老年代的对象采取不同的收集算法而已。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

Java内存区域

发表于 2016-11-02   |   分类于 JVM   |  

  Java虚拟机在执行Java程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域,称为运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区,如下图所示。
  
  image_1b0hsh9m92i31t8oacfvqe4njm.png-65.8kB      

程序计数器:
  程序计数器是一块较小的内存空间,可以被看作是当前线程所执行的字节码的行号指示器,分支、循环、跳转、异常处理和线程恢复等基础功能都需要依赖这个计算器来完成。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线成中的指令。因为,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储(我们称这类内存区域为线程私有的内存,反之称为线程共享的内存,上图说明了运行时内存区域中哪些是线程私有的区域,哪些是线程共享的区域)。
  由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
  
Java虚拟机栈:
  Java虚拟机栈,或者说其中的局部变量表,就是我们常说的栈。Java虚拟机栈描述的是Java方法执行的内存模型,每个方法被执行时都会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用直到执行完成,对应着一个栈帧在Java虚拟机栈中入栈到出栈的过程。
  局部变量表,顾名思义,就是用来存储方法中的局部变量的,它存放着编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double、对象引用以及returnAddress)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法所需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  对这个区域,JVM规范规定了两种异常状况:
1、StackOverflowError异常:线程请求的栈深度大于虚拟机所允许的深度;
2、OutOfMemoryError异常:在虚拟机栈可以动态扩展的情况下,如果扩展时无法申请到足够的内存。

本地方法栈:
  本地方法栈与Java虚拟机栈作用非常相似,区别仅在于虚拟机栈为Java方法(也就是字节码)服务,而本地方法则为Native方法服务。
  
Java堆:
  Java堆在大多数应用中是JVM所管理的内存中最大的一块区域,此内存区域的唯一目的就是存放对象实例。对于可扩展的JVM实现,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  
方法区:
  方法区用来存储已被虚拟机加载的类信息(Class文件)、常量(final)、静态变量(static)、即时编译器编译后的代码等数据。
  运行时常量池:运行时常量池是方法区的一部分。Class文件中有一项信息叫做常量池,其中存放着编译期生成的各种字面量和符号引用,当类加载后,Class文件中常量池的内容就进入方法区的运行时常量池中存放,除此之外,由符号引用翻译出的直接引用也会存储在运行时常量池。
  当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
  
补充:
  在上述五个内存区域中,程序计数器、虚拟机栈和本地方法栈这3个区域随线程而生,随线程而灭,方法结束或者线程结束时,内存自然就跟着回收了,所以这几个区域的内存分配和回收都具备确定性,不需要过多地考虑回收的问题。而Java堆和方法区则不一样,它们是线程共享的内存区域,我们只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的也都是这部分内存。

1…4567
Alan

Alan

Better late than never.

61 日志
11 分类
74 标签
GitHub weibo
© 2016 Alan
由 Hexo 强力驱动
主题 - NexT.Pisces