内存泄漏和可运行的奇怪错误 class - 如何正确处理对象?

Memory leaks, and strange bugs with runnable class - How to properly dispose of objects?

我正在构建一个服务器应用程序来跟踪当前有多少人在使用我最近构建的另一个应用程序运行。这更多的只是为了测试我自己,看看我能用这门语言做什么,而且几乎很快我就能够让事情正常进行。但是,当 运行ning 对应用程序进行人工测试时,我 运行 遇到了一个严重的问题。生病 post 我的代码...

主要class:

public class ServerGUIStatistics {

public static MainWindow mw = null;
private static int _port = 33672;

public static void main(String[] args) {
    mw = new MainWindow();
    new ServerGUIStatistics();
}

public ServerGUIStatistics(){
    ServerSocket listener = null;
    try{
        listener = new ServerSocket(_port, 100);
        while(true){
           System.out.println("Waiting for connection....");
           Socket connection = listener.accept(); 
           Connection nc = new Connection(connection);
        }
    }
    catch(Exception ex){
        mw.setConsoleOutput(ex.getMessage(), 2);// Prints to a console output window...
    }
}

我认为是可燃物 class 引起了问题。

class Connection implements Runnable {
private static int _totalConnections = 0;
private static long _totalConnectionsAllTime = 0;
private Socket socket = null;
private boolean stopFlag = true;

@Override
public void run(){
    while(stopFlag){
        stopFlag = false;
        ServerGUIStatistics.mw.setConsoleOutput("New connection from \"" + socket.getInetAddress().toString() + "\" on port " + socket.getPort(), 0);// Prints to a console output window... 
        ServerGUIStatistics.mw.updateConNumber(_totalConnections);
        ServerGUIStatistics.mw.updateConAllTimeNumber(_totalConnectionsAllTime);//Updates a field on the main window which displays the total number of users connected
        boolean stayAlive = true;
        try{
            socket.setSoTimeout(10000);
            BufferedInputStream connectionStatus = new BufferedInputStream(socket.getInputStream());
            do{
                int holder = connectionStatus.read();
                switch(holder){
                    case 1:
                        stayAlive = true;
                        break;
                    case 0:
                        stayAlive = false;
                        break;
                } 
            }
            while(stayAlive);
        }
        catch(IOException ex){
            ServerGUIStatistics.mw.setConsoleOutput(socket.getInetAddress().toString() + " " + ex.getMessage(), 2); // Prints to a console output window...
        }
        _totalConnections--;
        ServerGUIStatistics.mw.updateConNumber(_totalConnections); //Updates a field on the main window which displays the current number of users connected
        return;
    }
}

Connection(Socket soc){
    socket = soc;
    _totalConnections++;
    _totalConnectionsAllTime++;
    new Thread(this).start();
}

public static int getTotalConnections(){
    return _totalConnections;
}

这是秋千 window(由 netbeans 生成):

import java.util.Date;
import java.text.*;

public class MainWindow extends javax.swing.JFrame {

/**
 * Creates new form MainWindow
 */
private Date curDate = null;
private SimpleDateFormat ft = new SimpleDateFormat ("MM/dd/yyyy @ hh:mm:ss");

public MainWindow() {
    initComponents();
    this.setVisible(true);
}
/**
 * This method is called from within the constructor to initialize the form.
 * WARNING: Do NOT modify this code. The content of this method is always
 * regenerated by the Form Editor.
 */
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">                          
private void initComponents() {
    bindingGroup = new org.jdesktop.beansbinding.BindingGroup();

    jTabbedPane1 = new javax.swing.JTabbedPane();
    jPanel1 = new javax.swing.JPanel();
    totalConnectionsOut = new javax.swing.JTextField();
    totalConnectionsAllTimeOut = new javax.swing.JTextField();
    jLabel1 = new javax.swing.JLabel();
    jLabel2 = new javax.swing.JLabel();
    jPanel3 = new javax.swing.JPanel();
    jScrollPane2 = new javax.swing.JScrollPane();
    consoleOutput = new javax.swing.JTextArea();

    setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    totalConnectionsOut.setFont(new java.awt.Font("Tahoma", 0, 58)); // NOI18N
    totalConnectionsOut.setHorizontalAlignment(javax.swing.JTextField.CENTER);
    totalConnectionsOut.setText("0");

    org.jdesktop.beansbinding.Binding binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ_WRITE, totalConnectionsOut, org.jdesktop.beansbinding.ELProperty.create("false"), totalConnectionsOut, org.jdesktop.beansbinding.BeanProperty.create("editable"));
    bindingGroup.addBinding(binding);

