java面试题六

来源:互联网 发布:实战linux编程精髓 编辑:程序博客网 时间:2024/05/16 10:36

51、 什么时候使用字节流、什么时候使用字符流,二者的区别

在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。


InputStream 和OutputStream,两个是为
字节流设计的,主要用来处理字节或二进制对象,
Reader和Writer.两个是为
字符流(一个字符占两个字节)设计的,主要用来处理字符或字符串.
 
字符流处理的单元为2个字节的Unicode字符,操作字符、字符数组或字符串,
字节流处理单元为1个字节,操作字节和字节数组。
所以字符流是由
Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,
所以它对多国语言支持性比较好!
如果是音频文件、图片、歌曲,就用字节流好点,
如果是关系到中文(文本)的,用字符流好点


所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取以形成字节序列
字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;
字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以
字节流是最基本的,所有的InputStrem和OutputStream的子类都是,主要用在处理二进制数据,它是按字节来处理的 
但实际中很多的数据是文本,
又提出了字符流的概念,
它是按虚拟机的encode来处理,也就是要进行字符集的转化 
这两个之间通过InputStreamReader,OutputStreamWriter来关联,
实际上是通过byte[]和String来关联 
在实际开发中出现的汉字问题实际上都是在字符流和字节流之间转化不统一而造成的 

Reader类的read()方法返回类型为int :作为整数读取的字符(占两个字节共16位),范围在 0 到 65535 之间(0x00-0xffff),如果已到达流的末尾,则返回-1

inputStream的read()虽然也返回int,但由于此类是面向字节流的,一个字节占8个位,所以返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值-1。因此对于不能用0-255来表示的值就得用字符流来读取!比如说汉字.

字节流和字符流的主要区别是什么呢?

          一.字节流在操作时不会用到缓冲区(内存),是直接对文件本身进行操作的。而字符流在操作时使用了缓冲区,通过缓冲区再操作文件。

 

          二.在硬盘上的所有文件都是以字节形式存在的(图片,声音,视频),而字符值在内存中才会形成。

 

 

    上面两点能说明什么呢?

       针对第一点,

      我们知道,如果一个程序频繁对一个资源进行IO操作,效率会非常低。此时,通过缓冲区,先把需要操作的数据暂时放入内存中,以后直接从内存中读取数据,则可以避免多次的IO操作,提高效率

      针对第二点,

      真正存储和传输数据时都是以字节为单位的,字符只是存在与内存当中的,所以,字节流适用范围更为宽广

52、session和cookie的区别和联系,session的生命周期,多个服务部署时session管理

Session和Cookie的区别

对象

信息量大小

保存时间

应用范围

保存位置

Session

小量,简单的数据

用户活动时间+一段延迟时间(一般为20分钟)

单个用户

服务器端

Cookie

小量,简单的数据

可以根据需要设定

单个用户

客户端

1.1 Session对象 

浏览器访问服务器时,服务器会创建一个对象(该对象也称为session对象,该对象有一个唯一的id号与其对应)。然后
,服务器会将id号发送给浏览器(默认情况下,使用cookie机制发送)。当浏览器再次访问服务器时,会将id号发送过
来。服务器可以依据id号找到对应的session对象。通过这个session对象,来保存状态。 
1.1.1 session在不同环境下的不同含义 
  session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂
断电话这中间的一系列过程可以称之为一个session。 
然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义。 
  session在Web开发环境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器端之间保持状态的解决
方案。有时候Session也用来指这种解决方案的存储结构。 
1.1.2 保存session id的几种方式 
A.保存session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给服务器。 
如果客户端支持Cookie,Web Server在返回Response的时候,在Response的Header部分,加入一个“set-cookie: 
jsessionid=XXXX”header属性,把jsessionid放在Cookie里传到客户端。 
B.由于cookie可以被人为的禁止,必须有其它的机制以便在cookie被禁止时仍然能够把session id传递回服务器,经常
采用的一种技术叫做URL重写,就是把session id附加在URL路径的后面,附加的方式也有两种,一种是作为URL路径的附
加信息,另一种是作为查询字符串附加在URL后面。网络在整个交互过程中始终保持状态,就必须在每个客户端可能请求
的路径后面都包含这个session id。 
C.另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session 
id传递回服务器。 
1.1.3 URL重写有什么缺点 
   对所有的URL使用URL重写,包括超链接,form的action,和重定向的URL。每个引用你的站点的URL,以及那些返
回给用户的URL(即使通过间接手段,比如服务器重定向中的Location字段)都要添加额外的信息。 
   这意味着在你的站点上不能有任何静态的HTML页面(至少静态页面中不能有任何链接到站点动态页面的链接)。因
