signed

QiShunwang

“诚信为本、客户至上”

JAVA开发的二十多种死法,阅读避坑

2021/5/14 21:15:39   来源:

最近公司分享了一篇文章,java项目开发中的常见问题,这里我分享一下,后期我会加一些自己的看法

不知道这个内容是老大自己写的还是从网上照搬的数据,如有侵权联系删除,这里只是想给大家分享一下,让大家及时避坑。

人固有一死,服务器也是,哪怕活到99.99,它也有必须死的时候千姿百态,死得光荣:
1、	内存溢出
2、	连接泄漏
3、	内存泄漏
4、	堆栈溢出
5、	游标溢出
6、	线程泄漏
7、	死锁
8、	频繁GC
9、	系统管理员停机维护
10、	错误的异常处理
死并不可怕,可怕的是不知道为什么死 死并不新鲜,难的是每一次死都要死出新花样。
你身边的代码,触目惊心,不要骂娘,因为你也可能有这些问题。

以下代码纯属虚构,如有雷同,你一定要注意了(赶紧改)!

1、HashMap并发访问CPU 100%,卡死

HashMap不是线程安全的,您不妨试试在并发场景使用HashMap,绝对酸爽,妥妥的死循环卡死,ConcurrentHashMap用起来就没这么刺激了。什么是并发场景?就是一定要把它开放出去,让外部既能get,又能put,用起来很方便。

2、不使用线程池无节制新建线程,线程溢出死

来一个提升性能的终极优化,给协同中所有人发消息。

for(Long id : receivers){
    sendMessage(id);
}

for循环好慢,异步吧!本机测试,时间省出来了,神速,没毛病。

for(Long id : receivers){
    new Thread(){
        public void run(){
            sendMessage(id);
        }
    }
}

部署到生产系统,怎么宕机了。。。
线程满,内存满,好像有一种东西叫线程池。

3、无节制使用内存,不清理,OOM

我的自测,是世界上最认真的,测试同学严谨认真提供的用例,全功能无死角测试。
但是,我一定要绽放出颜色不一样的焰火…
我可以把十万条记录的表数据全部加载内存中…
我可以把数十M的Excel解析成对象,放到List中…
反正我本地正常…
Oh,My God! 请照顾一下服务器的感受。

4、JDBC连接不释放,连接溢出死

JDBCAgent agent = new JDBCAgent();
agent.execute(sql);

执行一次,泄漏一次,连接池很快就没有可用的连接了。
修正版:

JDBCAgent agent = new JDBCAgent();
try{
    agent.execute(sql);
}catch(Exception e){

}finally{
    //但凡JDBCAgent,都这样写最好
    agent.close();
}

5、游标不关闭,游标溢出死

ORA-01000: maximum open cursors exceeded
想看到这个提示可以这样做

PreparedStatement statement = null;
try {
    Connection conn = session.connection();
    for (int j = 0; j < count; j++) {
        statement = conn.prepareStatement(Insertsql);
        statement.executeBatch();
    }
} finally {
    if (statement != null) {
        statement.close();
    }                
}

statement不关闭,for循环中创建statement,看似关闭了,但其实并没有关闭,泄漏得非常快。
使用JDBCAgent同样会有statement不关闭问题,尽量注意不要在for循环中执行JDBCAgent命令。
jdbcAgent.batch3Execute(); //不会关闭statement,只有等jdbcAgent.close才会关闭
jdbcAgent.execute(“select …”) //不会关闭statement,只有等jdbcAgent.close才会关闭
jdbcAgent.execute(“update / delete …”); //会立即关闭statement
jdbcAgent.resultSetToList(); //会立即关闭statement

6、while true抢占CPU,CPU 100%,卡死

new Thread(){
    public void run(){
        while(true){
            // doSth.
        }
    }
}

专家点评:
文武之道一张一弛,服务器太累了,让它休息一下吧!

new Thread(){
    public void run(){
        while(true){
            // doSth.
            //...
           long millis = 5 * 1000L;
            try {
                Thread.sleep(millis);
            } catch (Exception e) {
                LOG.error(e.getLocalizedMessage(), e);
            }                    
        }
    }
}

7、堆栈溢出-死循环

public String getBaseName(Map<String, String> preference) {
    return this.getBaseName(preference);
}

