For non-simple programs that's nearly impossible. The response above shows how to determine which classes are used during one single invocation; another invocation with other parameters may need other classes because other functionality is used.
Analyzing the code using reflection is also not feasible if advanced features are used (where advanced features start already with stuff like XML and
JDBC).
JDBC is a good sample to explain why this causes problems: The old way to configure program with the JDBC driver was to call Class.forName( "JDBCDriverClass" ). Obviously there is no reference to this class in the binary code of that program. And when the name of the JDBCDriverClass is not hardcoded but read from somewhere, the program will never have any static reference of any kind to the jar holding the JDBC driver.
Similar scenarios can be found for XML parsers, Script engines, JNDI providers, Security providers, Encryption providers, Codecs, URL protocols ... I bet I missed a few ...
This also shows that different sessions with a program may use different classes from different jars: just assume a program like Squirrel that can be used to visualize different RDBMS databases.
On the other hand, if you have a simple, straight-forward program that does not use any kind of runtime plug-in mechanism, it could be possible to determine the really necessary jars: when you compile start with an empty CLASSPATH and add the jars until the compiler does not complain any longer.
This can be done also with running the program, but you may get surprised if someone uses a function that was not included into your tests - and now causes a CNF-Exception. But again, this will only work for really straight-forward programs.