此,每个页面都必须使用servlet或JSP动态生成。即使所有的页面都动态生成,如果用户离开了会话并通过书签或链接
再次回来,会话的信息都会丢失,因为存储下来的链接含有错误的标识信息-该URL后面的SESSION ID已经过期了。 
1.1.4 使用隐藏的表单域有什么缺点 
仅当每个页面都是有表单提交而动态生成时,才能使用这种方法。单击常规的<A HREF..>超文本链接并不产生表单提交
,因此隐藏的表单域不能支持通常的会话跟踪,只能用于一系列特定的操作中,比如在线商店的结账过程。
1.1.5 session什么时候被创建 
一个常见的错误是以为session在有客户端访问时就被创建,然而事实是直到某server端程序(如Servlet)调用
HttpServletRequest.getSession(true)这样的语句时才会被创建。 
1.1.6 session何时被删除 
session在下列情况下被删除: 
A.程序调用HttpSession.invalidate() 
B.距离上一次收到客户端发送的session id时间间隔超过了session的最大有效时间 
C.服务器进程被停止 
再次注意关闭浏览器只会使存储在客户端浏览器内存中的session cookie失效,不会使服务器端的session对象失效。 
1.2 Cookie对象 
浏览器向服务器发送请求时,服务器会将少量的数据返回给浏览器(该数据以set-cookie消息头的形式返回给浏览器)
,浏览器会将这些数据存放到硬盘或者内存上。当浏览器下次再次访问服务器时,会将之前存放的数据发送给服务器(
以cookie消息头的形式发送给服务器)。通过这种方式,就可以记录浏览器与服务器之间交互的数据,也就是状态。 
1.2.1 会话cookie和持久cookie的区别 
如果不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生
命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。 
  如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过
设定的过期时间。 
  存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏
览器有不同的处理方式。 
2.Session和Cookie的关系 
这里用一个形象的比喻来解释session的工作方式。假设 
Web Server:是一个商场的存包处
HTTP Request:是一个顾客
Session:商场的存包处的柜子
Session ID:存包号码牌
Cookie:客户随身携带不离身的钱包
情况一:一个顾客(HTTP Request),第一次来到存包处(Web Server),管理员把顾客的物品存放在某一个柜子里面
(Session),然后把一个号码牌(Session ID)交给这个顾客,作为取包凭证。 
情况二:顾客(HTTP Request)下一次来的时候,就要把号码牌(Session ID)交给存包处(Web Server)的管理员。
管理员根据号码牌(Session ID)找到相应的柜子(Session),根据顾客(HTTP Request)的请求,存包处(Web 
Server)可以取出、更换、添加柜子(Session)中的物品,存包处(Web Server)也可以让顾客(HTTP Request)的号
码牌和号码牌对应的柜子(Session)失效。 
情况三:顾客(HTTP Request)的忘性很大,管理员在顾客回去的时候(HTTP Response)都要重新提醒顾客记住自己的
号码牌(Session ID)。这样,顾客(HTTP Request)下次来的时候,就又带着号码牌回来了。 
情况四:客户(HTTP Request)把拿到的号码牌(Session ID)放到随身携带不离身的钱包(Cookie)中。 
3.Session和Cookie的应用 
3.1 如何利用实现自动登录 
当用户在某个网站注册后,就会收到一个惟一用户ID的cookie。客户后来重新连接时,这个用户ID会自动返回,服务器
对它进行检查,确定它是否为注册用户且选择了自动登录,从而使用户务需给出明确的用户名和密码,就可以访问服务
器上的资源。 
3.2 如何根据用户的爱好定制站点 
网站可以使用cookie记录用户的意愿。对于简单的设置,网站可以直接将页面的设置存储在cookie中完成定制。然而对
于更复杂的定制,网站只需仅将一个惟一的标识符发送给用户,由服务器端的数据库存储每个标识符对应的页面设置。 
3.3 cookie的发送 
    1) 创建Cookie对象 
    2) 设置最大时效 
    3) 将Cookie放入到HTTP响应报头 
如果你创建了一个cookie,并将他发送到浏览器,默认情况下它是一个会话级别的cookie:存储在浏览器的内存中,用户
退出浏览器之后被删除。如果你希望浏览器将该cookie存储在磁盘上,则需要使用maxAge,并给出一个以秒为单位的时
间。将最大时效设为0则是命令浏览器删除该cookie。 
发送cookie需要使用HttpServletResponse的addCookie方法,将cookie插入到一个Set-Cookie HTTP请求报头中。由于
这个方法并不修改任何之前指定的Set-Cookie报头,而是创建新的报头,因此我们将这个方法称为是addCookie,而非
setCookie。同样要记住响应报头必须在任何文档内容发送到客户端之前设置。 
Java代码  收藏代码
Cookie cookie = new Cookie("duanqftest", "22222");   
cookie.setDomain("172.20.40.73");    
cookie.setMaxAge(60000);    
cookie.setPath("/");    
response.addCookie(cookie);   
javax.servlet.http.Cookie[] diskCookies = request.getCookies();    
response.sendRedirect("ReadCookie");  
3.4 cookie的读取 
1) 调用request.getCookie 
要获取有浏览器发送来的cookie,需要调用HttpServletRequest的getCookies方法,这个调用返回Cookie对象的数组,
对应由HTTP请求中Cookie报头输入的值。 
2) 对数组进行循环,调用每个cookie的getName方法,直到找到感兴趣的cookie为止 
cookie与你的主机(域)相关,而非你的servlet或JSP页面。因而,尽管你的servlet可能只发送了单个cookie,你也可能
会得到许多不相关的cookie。 
Java代码  收藏代码
String cookieName = “userID”;  
Cookie cookies[] = request.getCookies();  
if (cookies!=null)  
{  
    for(int i=0;i<cookies.length;i++)  
    {  
        Cookie cookie = cookies[i];  
        if (cookieName.equals(cookie.getName()))  
            doSomethingWith(cookie.getValue()); 
    }  
}  
3.5 如何使用cookie检测初访者 
    A.调用HttpServletRequest.getCookies()获取Cookie数组 
    B.在循环中检索指定名字的cookie是否存在以及对应的值是否正确 
    C.如果是则退出循环并设置区别标识 
    D.根据区别标识判断用户是否为初访者从而进行不同的操作 