    totalConnectionsAllTimeOut.setFont(new java.awt.Font("Tahoma", 0, 58)); // NOI18N
    totalConnectionsAllTimeOut.setHorizontalAlignment(javax.swing.JTextField.CENTER);
    totalConnectionsAllTimeOut.setText("0");

    binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ_WRITE, totalConnectionsAllTimeOut, org.jdesktop.beansbinding.ELProperty.create("false"), totalConnectionsAllTimeOut, org.jdesktop.beansbinding.BeanProperty.create("editable"));
    bindingGroup.addBinding(binding);

    jLabel1.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
    jLabel1.setText("Total Connections:");

    jLabel2.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
    jLabel2.setText("Total Current Connections:");

    javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
    jPanel1.setLayout(jPanel1Layout);
    jPanel1Layout.setHorizontalGroup(
        jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(jPanel1Layout.createSequentialGroup()
            .addContainerGap()
            .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addComponent(totalConnectionsOut)
                .addComponent(totalConnectionsAllTimeOut, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 571, Short.MAX_VALUE)
                .addComponent(jLabel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
            .addContainerGap())
    );
    jPanel1Layout.setVerticalGroup(
        jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(jPanel1Layout.createSequentialGroup()
            .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addGap(4, 4, 4)
            .addComponent(totalConnectionsOut, javax.swing.GroupLayout.PREFERRED_SIZE, 150, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(jLabel1, javax.swing.GroupLayout.DEFAULT_SIZE, 34, Short.MAX_VALUE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(totalConnectionsAllTimeOut, javax.swing.GroupLayout.PREFERRED_SIZE, 150, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addContainerGap())
    );

    jTabbedPane1.addTab("Info", jPanel1);

    consoleOutput.setColumns(20);
    consoleOutput.setRows(5);

    binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ_WRITE, consoleOutput, org.jdesktop.beansbinding.ELProperty.create("false"), consoleOutput, org.jdesktop.beansbinding.BeanProperty.create("editable"));
    bindingGroup.addBinding(binding);

    jScrollPane2.setViewportView(consoleOutput);

    javax.swing.GroupLayout jPanel3Layout = new javax.swing.GroupLayout(jPanel3);
    jPanel3.setLayout(jPanel3Layout);
    jPanel3Layout.setHorizontalGroup(
        jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(jPanel3Layout.createSequentialGroup()
            .addContainerGap()
            .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 571, Short.MAX_VALUE)
            .addContainerGap())
    );
    jPanel3Layout.setVerticalGroup(
        jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(jPanel3Layout.createSequentialGroup()
            .addContainerGap()
            .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 368, Short.MAX_VALUE)
            .addContainerGap())
    );

    jTabbedPane1.addTab("Console Output", jPanel3);

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
    getContentPane().setLayout(layout);
    layout.setHorizontalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addComponent(jTabbedPane1)
    );
    layout.setVerticalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addComponent(jTabbedPane1)
    );

    bindingGroup.bind();

    pack();
}// </editor-fold>                        

public void setConsoleOutput(String s, int i){
    String type = null;
    switch(i){
        case 0:
            type = "Info";
            break;
        case 1:
            type = "Error";
            break;
        case 2:
            type = "Exception";
            break;
        default:
            type = "Unknown";
            break;
    }
    curDate = new Date();
    String output = consoleOutput.getText() + "[" + ft.format(curDate) + "][" + type + "] " + s + "\n";
    consoleOutput.setText(output);
}