整齐的异常信息

 java.lang.StackOverflowError
    at com.seeyon.ctp.portal.section.MyTaskSection.getBaseName(MyTaskSection.java:548)
    at com.seeyon.ctp.portal.section.MyTaskSection.getBaseName(MyTaskSection.java:548)
    at com.seeyon.ctp.portal.section.MyTaskSection.getBaseName(MyTaskSection.java:548)
    at com.seeyon.ctp.portal.section.MyTaskSection.getBaseName(MyTaskSection.java:548)

8、for循环嵌套SQL,太多的SQL语句,数据库卒

    List<TaskInfo> taskInfoList = taskInfoDao.findList(params);
    for(TaskInfo taskInfo : taskInfoList) {
        TaskInfoBody body = taskInfoDao.getTaskInfoBodyByTaskId(taskInfo.getId());
        //...
    }

问题:taskInfoList有N条,for循环就执行N条sql,你不死谁死
解决方法:把for中的sql放到外面一次取出

   List<TaskInfo> taskInfoList = taskInfoDao.findList(params);
   List<Long> taskIds = getTaskIds(taskInfoList);
   List<TaskInfoBody> bodyList = taskInfoDao.findBodyByTaskIds(taskIds);
   for(TaskInfo taskInfo : taskInfoList) {
       //TaskInfoBody body = taskInfoDao.getTaskInfoBodyByTaskId(taskInfo.getId());
       //...
   }

9、粗粒度同步(synchronized)卡死

//这段代码纯属杜撰,我实在找不到更好的例子了
public class OrgManagerImpl{
    public synchronized V3xOrgMember getMemberById(long id){
    }
}

专家点评
这段代码的精髓就是synchronized ,简直是神来之笔。据不完全统计,1千人的企业,每日要访问这个接口数亿次,它成功的为1千个人提供了足够的休息时间。

10、太多的日志,卡死,硬盘满

11、SQL注入死

public List<String> findById(String id) {
    String hql = "SELECT subject FROM TaskInfo WHERE status = 1 AND id =" + id;
    return DBAgent.find(hql);
}

危害1:变量id会被SQL注入,严重安全漏洞,这种写法扣钱的!
危害2:Hibernation会缓存每一条不重复的SQL,因为id是变量,会缓存N条不同id的SQL,导致缓存过多内存溢出。
推荐写法如下:

  public List<String> findById(String id) {
       String hql = "SELECT subject FROM TaskInfo WHERE status = 1 AND id = :id";
       Map<String, Object> params = new HashMap<String, Object>();
       params.put("id", Long.valueOf(id));
       return DBAgent.find(hql,params);
   }

12、吞掉异常

总会有一天有人会被你坑死的。

  MeetingVideoManager meetingVideoManager = null;
      try {
             meetingVideoManager = meetingApplicationHandler.getMeetingVideoHandler();
           } catch (Exception e) {

//这里的异常捕获代码去旅游了????
}

13、抽象,再抽象

这样的代码,你强制规定调用者给你传入的List必须是ArrayList,这是不人道的:
public void xxx(Map data){
ArrayList list = (ArrayList) data.get(“list”);
}
起码你应该
List list = (List) data.get("list");
// 在中国,在北京,我们通常都这样定义
List<String> list = new ArrayList<String>()
// 而不是这样
ArrayList<String> list =new ArrayList<String>();
// 总觉得怪怪的,如果有必要,我们还会这样定义
Collection<String> collection = new ArrayList<String>();

14、List contains,慢死

private List plugins;

public boolean hasPlugin(String id){
    return plugins.contains(id);
}

总体而言,这段代码99%都是正确的。
就是数据结构不对。让我们来看看JDK源码里List的contains实现,以ArrayList为例:

/**
 * Returns <tt>true</tt> if this list contains the specified element.
 * More formally, returns <tt>true</tt> if and only if this list contains
 * at least one element <tt>e</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
 *
 * @param o element whose presence in this list is to be tested
 * @return <tt>true</tt> if this list contains the specified element
 */
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/**
 * Returns the index of the first occurrence of the specified element
 * in this list, or -1 if this list does not contain the element.
 * More formally, returns the lowest index <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
 * or -1 if there is no such index.
 */
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

    private Set plugins;

15、Queue的size