3.6 使用cookie检测初访者的常见错误 
不能仅仅因为cookie数组中不存在在特定的数据项就认为用户是个初访者。如果cookie数组为null,客户可能是一个初
访者,也可能是由于用户将cookie删除或禁用造成的结果。 
但是,如果数组非null,也不过是显示客户曾经到过你的网站或域,并不能说明他们曾经访问过你的servlet。其它
servlet、JSP页面以及非Java Web应用都可以设置cookie,依据路径的设置,其中的任何cookie都有可能返回给用户的
浏览器。 
正确的做法是判断cookie数组是否为空且是否存在指定的Cookie对象且值正确。 
3.7 使用cookie属性的注意问题 
属性是从服务器发送到浏览器的报头的一部分;但它们不属于由浏览器返回给服务器的报头。  
  因此除了名称和值之外,cookie属性只适用于从服务器输出到客户端的cookie;服务器端来自于浏览器的cookie并
没有设置这些属性。  
  因而不要期望通过request.getCookies得到的cookie中可以使用这个属性。这意味着,你不能仅仅通过设置cookie
的最大时效,发出它,在随后的输入数组中查找适当的cookie,读取它的值,修改它并将它存回Cookie,从而实现不断改
变的cookie值。 
3.8 如何使用cookie记录各个用户的访问计数 
    1) 获取cookie数组中专门用于统计用户访问次数的cookie的值 
    2) 将值转换成int型 
    3) 将值加1并用原来的名称重新创建一个Cookie对象 
    4) 重新设置最大时效 
    5) 将新的cookie输出 
3.9 会话跟踪的基本步骤 
    1) 访问与当前请求相关的会话对象 
    2) 查找与会话相关的信息 
    3) 存储会话信息 
    4) 废弃会话数据 
3.10 getSession()/getSession(true)、getSession(false)的区别 
getSession()/getSession(true):当session存在时返回该session,否则新建一个session并返回该对象。 
getSession(false):当session存在时返回该session,否则不会新建session,返回null

 

53、关于Servlet对象的线程安全问题:

      

              1、什么时候需要考虑线程安全问题?

                     *多线程并发的环境下

                     *有共享的数据

                     *涉及到修改操作

             

              2、怎么解决线程安全问题?

                     *能够使用局部变量代替实例变量,尽量使用局部变量

                     *如果必须使用实例变量,可以创建多个对象,或者我们叫做多例,一个线程一个对象,这样实例变量的内存也是不共享的,保证线程安全问题

                     *如果必须使用单实例,这个时候就需要使用线程同步机制,使用线程同步机制会使吞吐量降低,用户体验差,因为用户排队了。【synchronized】

             

              3、Servlet线程安全问题:

                     *Servlet对象是单实例的,并且在多线程的环境下运行。【注意:Servlet不可能变成多例,因为这个对象是服务器创建的,只创建了一次】

                     *当一个Servlet对象中存在实例变量的时候,多线程对该实例变量进行修改操作的时候必然存在线程安全问题。

             

              4、怎么保证Servlet在多线程并发的环境下是安全的?

                     *在Servlet中不建议使用实例变量,尽量使用局部变量代替实例变量

                     *如果必须使用实例变量,为了保证线程安全问题,那么就需要使用线程同步机制了。

                     *在使用线程同步机制的时候,尽可能让同步的代码块“小”。

54、Servlet对象的生命周期是怎样的一个过程?