public void updateConNumber(int i){
    totalConnectionsOut.setText(Integer.toString(i));
}

public void updateConAllTimeNumber(long i){
    totalConnectionsAllTimeOut.setText(Long.toString(i));
}

// Variables declaration - do not modify                     
private javax.swing.JTextArea consoleOutput;
private javax.swing.JLabel jLabel1;
private javax.swing.JLabel jLabel2;
private javax.swing.JPanel jPanel1;
private javax.swing.JPanel jPanel3;
private javax.swing.JScrollPane jScrollPane2;
private javax.swing.JTabbedPane jTabbedPane1;
private javax.swing.JTextField totalConnectionsAllTimeOut;
private javax.swing.JTextField totalConnectionsOut;
private org.jdesktop.beansbinding.BindingGroup bindingGroup;
// End of variables declaration                   

}

如果有任何东西看起来不对运行ge,或者不正确,那么我提醒你,我对 Java 还是很陌生,并且是制作此类应用程序的新手。

下面是我做的人工测试:

class Main{
    public static void main(String[] args){
        while(true){
            try{
                Socket socket = new Socket("127.0.0.1", 33672);
                BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());

                for(int i = 0; i < 5; i++){
                    out.write(1);
                    out.flush();
                    Thread.sleep(10);
                }

                out.write(0);
                out.flush();

                out.close();
                socket.close();
            }
            catch(Exception ex){System.out.println(ex);}
            System.out.println("Finished! Running again...");
        }
    }
}

如您所见,它快速连接以创建新线程,交换一些消息,然后断开连接然后重复。我离开这个 运行ning 直到它有 运行 它的循环大约 1500 次,它很快就完成了。但似乎不知从哪里开始,我的内存使用量开始攀升……就像很多。这么多,它开始冻结我的电脑了。正如我之前所说,它毫无问题地达到了 1500 个周期。大约那时它使用了 80 到 100 兆字节的内存,但之后它开始缓慢攀升。到 2500 个周期时,它使用了 1GB 多一点的内存。到 5000 年,它几乎已经饱和了我所有的系统内存。

现在,我相信解决此问题的方法是在我完成连接 class 的实例后将它们设置为 null。但是我不确定如何通过程序的设置来做到这一点。

有没有一种方法可以从自身内部将对象设置为 null,或者是否有另一种我应该使用的方法来执行此操作?

所以我突然想到的事情...

在下面的代码中...

while(stopFlag){
    stopFlag = false;
    ServerGUIStatistics.mw.setConsoleOutput("New connection from \"" + socket.getInetAddress().toString() + "\" on port " + socket.getPort(), 0);// Prints to a console output window... 
    ServerGUIStatistics.mw.updateConNumber(_totalConnections);
    ServerGUIStatistics.mw.updateConAllTimeNumber(_totalConnectionsAllTime);//Updates a field on the main window which displays the total number of users connected
    boolean stayAlive = true;
    try{
        socket.setSoTimeout(10000);
        BufferedInputStream connectionStatus = new BufferedInputStream(socket.getInputStream());
        do{
            int holder = connectionStatus.read();
            switch(holder){
                case 1:
                    stayAlive = true;
                    break;
                case 0:
                    stayAlive = false;
                    break;
            } 
        }
        while(stayAlive);
    }
    catch(IOException ex){
        ServerGUIStatistics.mw.setConsoleOutput(socket.getInetAddress().toString() + " " + ex.getMessage(), 2); // Prints to a console output window...
    }
    _totalConnections--;
    ServerGUIStatistics.mw.updateConNumber(_totalConnections); //Updates a field on the main window which displays the current number of users connected
    return;
}
  • 外层 (while (stopFlag)) 没有任何意义,因为您在从内循环读取时从不测试它,一旦退出内循环,您就会调用 return
  • 您永远不会关闭 BufferedInputStreamSocket。这两者中的任何一个都可以保持对不再使用的资源的强引用,从而防止 class 被垃圾收集

更好的解决方案 "might" 看起来像...

