It’s possible, with some catches.
Debugging must have been enable for the JVM at launch time already. To connect with your own JVM, you need to use either, a predefined port that the applications knows or the attach feature, which requires self-attach to be enabled explicitly for recent JVMs.
Then, since you have to suspend the thread you want to inspect, it can’t be the same thread performing the inspection. So you have to delegate the task to a different thread.
For example
public static void main(String[] args) throws Exception {
Object o = null;
int test = 42;
String s = "hello";
Map<String, Object> vars = variables();
System.out.println(vars);
}
// get the variables in the caller’s frame
static Map<String,Object> variables() throws Exception {
Thread th = Thread.currentThread();
String oldName = th.getName(), tmpName = UUID.randomUUID().toString();
th.setName(tmpName);
long depth = StackWalker.getInstance(
StackWalker.Option.SHOW_HIDDEN_FRAMES).walk(Stream::count) - 1;
ExecutorService es = Executors.newSingleThreadExecutor();
try {
return es.<Map<String,Object>>submit(() -> {
VirtualMachineManager m = Bootstrap.virtualMachineManager();
for(var ac: m.attachingConnectors()) {
Map<String, Connector.Argument> arg = ac.defaultArguments();
Connector.Argument a = arg.get("pid");
if(a == null) continue;
a.setValue(String.valueOf(ProcessHandle.current().pid()));
VirtualMachine vm = ac.attach(arg);
return getVariableValues(vm, tmpName, depth);
}
return Map.of();
}).get();
} finally {
th.setName(oldName);
es.shutdown();
}
}
private static Map<String,Object> getVariableValues(
VirtualMachine vm, String tmpName, long depth)
throws IncompatibleThreadStateException, AbsentInformationException {
for(ThreadReference r: vm.allThreads()) {
if(!r.name().equals(tmpName)) continue;
r.suspend();
try {
StackFrame frame = r.frame((int)(r.frameCount() - depth));
return frame.getValues(frame.visibleVariables())
.entrySet().stream().collect(HashMap::new,
(m,e) -> m.put(e.getKey().name(), t(e.getValue())), Map::putAll);
} finally {
r.resume();
}
}
return Map.of();
}
private static Object t(Value v) {
if(v == null) return null;
switch(v.type().signature()) {
case "Z": return ((PrimitiveValue)v).booleanValue();
case "B": return ((PrimitiveValue)v).byteValue();
case "S": return ((PrimitiveValue)v).shortValue();
case "C": return ((PrimitiveValue)v).charValue();
case "I": return ((PrimitiveValue)v).intValue();
case "J": return ((PrimitiveValue)v).longValue();
case "F": return ((PrimitiveValue)v).floatValue();
case "D": return ((PrimitiveValue)v).doubleValue();
case "Ljava/lang/String;": return ((StringReference)v).value();
}
if(v instanceof ArrayReference)
return ((ArrayReference)v).getValues().stream().map(e -> t(e)).toArray();
return v.type().name()+'@'+Integer.toHexString(v.hashCode());
}
When I run this on my machine with JDK 12 using the options
-Djdk.attach.allowAttachSelf -agentlib:jdwp=transport=dt_socket,server=y,suspend=n
, it prints
Listening for transport dt_socket at address: 50961
{args=[Ljava.lang.Object;@146ba0ac, s=hello, test=42, o=null}