Search code examples
javaiojava-io

Is there a way to count the IO activity (file access, bytes read)?


I would like to be able to find out how often a running JVM accesses the disk and if possible what it accesses.
I would like for instance to know: does loading an image via ImageIO really load this into memory or does it just create a pointer to the file and access it when its really drawn to the screen? And other things like that. Is there a way to do this?

Update, I have added this:

public class SecurityManagerExtension extends SecurityManager {


    @Override
    public void checkRead(FileDescriptor fd)
    {
        logger.error("reading file1: {}", fd.toString());
        super.checkRead(fd);
    }

    @Override
    public void checkRead(String file)
    {
        logger.error("reading file2: {}", file);
        super.checkRead(file);
    }

    @Override
    public void checkRead(String file, Object context)
    {
        logger.error("reading file3: {} in context {}", file, context.toString());
        super.checkRead(file, context);
    }
}

I have set the Security Manager:

    SecurityManagerExtension secMan = new SecurityManagerExtension();
    java.lang.System.setSecurityManager(secMan);

Now, the new question is: Basically every file I load or gets loaded, apart from class files, it seems, causes a StackTrace like this:

Caused by: java.security.AccessControlException: access denied ("java.io.FilePermission" "C:\Program Files\Java\jdk-13.0.1\bin\awt.dll" "read")

If I comment the setSecurityManager() line out, everything runs normal again. I even tried to give my User full access to the JDK directory, so this cannot be a real permission problem. And it isnt, because if I go back to default, everything runs properly once more.

What am I doing wrong?


Solution

  • For tracing filesystem accesses of Java applications, native tracing facilities are always the first choice. On Windows, use Process Monitor to trace I/O. On Linux, use strace. Other platforms provide similar facilities.

    By tracing filesystem accesses directly in Java, you can work around environment limitations. For example, strace is unavailable in a container that lacks the CAP_SYS_PTRACE capability and the container host is not always accessible.

    To go the Java route, you can implement your own security manager by extending java.lang.SecurityManager. This class provides checkRead, checkWrite, and checkDelete methods that get called as soon as code attempts corresponding accesses.

    A sample implementation:

    public class TraceSecurityManager extends SecurityManager {
      public void checkRead(String file) {
        System.out.println("Read: " + file);
      }
    
      public void checkRead(String file, Object context) {
        System.out.println("Read: " + file);
      }
    
      public void checkWrite(String file) {
        System.out.println("Write: " + file);
      }
    
      public void checkDelete(String file) {
        System.out.println("Delete: " + file);
      }
    }
    

    For testing the sample, we use the Java compiler as our test subject. To enable the trace security manager, we set the appropriate system property and execute the command with a valid Java source file Test.java:

    $ java -Djava.security.manager=TraceSecurityManager com.sun.tools.javac.Main Test.java
    Read: /home/user/com/sun/tools/javac/resources/spi/compilerProvider.class
    Read: /home/user/com/sun/tools/javac/resources/compiler_en.properties
    Read: /home/user/com/sun/tools/javac/resources/compiler_en_US.properties
    Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "getenv.JDK_JAVAC_OPTIONS")
            at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
            at java.base/java.security.AccessController.checkPermission(AccessController.java:897)
            at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:322)
            at java.base/java.lang.System.getenv(System.java:999)
            at jdk.compiler/com.sun.tools.javac.main.CommandLine.appendParsedEnvVariables(CommandLine.java:252)
            at jdk.compiler/com.sun.tools.javac.main.CommandLine.parse(CommandLine.java:99)
            at jdk.compiler/com.sun.tools.javac.main.CommandLine.parse(CommandLine.java:123)
            at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:215)
            at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:170)
            at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:57)
            at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:43)
    

    The trace implementation works. We can even see class loading attempts. However, javac fails because permissions are missing. The reason is that, with the installation of the security manager via the system property, the default Java security policy is active and does not grant the required permission. To work around that, you can either provide a minimal custom policy or you can override the checkPermission method with an empty implementation. In this case, I chose the minimal policy:

    grant {
      permission java.security.AllPermission "", "";
    };
    

    With the policy in place, we can retest:

    $ java -Djava.security.policy=test.policy -Djava.security.manager=TraceSecurityManager com.sun.tools.javac.Main Test.java
    Read: /home/user/com/sun/tools/javac/resources/spi/compilerProvider.class
    Read: /home/user/com/sun/tools/javac/resources/compiler_en.properties
    Read: /home/user/com/sun/tools/javac/resources/compiler_en_US.properties
    Read: Test.java
    Read: Test.java
    Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
    Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
    Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/jfxrt.jar
    Read: /home/user/META-INF/services/java.nio.file.spi.FileSystemProvider
    Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
    Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
    Read: Test.java
    Read: /home/user/Test.java
    Read: ./.bash_logout
    Read: /home/user/.bash_logout
    Read: ./.bash_profile
    Read: /home/user/.bash_profile
    Read: ./.bashrc
    Read: /home/user/.bashrc
    [...]
    Read: /home/user/com/sun/tools/javac/resources/spi/ctProvider.class
    Read: /home/user/com/sun/tools/javac/resources/ct_en.properties
    Read: /home/user/com/sun/tools/javac/resources/ct_en_US.properties
    Read: /home/user/TraceSecurityManager.class
    Read: /home/user/Test.class
    Read: /home/user
    Write: /home/user/Test.class
    Read: Test.java
    

    This time around, we got a full filesystem access trace of javac. The security manager can also be enabled at runtime, which is useful if you cannot control the Java command line for whatever reason:

    System.setSecurityManager(new TraceSecurityManager());
    

    In that particular case, a custom policy is not necessary because the default policy is not active.

    Using a security manager to trace filesystem accesses is certainly not the best option as details are missing that might be relevant to your debugging scenario, but it's a good compromise if you're out of alternatives and need to get things done.