ServerGUIStatistics.mw.setConsoleOutput("New connection from \"" + socket.getInetAddress().toString() + "\" on port " + socket.getPort(), 0);// Prints to a console output window... 
ServerGUIStatistics.mw.updateConNumber(_totalConnections);
ServerGUIStatistics.mw.updateConAllTimeNumber(_totalConnectionsAllTime);//Updates a field on the main window which displays the total number of users connected
boolean stayAlive = true;
try {
    socket.setSoTimeout(10000);
    try (BufferedInputStream connectionStatus = new BufferedInputStream(socket.getInputStream())) {
        do {
            System.out.println(number + " reading...");
            int holder = connectionStatus.read();
            System.out.println(number + " read " + holder + "...");
            switch (holder) {
                case 1:
                    stayAlive = true;
                    break;
                case 0:
                    stayAlive = false;
                    break;
            }
        } while (stayAlive);
    }
} catch (IOException ex) {
    ServerGUIStatistics.mw.setConsoleOutput(socket.getInetAddress().toString() + " " + ex.getMessage(), 2); // Prints to a console output window...
} finally {
    System.out.println(number + " exiting");
    _totalConnections--;
    try {
        socket.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
    socket = null;
    ServerGUIStatistics.mw.updateConNumber(_totalConnections); //Updates a field on the main window which displays the current number of users connected
}

注意使用 The try-with-resources Statement 以确保当代码存在 try 块时 BufferedInputStream 关闭。

注意使用finally来确保无论try块如何退出都执行代码块。这 gua运行 表示代码已执行。

接下来,我将注意力转向...

public void setConsoleOutput(String s, int i){
    String type = null;
    switch(i){
        case 0:
            type = "Info";
            break;
        case 1:
            type = "Error";
            break;
        case 2:
            type = "Exception";
            break;
        default:
            type = "Unknown";
            break;
    }
    curDate = new Date();
    String output = consoleOutput.getText() + "[" + ft.format(curDate) + "][" + type + "] " + s + "\n";
    consoleOutput.setText(output);
}

暂时忽略您对 Swing 单线程的违反,直接的问题是您在该方法的每个 运行 上创建了一个新的 Date 对象,称为 1000's of次,但 Date 对象本身是一个实例字段,可能会增加问题。

但真正令人担忧的是这个...

String output = consoleOutput.getText() + "[" + ft.format(curDate) + "][" + type + "] " + s + "\n";

虽然编译器做了很好的优化工作,但 consoleOutput.getText() 方法将使用文本区域的内容创建一个新的 String,然后您可以使用它应用回 JTextArea 使用 setText

这是非常低效的,因为底层 Document 正在将其 char 数组转换为 String 并将 String 转换回 char 数组每次...

相反,您可以相对简单地解决这两个问题...

public void setConsoleOutput(String s, int i) {
    String type = null;
    switch (i) {
        case 0:
            type = "Info";
            break;
        case 1:
            type = "Error";
            break;
        case 2:
            type = "Exception";
            break;
        default:
            type = "Unknown";
            break;
    }
    String output = "[" + ft.format(System.currentTimeMillis()) + "][" + type + "] " + s + "\n";
    consoleOutput.append(output);
}

无需创建新的 Date 对象,只需使用 System.currentTimeMillis() 并将其传递给 SimpleDateFormatter。接下来,只需使用 append 将文本追加到 JTextArea 的末尾,这将至少减少一个(如果不是更多的话)创建的临时对象的数量,从而减少 GC 循环的开销而不是留下一堆堆在堆中的短暂对象...

我 运行 通过探查器(在 Netbeans 中)使用 Integer.MAX_VALUE 作为循环终止符更新了代码,直到大约第 4000 个周期左右才看到使用的堆大小超过 10mb。 ..

现在,由于 JTextArea 的性质,我预计随着时间的推移内存使用量会略有增加,因为它需要将所有文本保存在内存中(以及相关的计算开销文本布局)

查看 Concurrency in Swing 了解有关 Swing 和线程的更多详细信息