避免使用ConcurrentLinkedQueue的size。看了下API你就会知道,它的size()方法是要遍历一遍所有元素的。

  //每次取1000条数据
   int size = queue.size();
   int toIndex = 0;
   if(size > 1000) {
      toIndex = 1000;
   }else {
      toIndex = size;
   }
   for(int i = 0; i < toIndex; i++) {
      CtpAffair affair = queue.poll();
      if(affair != null) {
         calculateList.add(affair);
      }
   }

修改为

  //每次取1000条数据
  for(int i = 0; i < 1000; i++) {
      CtpAffair affair = queue.poll();
      if(affair != null) {
         calculateList.add(affair);
      }else {
         break;
      }
  }

16、List size的好习惯

List list;
for (int i = 0; i < list.size(); i++)
}

不要太相信黑盒,如果你所使用的List正巧是size无缓存的链表实现,你的for循环可以绕地球一圈。

List list;
int length = list.size();
for (int i = 0; i < length; i++)
}

17、指定编码是个好习惯

总有一天你会吃亏的

string.getBytes("utf-8");
new String(bytes, "utf-8");

18、remove item for循环的方向

for(int i=0;i<size;i++){
    collection.remove(i);
}

总是清除不干净,因为元素的索引是在不停的前移的,remove到最后你就可能会发现没有元素可以remove了。应该这样:

for(int i=size-1;i>0;i--){
    collection.remove(i);
}

19、经典的IF-ELSE

if-else是编码界经典的bad smell,如果还要找比它更糟糕的,也许就是if-else if
看下面的代码:

int xxx(int n){
    if( n==0 ){
        return 0;
    }else if( n==1 ){
        return 1;
    }else if( n==2 ){
        return 1;
    }else if( n==3 ){
        return 2;
    }else if( n==4 ){
        return 3;
    }else if( n==5 ){
        return 5;
    }
    return xxx;
}

仔细看,不错,是Fibonacci,你可以说,我才不会写这样的代码,我可以

int fibonacci(int n){
    if( n < 2 ){
        return n;
    }
    return fibonacci(n-1) + fibonacci(n-2);
}

是的,我相信你不会写这样的代码
但是,你可能会写这样的代码
// 伪造代码,旨在说明问题,或许你用的是switch case

String getControllHTML(int fieldType)
    if(fieldType == TEXTAREA){
        return "<textarea></textarea>";
    }else if(fieldType == CHECKBOX){
        return "<input type=\checkbox\"/>";
    }else if(fieldType == RADIO){
        return "<input type=\radio\"/>";
    }else if(fieldType == SELECT){
        return "<select></select>";
    }else if(fieldType == RELATIONFORM){
        return "xxx";
    }
    // balabala...
}

如果你这样写,你的代码就失去了扩展的可能性,而且圈复杂度急剧上升。
最简单也是最通用的做法,我们用Map来消除if-else

private Map<Integer,String> htmlMap = new HashMap<Integer,String>();

String getControllHTML(int fieldType){
    return htmlMap.get(fieldType);
}
如果一定要在这个Map上加上行为,我们可以用设计模式
interface FieldHtmlBuilder{
    String build();
}
private Map<Integer,FieldHtmlBuilder> builders = new HashMap<Integer,String>();
String getControllHTML(int fieldType){
    return builders.get(fieldType).build();
}
// 这样,外部就可以扩展了
public void register(int fieldType,FieldHtmlBuilder builder){
    if(builders.containKey()){
        throw UnsupportedOperationException(fieldType + "的builder已经存在!");
    }
    builders.put(fieldType,builder);
}

register(TEXTAREA,new FieldHtmlBuilder(){
    public String build(){
        return "<textarea></textarea>";
    }
});

20、重要的事说三遍,但没必要写三遍

Bad

if(_this.attr("data") && _this.attr("data")!="") {
    try {
        var data = $.parseJSON('{' + _this.attr("data") + '}');
    } catch(e) {}
}

Good

var attr = _this.attr("data");
if(attr && attr!="") {
        var data = $.parseJSON('{' + attr + '}');
    } catch(e) {}
}

21、勤俭节约

    HashSet<Long> list = category2Config.get(item.getConfigCategory() + item.getOrgAccountId());
    if(null != list){
        list.remove(id);
    }
    //集群需要重新设置
    category2Config.put(item.getConfigCategory() + item.getOrgAccountId(), list);