- 当服务器启动的时候,会解析web.xml文件,将web.xml文件中的“请求路径”和对应要执行的“Servlet完整类名”绑定到Map集合当中。【map1】

                     -当用户发送请求的时候:http://localhost:8080/x/test/lifyCycle

                     -服务器截获请求路径:/test/lifyCycle,从web容器中查找该“请求路径”对应的“Servlet对象”【这里的查找是从ServletCache当中找】:

                            -找到了:

                                   -直接调用该Servlet对象的service方法处理请求

                            -没找到:

                                   -从map1中找到/test/lifyCycle路径对应的完整Servlet类名

                                   -通过反射机制调用该Servlet类的无参数构造方法完成对象的实例化

                                   -将创建之后的Servlet对象存储到ServletCache当中

                                   -web容器调用该Servlet对象的init方法完成初始化操作

                                   -web容器调用该Servlet对象的service方法处理请求

                     -当用户长时间没有再访问该Servlet对象,或者服务器关闭的时候,或者项目重新部署的时候,WEB容器会自动调用该Servlet对象的destroy方法,

                     做销毁之前的准备工作,当一个destroy方法调用之后,不久,Servlet对象内存被释放。

55、jsp和servlet的区别

JSP本质上和Servlet是完全相同的,只不过它们的职责不同,Servlet主要负责,数据收集,业务处理,JSP主要是将数据展示到页面上。

              * Servlet:收集数据

              * Jsp:展示数据

              但是本质上:Servlet类和JSP翻译的类都继承HttpServlet。都有init、service、destroy方法。

              Servlet和Jsp的生命周期完全相同。

              Servlet和Jsp都是单实例多线程环境下运行的java对象。

Jsp的执行原理?

              访问xxx.jsp文件的时候,WEB服务器将xxx.jsp文件翻译生成xxx_jsp.java源程序,该类是一个Servlet,继承HttpServlet

              xxx_jsp.java编译生成xxx_jsp.class字节码文件,创建对象,调用service方法,这个过程省略了,和Servlet是完全相同的。

              那么什么时候JSP会被重新翻译呢?当JSP文件的最后修改时间发生改变之后,JSP文件会被重新翻译。并且重新编译。

56、java内存模型及GC原理

JVM内存模型中分两大块,一块是 NEW Generation, 另一块是Old Generation. 在New Generation中,有一个叫Eden的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from,to), 它们用来存放每次垃圾回收后存活下来的对象。在Old Generation中,主要存放应用程序中生命周期长的内存对象,还有个PermanentGeneration,主要用来放JVM自己的反射对象,比如类对象和方法对象等。

在New Generation块中,垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个Survivor Space, 当Survivor Space空间满了后, 剩下的live对象就被直接拷贝到Old Generation中去。因此,每次GC后,Eden内存块会被清空。在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求.
垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收NEW中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

 

1,out of memory 只发生在jvm对old和perm generation 回收后还不能获足够内存的情况.

当一个URL被访问时,内存申请过程如下:
A. JVM会试图为相关Java对象在Eden中初始化一块内存区域
B. 当Eden空间足够时,内存申请结束。否则到下一步
C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收), 释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
D. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
E. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
F. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”

 

造成full gc的原因:

new了很多对象,没有即时在主动释放掉->Eden内存不够用->不断把对象往old迁移->old满了->full gc

full gc 如何预防,:

1,使用了缓存

      访问有两种,第一种是缓存命中率不高的访问,第二种种是缓存命中率很高的访问.对于第一种情况,就没必要缓存了,缓存反而效果不好,浪费内存,没有提升程序效率还浪费空间,特别是如果这种访问量级别很大的时候还会导致full gc.第二种情况,不得不缓存很多对象,不缓存的话就要调用数据库或者其它是要发生io的,所以这时候要不就是想办法减少缓存对象的大小,例如不缓存没必要缓存的数据,或者合并一些数据减少内存的使用.如果还是不行那就加机器,加内存.

       总结:在不影响功能的情况下,缓存对象越小越要,命中率越高越好.低命中率的缓存对象还不如不缓存.

2,没使用缓存的情况,貌似不会出现full gc的情况,除非内存太小,或者设置不对,程序有漏洞.

57、jvm性能调优都做了什么?

主要调优的目的:

控制GC的行为.GC是一个后台处理,但是它也是会消耗系统性能的,因此经常会根据系统运行的程序的特性来更改GC行为

控制JVM堆栈大小.一般来说,JVM在内存分配上不需要你修改,(举例)但是当你的程序新生代对象在某个时间段产生的比较多的时候,就需要控制新生代的堆大小.同时,还要需要控制总的JVM大小避免内存溢出

控制JVM线程的内存分配.如果是多线程程序,产生线程和线程运行所消耗的内存也是可以控制的,需要通过一定时间的观测后,配置最优结果

一、JVM内存模型及垃圾收集算法

 1.根据Java虚拟机规范,JVM将内存划分为:

·        New(年轻代)

·        Tenured(年老代)

·        永久代(Perm)

  其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,有虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。

 

·        年轻代(New):年轻代用来存放JVM刚分配的Java对象

·        年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代

·        永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间。

New又分为几个部分:

·        Eden:Eden用来存放JVM刚分配的对象

·        Survivor1

·        Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。

 2.垃圾回收算法

  垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:

·        Serial算法(单线程)

·        并行算法

