• 避免使用finalize方法


    一、为什么不要使用finalize


      终结方法finalize是不可预测的:
    (1)无法保证什么时间执行。
    (2)无法保证执行该方法的线程优先级。
    (3)无法保证一定会执行。
    (4)如果在终结方法中抛出了异常,并且该异常未捕获处理,则当前对象的终结过程会终止,且该对象处于破坏状态。
    (5)影响GC的效率,特别是在finalize方法中执行耗时较长的逻辑。
    (6)有安全问题,可以进行终结方法攻击。其原理是一个类的构造器如果抛出异常,正常程序中无法获取到这个对象的引用,但是可以在终结方法finalize中获取到这个夭折对象的引用。因此,如果创建一个该类的恶意子类,覆盖其finalize方法,并且在finalize方法中将这个夭折对象的引用保存到一个静态域上,之后就可以任意调用该对象的方法了。下面是一个模拟攻击的代码,为了保证简单,并未使用恶意子类的方式进行说明:
     
    public class FinalizeAttack {
    
        public static void main(String[] args) throws InterruptedException {
            FinalizeAttack instance = null;
            try {
                instance = new FinalizeAttack();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            System.out.println("FinalizeAttack instace is: " + instance);
            System.gc();
            Thread.sleep(2000L);
            Attacker.invoke();
        }
    
        /**
         * 构造器,抛出异常
         */
        public FinalizeAttack() {
            System.out.println("start construct FinalizeAttack instance");
            throw new IllegalStateException("construct error, exit");
        }
    
        /**
         * 被攻击的方法
         */
        public void sayHello() {
            System.out.println("hello");
        }
    
        /**
         * 被攻击的方法
         */
        @Override
        public void finalize() {
            System.out.println("attack!");
            Attacker.instance = this;
        }
    
        /**
         * 攻击者
         */
        private static class Attacker {
    
            /** 静态实例 */
            private static FinalizeAttack instance;
    
            /**
             * 调用被攻击对象的方法
             */
            public static void invoke() {
                instance.sayHello();
            }
    
        }
    
    }
    
      这个类的执行结果如下,可以看到构造器异常后,在main方法中无法获取到对象引用(即仍然为null),但是在finalize方法中,我们将其保存到另外一个类的静态域上,并且能够成功调用该对象的sayHello方法。
    start construct FinalizeAttack instance
    construct error, exit
    FinalizeAttack instace is: null
    attack!
    hello
    

    二、如何正确的关闭资源


      对于一个已经存在的资源类,例如JDK中的流、数据库连接,它们都具有close方法,我们要做的就是使用try-catch-finally或try-finally又或者是try-with-resources进行关闭。对于JDK1.7之后的版本,更推荐使用try-with-resources方法,至于原因,可以看下面的一个例子:
    //使用try-finally进行关闭,对于下面这种将异常直接向上抛的方法,存在一个异常覆盖的问题:
    //即如果br.readLine()、br.close()都抛出了异常,则后一个异常会抹除前一个异常,
    //导致前一个异常的堆栈完全不打印,这样就没办法判断真实原因
    public static String readFirstLine(String filename) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(filename));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }
    
    //使用try-with-resources进行关闭,对于将异常直接向上抛的方法,不会存在异常覆盖的问题:
    //即如果br.readLine()、br.close()都抛出了异常,后一个异常会被禁止,方法抛出的是第一个异常,
    //但是被禁止的异常并不是简单的被抛弃了,而是会被打印在异常堆栈中,并且标注为被禁止的异常,
    //另外,还可以通过getSuppressed方法从捕获到的异常中访问到被禁止的异常。
    public static String readFirstLine(String filename) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            return br.readLine();
        }
    }
    

      上面的例子描述了try-with-resources在保留多个异常上的优势,这是对于异常直接被上抛的方法而言的。如果要在方法内部处理异常,try-with-resources也能带来编码简洁、清晰的好处:  

    //使用try-catch-finally进行关闭,对于下面这种在内部处理异常的方法,代码很繁琐
    public static String readFirstLine(String filename) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filename));
           return br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
            return "-1";
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    //close quietly
                    return "-1";
                }
            }
        }
    }
    
    //使用try-with-resources进行关闭,对于下面这种在内部处理异常的方法,代码简单、清晰
    public static String readFirstLine(String filename) {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            return br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
            return "-1";
        }
    }
    

    三、编写自己的可关闭资源类


      如果想自己封装一个包含资源的类,如一个典型的JDBC工具,那么最好遵循以下原则:
    • 让这个类实现AutoCloseable接口,并且在实现的close方法中关闭资源;
    • 为这个类设置一个状态私有域用于标记当前实例是否已经被关闭。当调用close方法时,需要将该标记的值置于关闭状态;当调用其他方法时,需要检查这个标记,如果发现为关闭状态,则抛出异常;
    public class MyConnection implements AutoCloseable  {
    
        /** 数据库连接 */
        private Connection   connection;
    
        /** 驱动名 */
        private final String driverName;
    
        /** 连接串 */
        private final String url;
    
        /** 用户名 */
        private final String username;
    
        /** 密码 */
        private final String password;
        
        /** 是否关闭 */
        private volatile boolean closed = false;
    
        /**
         * 构造函数
         * @param driverName 驱动名
         * @param url        连接串
         * @param username   用户名
         * @param password   密码
         */
        public MyConnection(String driverName, String url, String username, String password) {
            this.driverName = driverName;
            this.url = url;
            this.username = username;
            this.password = password;
        }
    
        /**
         * 初始化连接
         * @throws Exception 连接初始化未找到驱动类或其他SQL异常
         */
        public void init() throws Exception {
            Class.forName(driverName);
            this.connection = DriverManager.getConnection(url, username, password);
            this.connection.setAutoCommit(false);
        }
    
        /**
         * 执行查询
         * @param sql 查询SQL语句
         * @return 查询结果
         * @throws Exception 查询失败抛出的SQLException
         */
        public List<Map<String, String>> execQuery(String sql) throws Exception {
            if (!closed) {
              throw new IllegalStateException("connection has closed");
            }
            //其他处理
        }
    
        /**
         * 执行插入/更新/删除语句
         * @param formatSql 格式化SQL,使用占位符%s
         * @param args      格式化sql参数
         * @return 执行影响的行数
         * @throws Exception 执行失败抛出的SQLException
         */
        public int execUpdate(String formatSql, Object... args) throws Exception {
            if (!closed) {
              throw new IllegalStateException("connection has closed");
            }
            //其他处理
        }
    
        /**
         * 提交事务
         * @throws Exception 提交事务时失败
         */
        public void commit() throws Exception {
            //略
        }
    
        /**
         * 关闭连接
         */
        @Override
        public void close() {
    this.closed = true; try { this.connection.commit(); this.connection.close(); } catch (SQLException e) { //close quietly } } }

    四、哪些地方仍在使用finalize


      尽管我们在上面说明了应当避免使用finalize方法,但是在JDK中某些地方仍然在使用finalize,这些finalize方法只是为我们提供了最后一道很小的保障,毕竟释放一点资源总比永远不释放要好,但是我们不能将资源释全权委托给该方法来执行。
      比较典型的几个类:FileInputStream、FileOutputStream、ThreadPoolExecutor都重写了finalize方法,下面是这三个类中重写的finalize方法源码:
    //FileInputStream,在简单的判断文件描述符不为null并且不为标准输入后,执行关闭
    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }
    
    //FileOutputStream,在简单的判断文件描述符不为null并且不为标准输出/标准错误后,执行关闭
    protected void finalize() throws IOException {
        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {
                /* if fd is shared, the references in FileDescriptor
                 * will ensure that finalizer is only called when
                 * safe to do so. All references using the fd have
                 * become unreachable. We can call close()
                 */
                close();
            }
        }
    }
    
    //ThreadPoolExecutor,根据安全策略,执行shutdown关闭线程池
    protected void finalize() {
        SecurityManager sm = System.getSecurityManager();
        if (sm == null || acc == null) {
            shutdown();
        } else {
            PrivilegedAction<Void> pa = () -> { shutdown(); return null; };
            AccessController.doPrivileged(pa, acc);
        }
    }
    
  • 相关阅读:
    Element没更新了?Element没更新,基于El的扩展库更新
    MVC与Validate验证提示的样式修改
    封装两个简单的Jquery组件
    VS20XX-Add-In插件开发
    CentOS7 配置环境
    PHP Laravel 5.4 环境搭建
    【设计经验】5、Verilog对数据进行四舍五入(round)与饱和(saturation)截位
    【设计经验】4、SERDES关键技术总结
    【高速接口-RapidIO】6、Xilinx RapidIO核仿真与包时序分析
    【高速接口-RapidIO】5、Xilinx RapidIO核例子工程源码分析
  • 原文地址:https://www.cnblogs.com/manayi/p/14651133.html
Copyright © 2020-2023  润新知