修改后

Set<Long> list = category2Config.get(item.getConfigCategory() + item.getOrgAccountId());
if(CollectionUtils.isNotEmpty(list)){
    if(list.remove(id)){
        //集群需要重新设置
        category2Config.put(item.getConfigCategory() + item.getOrgAccountId(), list);
    }
}

22、使用正确的List

时间复杂度对比 ArrayList LinkedList
add (append) 常量 或 ~ 扩容log(n) 常量
insert (middle) 线性 或~ 扩容 n*log(n) 线性
remove (middle) 线性 (完整复制) 线性
迭代 线性 线性
按索引get 常量 线性
基本功不可废,建议阅读下面数据结构的实现:
ArrayList、HashMap、 HashSet、StringBuilder、StringBuffer。
如果有时间,再看看这几个:
String、Long、Integer、Date、BigDecimal。

23、硬编码密码

程序中采用硬编码方式处理密码,一方面会降低系统安全性,另一方面不易于程序维护。

   // com.seeyon.cap4.monitor.perf.dao.ConnectionUtil
    private static Connection getNewConn() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useSSL=false", "conn", "123456");
        } catch (Exception arg0) {
            LOGGER.error(arg0.getMessage(), arg0);
            return null;
        }
    }

惨案:数据库遭受非法攻击,起因是开发人员把带有密码的代码提交到了github。

24、代码注入:HTTP响应截断

程序从一个不可信赖的数据源获取数据,未进行验证就置于HTTP头文件中发给用户,可能会导致HTTP响应截断攻击。

//com.seeyon.cap4.form.modules.formlist.CAP4FormListController
public ModelAndView doDownload(HttpServletRequest request, HttpServletResponse response) throws Exception {
    String fileName = request.getParameter("fileName");   // 注意
    fileName = URLEncoder.encode(fileName, "UTF-8");
    String charset = "UTF-8";
    response.setContentType("application/octet-stream; charset=" + charset);
    response.setCharacterEncoding(charset);
    response.setHeader("Content-disposition", "attachment;filename=\"" + fileName + "\""); // 注意
}

25、API误用:文件泄露:Spring

若通过用户输入构造服务器端重定向路径,攻击者便能够下载应用程序二进制码(包括应用程序的类或jar文件)或者查看受保护的目录下的任意文件。

//com.seeyon.cap4.form.modules.component.CAP4ComponentController
public ModelAndView forward(HttpServletRequest request, HttpServletResponse response) throws Exception {
    String url = request.getParameter("source");
    return Strings.isNotBlank(url) ? new ModelAndView(url) : null;
}

26、代码质量:系统信息泄露:外部

程序不应通过系统输出流或程序日志将系统数据或调试信息输出程序。

// com.seeyon.ctp.portal.sso.SSOLoginServlet
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    PrintWriter out = resp.getWriter();
    try{
    }catch (Throwable e) {
        out.println(e.getMessage());
    }
}

27、输入验证:日志伪造

允许日志记录未经验证的用户输入,会导致日志伪造攻击。

// com.seeyon.apps.collaboration.controller.CollaborationController
Enumeration es = request.getHeaderNames();
StringBuilder stringBuilder = new StringBuilder();
if (es != null) {
    while (es.hasMoreElements()) {
        Object name = es.nextElement();
        String header = request.getHeader(name.toString());
        stringBuilder.append(name + ":=" + header + ",");
    }
    LOG.warn("request header---" + stringBuilder.toString());
}

28、代码质量:双重检查锁定

如果需要对实例字段使用线程安全的延迟初始化,请使用基于volatile的延迟初始化的方案。如果需要对静态字段使用线程安全的延迟初始化,基于类初始化的方案。

// com.seeyon.apps.common.image.utils.ImageUtils
if(imageHandlers == null){
    synchronized (lock) {
        if(imageHandlers == null){
        }
    }
}

例1:基于volatile的双重检查锁定的解决方案。

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;
    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//volatile instance
            }
        }
        return instance;
    }
}

例2:基于类初始化的解决方案。

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    public static Instance getInstance() {
        return InstanceHolder.instance ;  //InstanceHolder class is initialized
    }
}

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。