·        并发算法

  JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。

  稍微解释下的是,并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。所以,并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。

 

  还有一个问题是,垃圾回收动作何时执行?

·        当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC

·        当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代

·        当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

  另一个问题是,何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出

·        JVM98%的时间都花费在内存回收

·        每次回收的内存小于2%

  满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。

 

二、内存泄漏及解决方法

 1.系统崩溃前的一些现象:

·        每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s

·        FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC

·        年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

 之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值。

 

 2.生成堆的dump文件

 通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

 

 3.分析dump文件

 下面要考虑的是如何打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux。当然我们可以借助X-Window把Linux上的图形导入到Window。我们考虑用下面几种工具打开该文件:

1.    Visual VM

2.    IBM HeapAnalyzer

3.    JDK 自带的Hprof工具

 使用这些工具时为了确保加载速度,建议设置最大内存为6G。使用后发现,这些工具都无法直观地观察到内存泄漏,Visual VM虽能观察到对象大小,但看不到调用堆栈;HeapAnalyzer虽然能看到调用堆栈,却无法正确打开一个3G的文件。因此,我们又选用了Eclipse专门的静态内存分析工具:Mat。

 

 4.分析内存泄漏

 通过Mat我们能清楚地看到,哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系。针对本案,在ThreadLocal中有很多的JbpmContext实例,经过调查是JBPM的Context没有关闭所致。

 另,通过Mat或JMX我们还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。

 

 5.回归问题

   Q:为什么崩溃前垃圾回收的时间越来越长?

   A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据

   Q:为什么Full GC的次数越来越多?

   A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

   Q:为什么年老代占用的内存越来越大?

   A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代

 

三、性能调优

 除了上述内存泄漏外,我们还发现CPU长期不足3%,系统吞吐量不够,针对8core×16G、64bit的Linux服务器来说,是严重的资源浪费。

 在CPU负载不足的同时,偶尔会有用户反映请求的时间过长,我们意识到必须对程序及JVM进行调优。从以下几个方面进行:

·        线程池:解决用户响应时间长的问题

·        连接池

·        JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量

·        程序算法:改进程序逻辑算法提高性能

  1.Java线程池(java.util.concurrent.ThreadPoolExecutor)

    大多数JVM6上的应用采用的线程池都是JDK自带的线程池,之所以把成熟的Java线程池进行罗嗦说明,是因为该线程池的行为与我们想象的有点出入。Java线程池有几个重要的配置参数:

·        corePoolSize:核心线程数(最新线程数)

·        maximumPoolSize:最大线程数,超过这个数量的任务会被拒绝,用户可以通过RejectedExecutionHandler接口自定义处理方式

·        keepAliveTime:线程保持活动的时间

·        workQueue:工作队列,存放执行的任务

    Java线程池需要传入一个Queue参数(workQueue)用来存放执行的任务,而对Queue的不同选择,线程池有完全不同的行为:

·        SynchronousQueue: 一个无容量的等待队列,一个线程的insert操作必须等待另一线程的remove操作,采用这个Queue线程池将会为每个任务分配一个新线程

·        LinkedBlockingQueue : 无界队列,采用该Queue,线程池将忽略 maximumPoolSize参数,仅用corePoolSize的线程处理所有的任务,未处理的任务便在LinkedBlockingQueue中排队

·        ArrayBlockingQueue: 有界队列,在有界队列和 maximumPoolSize的作用下,程序将很难被调优:更大的Queue和小的maximumPoolSize将导致CPU的低负载;小的Queue和大的池,Queue就没起动应有的作用。

    其实我们的要求很简单,希望线程池能跟连接池一样,能设置最小线程数、最大线程数,当最小数<任务<最大数时,应该分配新的线程处理;当任务>最大数时,应该等待有空闲线程再处理该任务。

    但线程池的设计思路是,任务应该放到Queue中,当Queue放不下时再考虑用新线程处理,如果Queue满且无法派生新线程,就拒绝该任务。设计导致“先放等执行”、“放不下再执行”、“拒绝不等待”。所以,根据不同的Queue参数,要提高吞吐量不能一味地增大maximumPoolSize。

    当然,要达到我们的目标,必须对线程池进行一定的封装,幸运的是ThreadPoolExecutor中留了足够的自定义接口以帮助我们达到目标。我们封装的方式是:

·        以SynchronousQueue作为参数,使maximumPoolSize发挥作用,以防止线程被无限制的分配,同时可以通过提高maximumPoolSize来提高系统吞吐量

·        自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否可以执行新Task,如果可以把拒绝的Task重新放入到线程池,检查的时间依赖keepAliveTime的大小。

 2.连接池(org.apache.commons.dbcp.BasicDataSource)

    在使用org.apache.commons.dbcp.BasicDataSource的时候,因为之前采用了默认配置,所以当访问量大时,通过JMX观察到很多Tomcat线程都阻塞在BasicDataSource使用的Apache ObjectPool的锁上,直接原因当时是因为BasicDataSource连接池的最大连接数设置的太小,默认的BasicDataSource配置,仅使用8个最大连接。

    我还观察到一个问题,当较长的时间不访问系统,比如2天,DB上的Mysql会断掉所以的连接,导致连接池中缓存的连接不能用。为了解决这些问题,我们充分研究了BasicDataSource,发现了一些优化的点:

·        Mysql默认支持100个链接,所以每个连接池的配置要根据集群中的机器数进行,如有2台服务器,可每个设置为60

·        initialSize:参数是一直打开的连接数

·        minEvictableIdleTimeMillis:该参数设置每个连接的空闲时间,超过这个时间连接将被关闭

·        timeBetweenEvictionRunsMillis:后台线程的运行周期,用来检测过期连接

·        maxActive:最大能分配的连接数

·        maxIdle:最大空闲数,当连接使用完毕后发现连接数大于maxIdle,连接将被直接关闭。只有initialSize < x < maxIdle的连接将被定期检测是否超期。这个参数主要用来在峰值访问时提高吞吐量。

·        initialSize是如何保持的?经过研究代码发现,BasicDataSource会关闭所有超期的连接,然后再打开initialSize数量的连接,这个特性与minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis一起保证了所有超期的initialSize连接都会被重新连接,从而避免了Mysql长时间无动作会断掉连接的问题。

  3.JVM参数

    在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:

·        GC的时间足够的小

·        GC的次数足够的少

·        发生Full GC的周期足够的长

  前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。

   (1)针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
   (2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小

   (3)年轻代和年老代设置多大才算合理?这个我问题毫无疑问是没有答案的,否则也就不会有调优。我们观察一下二者大小变化有哪些影响

·        更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC

·        更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率

·        如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间

  (4)在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集

  (5)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

  (4)可以通过下面的参数打Heap Dump信息

·        -XX:HeapDumpPath

·        -XX:+PrintGCDetails

·        -XX:+PrintGCTimeStamps

·        -Xloggc:/usr/aaa/dump/heap_trace.txt

    通过下面参数可以控制OutOfMemoryError时打印堆的信息

·        -XX:+HeapDumpOnOutOfMemoryError

 请看一下一个时间的Java参数配置:(服务器:Linux 64Bit,8Core×16G)

 

 JAVA_OPTS="$JAVA_OPTS-server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m-XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps-Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G"

经过观察该配置非常稳定,每次普通GC的时间在10ms左右,Full GC基本不发生,或隔很长很长的时间才发生一次

通过分析dump文件可以发现,每个1小时都会发生一次Full GC,经过多方求证,只要在JVM中开启了JMX服务,JMX将会1小时执行一次FullGC以清除引用,关于这点请参考附件文档。

调优方法

一切都是为了这一步,调优,在调优之前,我们需要记住下面的原则:

 

1、多数的Java应用不需要在服务器上进行GC优化;

2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;

3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);

4、减少创建对象的数量;

5、减少使用全局变量和大对象;

6、GC优化是到最后不得已才采用的手段;

7、在实际使用中,分析GC情况优化代码比优化GC参数要多得多;

 

GC优化的目的有两个(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml):

1、将转移到老年代的对象数量降低到最小;

2、减少full GC的执行时间;

 

为了达到上面的目的,一般地,你需要做的事情有:

1、减少使用全局变量和大对象;

2、调整新生代的大小到最合适;

3、设置老年代的大小为最合适;

4、选择合适的GC收集器;

 

在上面的4条方法中,用了几个“合适”,那究竟什么才算合适,一般的,请参考上面“收集器搭配”和“启动内存分配”两节中的建议。但这些建议不是万能的,需要根据您的机器和应用情况进行发展和变化,实际操作中,可以将两台机器分别设置成不同的GC参数,并且进行对比,选用那些确实提高了性能或减少了GC时间的参数。

 

真正熟练的使用GC调优,是建立在多次进行GC监控和调优的实战经验上的,进行监控和调优的一般步骤为:

1,监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化;

 

2,分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;

注:如果满足下面的指标,则一般不需要进行GC:

   Minor GC执行时间不到50ms;

   Minor GC执行不频繁,约10秒一次;

   Full GC执行时间不到1s

   Full GC执行频率不算频繁,不低于10分钟1次;

 

3,调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择;

4,不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数

5,全面应用参数

如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。

 

调优实例

上面的内容都是纸上谈兵,下面我们以一些真实例子来进行说明:

实例1:

笔者昨日发现部分开发测试机器出现异常:java.lang.OutOfMemoryError: GC overhead limitexceeded,这个异常代表:

GC为了释放很小的空间却耗费了太多的时间,其原因一般有两个:1,堆太小,2,有死循环或大对象;

笔者首先排除了第2个原因,因为这个应用同时是在线上运行的,如果有问题,早就挂了。所以怀疑是这台机器中堆设置太小;

使用ps -ef |grep "java"查看,发现:

 

 

该应用的堆区设置只有768m,而机器内存有2g,机器上只跑这一个java应用,没有其他需要占用内存的地方。另外,这个应用比较大,需要占用的内存也比较多;

笔者通过上面的情况判断,只需要改变堆中各区域的大小设置即可,于是改成下面的情况:

 

 

跟踪运行情况发现,相关异常没有再出现;

 

实例2:(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml)

一个服务系统,经常出现卡顿,分析原因,发现Full GC时间太长

jstat -gcutil:

S0     S1   E     O       P        YGCYGCT FGC FGCT  GCT

12.16 0.00 5.18 63.78 20.32 54   2.047 5     6.946  8.993 

分析上面的数据,发现Young GC执行了54次,耗时2.047秒,每次Young GC耗时37ms,在正常范围,Full GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full GC耗时较长,分析该系统的是指发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的原因:

1,新生代太小,导致对象提前进入老年代,触发老年代发生Full GC;

2,老年代较大,进行Full GC时耗时较大;

优化的方法是调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种做法对一些应用是很有用的,但并不是对所有应用都要这么做)

 

实例3:

一应用在性能测试过程中,发现内存占用率很高,Full GC频繁,使用sudo -u admin -H  jmap -dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat差距进行分析,发现:


 

从图中可以看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,导致整个线程占用内存高达378m,此时通知开发人员进行代码优化,将相关对象释放掉即可。

58、JVM:内存监控及各区域内存溢出解决

本文仅关注一些常见的虚拟机内存监视手段,以及JVM运行时数据区各个部分内存溢出的发生和对应的解决方案,总体来说属于概括性总结,涉及相对不是很深入,目的是让自己和其它初学者有一个框架性、概念性的了解,当遇到问题时有迹可循、不至于不知所措。

 

一、虚拟机内存监视手段

虚拟机常出现的问题包括:内存泄露、内存溢出、频繁GC导致性能下降等,导致这些问题的原因可以通过下面虚拟机内存监视手段来进行分析,具体实施时可能需要灵活选择,同时借助两种甚至更多的手段来共同分析。

比如GC日志可以分析出哪些GC较为频繁导致性能下降、是否发生内存泄露。jstat工具和GC日志类似,同样可以查看GC情况、分析是否发生内存泄露。判断发生内存泄露后,可以通过jmap工具和MAT等分析工具的结合查看虚拟机内存快照,分析发生内存泄露的原因。内存溢出快照可以分析出内存溢出发生的原因等。

 

GC日志记录

JVM每次进行GC的情况记录下来,通过观察GC日志可以看出来GC的频度、以及每次GC都回收了哪些区域的内存,根据这些信息为依据来调整JVM相关设置,可以减少Minor GC的频率以及Full GC的次数,还可以判断是否有内存泄露发生。

下面是常见的GC日志输出参数:

u  -verbose.gc:显示GC的操作内容。打开它,可以显示最忙和最空闲收集行为发生的时间、收集前后的内存大小、收集需要的时间等。

u  -XX:+printGCdetails:详细了解GC中的变化。

u  -XX:+PrintGCTimeStamps:了解垃圾收集发生的时间,自JVM启动以后以秒计量。

u  -XX:+PrintHeapAtGC:了解堆的更详细的信息。

u  -Xloggc:[file]:将GC信息输出到单独的文件中

 

jstat:虚拟机统计信息监控工具

实时监视虚拟机运行时的类装载情况、各部分内存占用情况、GC情况、JIT编译情况等。

例:每隔250ms查询一次进程2211的垃圾收集情况,查询50

步骤jps列出本机所有运行的jvm实例,获取jvmpid

步骤jstat实时监控gc情况,jstat–gc 2211 250 50

其他参数包括:

        -class监视类装载、卸载数量、总空间以及类装载所耗费时间

        -gccapacity监视内容与-gc相同,输出主要关注堆各个区域用到的最大、最小空间

        -gcutil监视内同与-gc相同,输出主要关注堆各个区域已使用空间所占总空间百分比

        -gcnew监视新生代GC情况

        -gcold监视旧生代GC情况

 

jmap:虚拟机内存映像工具

jmap工具可以让运行中的JVM生成Dump文件,当JVM内存出现问题时可以通过jmap生成快照,分析整个堆,主要经历两个步骤:

步骤1jps列出本机所有运行的jvm实例,获取jvmpid

步骤2:使用jmap命令将指定JVM快照导出为dump文件

jmap-dump:format=b,file=path/heap.bin PID   

获得JVM快照的dump文件之后,可以通过MAT工具进行分析。

MAT(MemoryAnalyzer Tool)工具是eclipse的一个插件,使用起来非常方便,尤其是在分析大内存的dump文件时,可以非常直观的看到各个对象在堆空间中所占用的内存大小、类实例数量、对象引用关系、利用OQL对象查询,以及可以很方便的找出对象GCRoots的相关信息,最吸引人的是能够快速为开发人员生成内存泄露报表,方便定位和分析问题。

除此之外,jmap还可以查询finalize执行队列、Java堆和持久代的详细信息,比如空间使用率,当前使用的是哪种收集器等。

 

内存溢出快照生成

通过设置JVM参数,可以让虚拟机发生OutOfMemoryError(OOM)内存溢出时自动生成dump文件,通过分析dump文件查看内存使用情况可以找到内存溢出发生的原因:

-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/path/to/heap/dump

获得JVM快照的dump文件之后,可以通过MAT工具进行分析。

二、运行时数据区内存溢出

JVM运行时数据区分为以下几个部分:

其中方法区和堆是所有工作线程共享的,而栈、程序计数器和本地方法栈是线程私有的。

注:图片转自网络

 

1.程序计数器

作用:指向当前线程下一条需要执行的字节码指令的地址

内存溢出:不会发生

2.虚拟机栈

作用:由栈帧组成、每个栈帧代表一次方法调用,其包含存储变量表、操作数栈和方法出口三个部分,方法执行完成后该栈帧将被弹出。

内存溢出:StackOverflowErrorOutOfMemoryError

溢出原因:

StackOverflowError:如果请求的栈的深度大于虚拟机所允许的深度,将会抛出这个异常,如果使用虚拟机默认参数,一般达到10002000这样的深度没有问题。

OutOfMemoryError:因为除掉堆内存和方法区容量,剩下的内存由虚拟机栈和本地方法栈瓜分,如果剩下的内存不足以满足更多的工作线程的运行、或者不足以拓展虚拟机栈的时候,就会抛出OutOfMemoryError异常。

解决方法:

针对StackOverflowError

1.      首先栈溢出会输出异常信息,根据信息查看对应的方法调用是否出现无限调用、或者栈帧过大等代码逻辑上的问题,通过修改代码逻辑解决;

2.      如果确确实实需要更大的栈容量,可以检查并调大栈容量:-Xss16m

针对OutOfMemoryError

1.      首先检查是否创建过多的线程,减少线程数

2.      可以通过减少最大堆容量减少栈容量来解决。

3.本地方法栈

作用:与虚拟机栈唯一的不同是虚拟机栈执行的是java方法,而本地方法栈执行的是本地的C/C++方法

内存溢出StackOverflowErrorOutOfMemoryError

溢出原因:同虚拟机栈

解决方法:同虚拟机栈

4.

作用:所有线程共享,存放对象实例

内存溢出OutOfMemoryError:Java heapspace

溢出原因:堆中没有足够内存完成实例分配,并且无法继续拓展时

解决方法

1.内存泄露检查:首先通过内存溢出快照 + MAT等分析工具,分析是否存在内存泄露现象,检查时可以怀疑的点比如集合、第三方库如数据库连接的使用、new关键字相关等。

2.如果没有内存泄露,那么就是内存溢出,所有对象却是都还需要存活,这个时候就只能调大堆内存了:-Xms-Xmx

5.方法区

作用:所有线程共享,存放已加载的class信息、常量、静态变量和即时编译后的代码

内存溢出OutOfMemoryError:PermGen space

溢出原因:方法区没有足够内存完成内存分配存放运行时新加载的class信息

解决方法

1. 内存泄露检查:检查是否加载过多class文件(jar文件),或者重复加载相同的class文件(jar文件)多次

2. 通过-XX:PermSize=64M-XX:MaxPermSize=128M改大方法区大小

 

6.运行时常量池

作用:方法区的一部分,存放常量

内存溢出OutOfMemoryError:PermGen space

溢出原因:方法区没有足够的内存完成内存分配,存放运行时新创建的常量,比如String类的intern()方法,其作用是如果常量池已经包含一个相同的字符串,则返回其引用,否则将此String对象包含的字符串添加到常量池中。

解决方法

1. 内存泄露检查:检查是否创建过多常量

2. 通过-XX:PermSize=64M-XX:MaxPermSize=128M改大方法区大小

7.直接内存

作用:不属于JVM运行时数据区,也不是虚拟机规范中定义的内存区域,JDK1.4引入的NIO中包含通道Channel和缓冲区Buffer,应用程序从通道获取数据是先经过OS的内核缓冲区,再拷贝至Buffer,因为比较耗时,所以Buffer提供了一种直接操作操作系统缓冲区的方式,即ByteBuffer.allocateDirector(size),这个方法返回DirectByteBuffer应用就是指向这个底层存储空间关联的缓冲区,即直接内存。

内存溢出OutOfMemoryError

溢出原因JVM所需内存 +直接内存 > 机器物理内存(或操作系统级限制),无法动态拓展

判断方法:内存泄露检查:例如内存占用较高,机器性能骤降,但是通过GC信息或者jstat发现GC很少,通过jmap获得快照分析后也没发现什么异常,而程序中又直接或者间接地用到了NIO,那么和可能就是直接内存泄露了。

解决方法:分析NIO相关的程序逻辑解决。

原创粉丝点击