Java - Install Usage - Prefetch (Sensor)
This page shows how to create the Java - Install Usage - Prefetch sensor in the Tanium Console, and how to use it to decide which Java on a machine can be removed and which one is actually being used.
What it returns: one row per Java on a Windows machine. Each row gives the Java folder, its version, how it was installed, when it was installed, when it last ran, a plain-language usage summary, which Java programs ran, and the uninstall command.
Why it exists: a machine often has several Java installs at once: two or three Java 8 updates, a JDK, and Java bundled inside applications. A list of what is installed doesn't tell you which one is used. Windows Prefetch keeps a record of every program that runs, and this sensor ties each of those records to the exact Java folder that ran, so you can see which Java is in use and which has never run.
Replaces Java - Last Run - Prefetch
Java - Last Run - Prefetch shows that Java ran, but not which Java. This sensor shows everything that one did, tied to the Java that ran it, and it also tells you when Prefetch is switched on but not being written. Use this one going forward.
How the sensor works
1. Windows Prefetch names each file after the program's path
When a program runs, Windows writes a file to C:\Windows\Prefetch named <PROGRAM>-<HASH>.pf, for example JAVAW.EXE-4A2EFA53.pf. The 8-character hash is worked out from the full path of the program that ran, so the same program in two different folders gets two different files. The file's date changes every time the program runs.
2. The sensor works out that hash for every Java it finds
For each Java folder on the machine, the script calculates the hash that Windows would use for java.exe, javaw.exe, javaws.exe, and jp2launcher.exe in that folder, and looks for a Prefetch file with that name. A match proves that this exact Java ran, and the file's date says when.
It finds Java in:
- Installed programs in Apps and Features whose name looks like Java (Oracle, OpenJDK, Temurin, Corretto, Zulu, Semeru, Liberica)
- The JavaSoft registry keys
- Running Java processes
- Oracle's
javapathshortcut folders, and copies ofjava.exeinC:\Windows\System32orSysWOW64 - The Java that Tanium Comply ships inside the Tanium Client folder
- A folder search:
Program FilesandProgram Files (x86)2 levels deep, other top-level folders on each fixed drive 3 levels deep, and in each user profile the.jdksfolder, VS Code Java extensions, andAppData\Local\Programs
The folder search stops after 15 seconds. If it stops early, the sensor adds a Scan incomplete row.
3. Java that has been removed
When a Prefetch file doesn't match any Java on disk, the script also tries Oracle's standard folder names for Java 6, 7, and 8 (for example C:\Program Files\Java\jre1.8.0_503). A match there means that Java ran before it was removed, which is common after an update replaces the previous version.
4. Checking that Prefetch is working
Prefetch can be switched on but not written. This is common on Windows Server, where the SysMain service is usually turned off. A running machine writes Prefetch files all day, so if no Prefetch file anywhere is newer than 3 days, the sensor reports Unknown (Prefetch not recording) instead of claiming that Java never ran.
The sensor is read-only. It doesn't use WMI, doesn't compile code, doesn't write files, and works whether the Tanium Client runs it in 64-bit or 32-bit PowerShell.
Use cases
Choosing which Java to remove on a machine with several. Rows with No runs recorded or Ran only on install day are safe to remove. A row with Ran after install is the one people use.
Telling an update apart from real use. When Java updates, the installer itself runs javaws.exe. The sensor compares the last run date with the install date, so those runs show as Ran only on install day instead of looking like use.
Finding Java that hides inside applications. Bundled Java shows with Installed By set to No installer entry, so you can hand those to the application owner instead of uninstalling them.
Knowing when the answer can't be trusted. Servers that don't write Prefetch show Unknown (Prefetch not recording), so you know to check those another way before removing anything.
Before you start
- You need a role that can create sensors in the content set you plan to use.
- If a sensor with the same name already exists, edit it instead of creating a second one.
- Copy the script with the copy button on its code block so nothing is changed.
Create the sensor
Step 1: Open a new sensor
- Sign in to the Tanium Console.
- Open the main menu, then go to Administration > Content > Sensors.
- Click Create Sensor.
Step 2: Details section
| Field | Value |
|---|---|
| Name | Java - Install Usage - Prefetch |
| Description | Lists every Java on a Windows endpoint, one row per install, and shows whether and when it last ran, based on Windows Prefetch files. Each Prefetch file is matched to the exact Java folder that ran it, including Oracle Java 6, 7 and 8 folders that have since been removed. Returns the Java folder, file version, installer name, install date, last run date, days since last run, a usage summary, the Java programs that ran, and the uninstall command. Reports "Unknown (Prefetch not recording)" when Prefetch is on but not being written, which is common on servers. Read-only. Returns "None" when no Java is found. |
| Content Set | Your own custom content set |
Step 3: Settings section
| Field | Value |
|---|---|
| Category | Miscellaneous (or any category you use for custom sensors) |
| Result Type | Text |
| Max Sensor Age | 1 and set the unit dropdown to Hours |
| Max String Age | Leave Enable unchecked |
| Max Strings | Leave Enable unchecked |
| Ignore case in result values | Checked |
| Hide this sensor from sensor lists and parse results | Unchecked |
| Split into multiple columns | Checked |
Step 4: Column settings
- Set Delimiter to
|(the pipe character, Shift + Backslash on a US keyboard). - Add these 9 columns in this order. The order must match, because the script returns the values in this order.
| Index | Column Name | Value Type | Ignore case | Hidden |
|---|---|---|---|---|
| 0 | Java Home | Text | Checked | Unchecked |
| 1 | Version | Text | Checked | Unchecked |
| 2 | Installed By | Text | Checked | Unchecked |
| 3 | Installed On | Text | Checked | Unchecked |
| 4 | Last Run | Text | Checked | Unchecked |
| 5 | Days Ago | Numeric | Checked | Unchecked |
| 6 | Usage | Text | Checked | Unchecked |
| 7 | Launchers | Text | Checked | Unchecked |
| 8 | Uninstall Command | Text | Checked | Unchecked |
Installed On and Last Run are Text because they also hold N/A and Never. The yyyy-MM-dd format still sorts correctly.
Step 5: Parameters section
Leave it empty. This sensor has no parameters.
Step 6: Scripts section
For each platform in the table: click the platform tab on the left, check Enable sensor for [platform] platform, set Query Type in the top right, click inside the script editor, press Ctrl + A, press Delete, then paste the script for that platform.
| Platform | Enable | Query Type | Script |
|---|---|---|---|
| Windows | Checked | PowerShell | Windows script below |
| Linux | Checked | UnixShell | Stub below |
| Mac | Checked | UnixShell | Stub below |
| Solaris | Checked | UnixShell | Stub below |
| AIX | Checked | UnixShell | Stub below |
# Tanium sensor: Java - Install Usage - Prefetch
# One row per Java on the endpoint: which Java it is, how it was installed, and when it last ran.
# Windows Prefetch names each file <EXE>-<HASH>.pf, where HASH comes from the full path of the
# program that ran. The script works out that hash for every Java folder it finds, so each Java
# run is tied to the exact Java that ran it. Read-only: no WMI, no compiled code, no files written.
# Columns: Java Home|Version|Installed By|Installed On|Last Run|Days Ago|Usage|Launchers|Uninstall Command
# Version 1.1: one row per Java (no app/jre duplicates), Tanium Comply Java named, user profiles searched,
# Prefetch that is switched on but not being written is reported instead of trusted
$ErrorActionPreference = 'SilentlyContinue'
$NA = 'N/A'
function Out-Row([string[]]$v) {
($v | ForEach-Object { (("$_" -replace '[\r\n]+', ' ') -replace '\|', '/').Trim() }) -join '|'
}
try {
$sw = [Diagnostics.Stopwatch]::StartNew()
$searchSecs = 15
$MOD = [long]4294967296
$LOW16 = [long]65535
$wow64 = [Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess
$win = $env:SystemRoot
$sysDrive = $env:SystemDrive.ToUpperInvariant()
$pf64 = if ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }
$pf86 = ${env:ProgramFiles(x86)}
$launchers = @('JAVA.EXE', 'JAVAW.EXE', 'JAVAWS.EXE', 'JP2LAUNCHER.EXE')
$incomplete = $false
# ---------- Prefetch hash helpers (Windows Vista and later) ----------
$pow = New-Object 'System.Collections.Generic.List[long]'
$pow.Add(1)
function Get-Pow([int]$n) {
while ($pow.Count -le $n) { $pow.Add(($pow[$pow.Count - 1] * 37) % $MOD) }
return $pow[$n]
}
function Get-H0([string]$s) {
$h = [long]0
foreach ($c in $s.ToUpperInvariant().ToCharArray()) {
$v = [long][int]$c
$h = ($h * 37 + ($v -band 255)) % $MOD
$h = ($h * 37 + ($v -shr 8)) % $MOD
}
return $h
}
function MulMod([long]$a, [long]$b) {
return ((($a -band $LOW16) * $b) + (((($a -shr 16) * $b) -band $LOW16) * 65536)) % $MOD
}
function Add-Str([long]$state, [string]$s) {
return ((MulMod $state (Get-Pow (2 * $s.Length))) + (Get-H0 $s)) % $MOD
}
$volState = @{}
foreach ($n in 1..64) { $volState[$n] = Add-Str 314159 "\DEVICE\HARDDISKVOLUME$n" }
$lH0 = @{}; $lPow = @{}
foreach ($l in $launchers) { $lH0[$l] = Get-H0 ('\' + $l); $lPow[$l] = Get-Pow (2 * ($l.Length + 1)) }
# ---------- Prefetch files ----------
$pfDir = Join-Path $win 'Prefetch'
$pfMode = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters' -Name EnablePrefetcher).EnablePrefetcher
$pfState = 'On'
$allPf = @()
if ($pfMode -eq 0) { $pfState = 'Prefetch disabled' }
elseif (-not (Test-Path -LiteralPath $pfDir)) { $pfState = 'Prefetch folder missing' }
else {
try { $allPf = @(Get-ChildItem -LiteralPath $pfDir -Filter '*.pf' -File -ErrorAction Stop) }
catch { $pfState = 'Prefetch folder unreadable' }
}
# Prefetch can be switched on but not written (for example when the SysMain service is off, the
# default on Windows Server). A running machine writes Prefetch files all day, so no file newer
# than 3 days means "No runs recorded" would not be trustworthy.
if ($pfState -eq 'On') {
$newest = $allPf | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $newest -or ((Get-Date) - $newest.LastWriteTime).TotalDays -gt 3) { $pfState = 'Prefetch not recording' }
}
$pfByKey = @{} # 'JAVAW.EXE|4A2EFA53' -> run record
$byExe = @{} # 'CMD.EXE' -> list of hashes, used to find the system volume number
foreach ($f in $allPf) {
if ($f.Name -notmatch '^(.+)-([0-9A-F]{8})\.pf$') { continue }
$exe = $Matches[1].ToUpperInvariant(); $hex = $Matches[2].ToUpperInvariant()
if (-not $byExe.ContainsKey($exe)) { $byExe[$exe] = New-Object 'System.Collections.Generic.HashSet[string]' }
[void]$byExe[$exe].Add($hex)
if ($launchers -contains $exe) {
$pfByKey["$exe|$hex"] = [pscustomobject]@{ Launcher = $exe; LastRun = $f.LastWriteTime; Owner = $null; Via = '' }
}
}
# Find the volume number of the system drive from Prefetch files of standard Windows programs
$driveVol = @{}
$winRel = $win.Substring(2)
$calib = @(
"$winRel\System32\conhost.exe", "$winRel\System32\cmd.exe", "$winRel\explorer.exe",
"$winRel\System32\WindowsPowerShell\v1.0\powershell.exe", "$winRel\SysWOW64\WindowsPowerShell\v1.0\powershell.exe",
"$winRel\System32\wbem\WmiPrvSE.exe", "$winRel\System32\taskhostw.exe", "$winRel\System32\msiexec.exe",
"$winRel\System32\reg.exe", "$winRel\System32\sc.exe", "$winRel\System32\RuntimeBroker.exe"
)
:calibLoop foreach ($rel in $calib) {
$exe = [IO.Path]::GetFileName($rel).ToUpperInvariant()
if (-not $byExe.ContainsKey($exe)) { continue }
$h0 = Get-H0 $rel; $p = Get-Pow (2 * $rel.Length)
foreach ($n in 1..64) {
$h = ((MulMod $volState[$n] $p) + $h0) % $MOD
if ($byExe[$exe].Contains($h.ToString('X8'))) { $driveVol[$sysDrive] = $n; break calibLoop }
}
}
# ---------- Java on disk ----------
function Get-FsPath([string]$p) {
if ($wow64) { return ($p -replace '(?i)^([A-Z]:\\Windows)\\System32(\\|$)', '$1\Sysnative$2') }
return $p
}
function Test-HasJava([string]$dir) {
$fs = Get-FsPath $dir
return ([IO.File]::Exists("$fs\java.exe") -or [IO.File]::Exists("$fs\javaw.exe"))
}
function Get-SubDirs([string]$p) {
try { return [IO.Directory]::GetDirectories($p) } catch { return @() }
}
$entries = @{} # home (case-insensitive) -> entry
# -Always keeps a known path even when it is not on disk, so runs from it can still be named
function Add-Entry([string]$jh, [string]$kind, [switch]$Always) {
if (-not $jh) { return }
$jh = $jh.Trim().Trim('"').TrimEnd('\')
if ($jh.Length -lt 3) { return }
if ($kind -ne 'Folder') {
# Use the folder that holds bin\java.exe, so an application folder and its jre subfolder are one row
if (-not (Test-HasJava "$jh\bin") -and (Test-HasJava "$jh\jre\bin")) { $jh = "$jh\jre" }
# A JDK's own jre subfolder belongs to the JDK
if ($jh -match '(?i)\\jre$' -and (Test-HasJava ((Split-Path -Parent $jh) + '\bin'))) { $jh = Split-Path -Parent $jh }
}
if ($entries.ContainsKey($jh)) { return }
if ($kind -eq 'Folder') { $dirs = @($jh) } else { $dirs = @("$jh\bin", "$jh\jre\bin") }
$exists = @($dirs | Where-Object { Test-HasJava $_ }).Count -gt 0
if (-not $exists -and -not $Always) { return }
$entries[$jh] = [pscustomobject]@{
Home = $jh; Kind = $kind; Dirs = $dirs; Exists = $exists; Version = $NA; InstalledBy = ''; InstalledOn = $NA
Uninstall = 'None registered'; Runs = New-Object System.Collections.ArrayList
}
}
# Installer entries, JavaSoft registry, and Tanium Comply's own Java
$uninst = New-Object System.Collections.ArrayList
$jsHomes = New-Object System.Collections.ArrayList
$taniumHomes = New-Object System.Collections.ArrayList
foreach ($view in 'Registry64', 'Registry32') {
$base = $null
try { $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', $view) } catch { continue }
$uk = $base.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
if ($uk) {
foreach ($name in $uk.GetSubKeyNames()) {
$sk = $uk.OpenSubKey($name); if (-not $sk) { continue }
$dn = [string]$sk.GetValue('DisplayName')
$loc = ([string]$sk.GetValue('InstallLocation')).Trim().Trim('"').TrimEnd('\')
if ($dn -match '(?i)java|jdk|jre|openjdk|corretto|temurin|zulu|semeru|liberica' -and $loc) {
$cmd = 'None registered'
$us = [string]$sk.GetValue('UninstallString'); $qs = [string]$sk.GetValue('QuietUninstallString')
if ($name -match '^\{[0-9A-Fa-f-]{36}\}$' -and ($us -match '(?i)msiexec' -or $sk.GetValue('WindowsInstaller') -eq 1)) {
$cmd = "MsiExec.exe /X$name /qn /norestart"
} elseif ($qs) { $cmd = $qs } elseif ($us) { $cmd = $us }
[void]$uninst.Add([pscustomobject]@{ Name = $dn; Loc = $loc; Date = [string]$sk.GetValue('InstallDate'); Cmd = $cmd })
}
$sk.Close()
}
$uk.Close()
}
foreach ($jk in 'SOFTWARE\JavaSoft\Java Runtime Environment', 'SOFTWARE\JavaSoft\Java Development Kit', 'SOFTWARE\JavaSoft\JRE', 'SOFTWARE\JavaSoft\JDK') {
$k = $base.OpenSubKey($jk); if (-not $k) { continue }
foreach ($v in $k.GetSubKeyNames()) {
$vk = $k.OpenSubKey($v); if ($vk) { [void]$jsHomes.Add([string]$vk.GetValue('JavaHome')); $vk.Close() }
}
$k.Close()
}
$tk = $base.OpenSubKey('SOFTWARE\Tanium\Tanium Client')
if ($tk) { $tp = [string]$tk.GetValue('Path'); if ($tp) { [void]$taniumHomes.Add("$tp\extensions\comply\jre") }; $tk.Close() }
$base.Close()
}
[void]$taniumHomes.Add("$pf86\Tanium\Tanium Client\extensions\comply\jre")
[void]$taniumHomes.Add("$pf64\Tanium\Tanium Client\extensions\comply\jre")
foreach ($t in $taniumHomes) { Add-Entry $t 'Tanium' -Always }
foreach ($u in $uninst) { Add-Entry $u.Loc 'Install' }
foreach ($j in $jsHomes) { Add-Entry $j 'Install' }
# Running Java processes (finds Java bundled inside applications)
foreach ($p in Get-Process -Name java, javaw, javaws, jp2launcher) {
$path = $p.Path
if ($path) {
$dir = Split-Path -Parent $path
if ((Split-Path -Leaf $dir) -eq 'bin') { Add-Entry (Split-Path -Parent $dir) 'Install' } else { Add-Entry $dir 'Folder' }
}
}
# Oracle javapath folders and old copies in the Windows folder: java.exe sits in the folder itself
$pathRoots = @("$pf64\Common Files\Oracle\Java", "$pf86\Common Files\Oracle\Java", "$env:ProgramData\Oracle\Java")
foreach ($r in $pathRoots) { foreach ($d in Get-SubDirs $r) { Add-Entry $d 'Folder' } }
foreach ($d in "$win\System32", "$win\SysWOW64") { Add-Entry $d 'Folder' }
# Folder search: Program Files 2 levels deep, other folders on each fixed drive 3 levels deep
function Search-Homes([string]$root, [int]$depth) {
if ($depth -le 0 -or $sw.Elapsed.TotalSeconds -gt $searchSecs) {
if ($depth -gt 0) { $script:incomplete = $true }
return
}
foreach ($d in Get-SubDirs $root) {
if ($sw.Elapsed.TotalSeconds -gt $searchSecs) { $script:incomplete = $true; return }
Add-Entry $d 'Install'
Search-Homes $d ($depth - 1)
}
}
foreach ($r in @($pf64, $pf86) | Select-Object -Unique) { if ($r) { Search-Homes $r 2 } }
# Folders already searched (system drive) or that never hold Java
$skip = @('$Recycle.Bin', 'System Volume Information', 'Recovery', 'PerfLogs', 'Config.Msi', 'MSOCache', 'Documents and Settings')
$skipSys = @('Windows', 'Program Files', 'Program Files (x86)', 'ProgramData', 'Users')
foreach ($drv in [IO.DriveInfo]::GetDrives()) {
if ($drv.DriveType -ne 'Fixed' -or -not $drv.IsReady) { continue }
$isSys = $drv.Name.Substring(0, 2).ToUpperInvariant() -eq $sysDrive
foreach ($d in Get-SubDirs $drv.RootDirectory.FullName) {
$leaf = Split-Path -Leaf $d
if ($skip -contains $leaf -or ($isSys -and $skipSys -contains $leaf)) { continue }
if ($sw.Elapsed.TotalSeconds -gt $searchSecs) { $incomplete = $true; break }
Add-Entry $d 'Install'
Search-Homes $d 2
}
}
# User profiles: per-user JDKs (.jdks), VS Code Java runtimes, and apps under AppData\Local\Programs
$usersDir = Join-Path $env:SystemDrive 'Users'
foreach ($up in Get-SubDirs $usersDir) {
if ((Split-Path -Leaf $up) -match '^(Public|Default|Default User|All Users)$') { continue }
if ($sw.Elapsed.TotalSeconds -gt $searchSecs) { $incomplete = $true; break }
foreach ($d in Get-SubDirs "$up\.jdks") { Add-Entry $d 'Install' }
foreach ($x in Get-SubDirs "$up\.vscode\extensions") { foreach ($d in Get-SubDirs "$x\jre") { Add-Entry $d 'Install' } }
Search-Homes "$up\AppData\Local\Programs" 2
}
# ---------- Details for each Java ----------
foreach ($e in $entries.Values) {
foreach ($d in $e.Dirs) {
$fs = Get-FsPath $d
foreach ($x in 'java.exe', 'javaw.exe') {
if ($e.Version -eq $NA -and [IO.File]::Exists("$fs\$x")) {
$fv = (Get-Item -LiteralPath "$fs\$x").VersionInfo.FileVersion
if ($fv) { $e.Version = ($fv -replace ',', '.' -replace '\s', '') }
}
}
}
$best = $null
foreach ($u in $uninst) {
if ($e.Home -eq $u.Loc -or $e.Home.StartsWith($u.Loc + '\', [StringComparison]::OrdinalIgnoreCase)) {
if (-not $best -or $u.Loc.Length -gt $best.Loc.Length) { $best = $u }
}
}
switch ($e.Kind) {
'Tanium' { $e.InstalledBy = 'Tanium Comply (do not remove)' }
'Folder' {
if ($e.Home -match '(?i)\\Oracle\\Java\\') { $e.InstalledBy = 'Oracle javapath (runs the default Java)' }
elseif ($e.Home -match '(?i)^[A-Z]:\\Windows\\') { $e.InstalledBy = 'Copy in Windows folder' }
else { $e.InstalledBy = 'No installer entry' }
}
default { if ($best) { $e.InstalledBy = $best.Name } else { $e.InstalledBy = 'No installer entry' } }
}
if ($best) {
$e.Uninstall = $best.Cmd
if ($best.Date -match '^(\d{4})(\d{2})(\d{2})$') { $e.InstalledOn = "$($Matches[1])-$($Matches[2])-$($Matches[3])" }
}
if ($e.InstalledOn -eq $NA -and $e.Kind -ne 'Folder') {
$ct = (Get-Item -LiteralPath (Get-FsPath $e.Home)).CreationTime
if ($ct) { $e.InstalledOn = $ct.ToString('yyyy-MM-dd') }
}
}
# ---------- Tie each Java run to the Java that ran it ----------
function Find-Runs($e) {
foreach ($d in $e.Dirs) {
if ($d -notmatch '^[A-Za-z]:\\') { continue }
$drive = $d.Substring(0, 2).ToUpperInvariant(); $rel = $d.Substring(2)
$h0 = Get-H0 $rel; $p = Get-Pow (2 * $rel.Length)
if ($driveVol.ContainsKey($drive)) { $vols = @($driveVol[$drive]) } else { $vols = 1..64 }
foreach ($n in $vols) {
$st = ((MulMod $volState[$n] $p) + $h0) % $MOD
foreach ($l in $launchers) {
$h = ((MulMod $st $lPow[$l]) + $lH0[$l]) % $MOD
$key = $l + '|' + $h.ToString('X8')
if ($pfByKey.ContainsKey($key) -and -not $pfByKey[$key].Owner) {
$pfByKey[$key].Owner = $e
$driveVol[$drive] = $n
}
}
}
}
}
if ($pfByKey.Count -gt 0) { foreach ($e in $entries.Values) { Find-Runs $e } }
# Runs from Java folders that no longer exist: try the standard Oracle folder names
$open = @($pfByKey.Values | Where-Object { -not $_.Owner })
if ($open.Count -gt 0 -and $driveVol.ContainsKey($sysDrive) -and $sw.Elapsed.TotalSeconds -lt 25) {
$want = @($open | ForEach-Object { $_.Launcher } | Select-Object -Unique)
$sysState = $volState[$driveVol[$sysDrive]]
$pad = { param($a, $b) $a..$b | ForEach-Object { if ($_ -lt 10) { "0$_" } else { "$_" } } }
$groups = @(
@{ Prefix = '\Java\jre1.8.0_'; Major = '8'; Us = (& $pad 1 600); Tails = @('\bin') },
@{ Prefix = '\Java\jdk1.8.0_'; Major = '8'; Us = (& $pad 1 600); Tails = @('\bin', '\jre\bin') },
@{ Prefix = '\Java\jre1.7.0_'; Major = '7'; Us = (& $pad 1 121); Tails = @('\bin') },
@{ Prefix = '\Java\jdk1.7.0_'; Major = '7'; Us = (& $pad 1 121); Tails = @('\bin', '\jre\bin') },
@{ Prefix = '\Java\jre1.6.0_'; Major = '6'; Us = (& $pad 1 45); Tails = @('\bin') },
@{ Prefix = '\Java\jdk1.6.0_'; Major = '6'; Us = (& $pad 1 45); Tails = @('\bin', '\jre\bin') },
@{ Prefix = '\Java\'; Major = ''; Us = @('jre6', 'jre7', 'jre8', 'jre-1.8', 'jdk-1.8'); Tails = @('\bin') }
)
foreach ($root in @($pf64, $pf86) | Select-Object -Unique) {
if (-not $root -or $root.Substring(0, 2).ToUpperInvariant() -ne $sysDrive) { continue }
$rootRel = $root.Substring(2)
foreach ($g in $groups) {
$base = Add-Str $sysState ($rootRel + $g.Prefix)
foreach ($us in $g.Us) {
foreach ($tail in $g.Tails) {
$st = $base
foreach ($c in "$us$tail".ToUpperInvariant().ToCharArray()) {
$v = [long][int]$c
$st = ($st * 37 + ($v -band 255)) % $MOD
$st = ($st * 37 + ($v -shr 8)) % $MOD
}
foreach ($l in $want) {
$pw = $lPow[$l]
$h = ((($st -band $LOW16) * $pw) + (((($st -shr 16) * $pw) -band $LOW16) * 65536) + $lH0[$l]) % $MOD
$key = $l + '|' + $h.ToString('X8')
if ($pfByKey.ContainsKey($key) -and -not $pfByKey[$key].Owner) {
$jh = $root + $g.Prefix + $us
if (-not $entries.ContainsKey($jh)) {
# Same style as the file version of an installed build: 8u503 -> 8.0.5030
$ver = if ($g.Major) { '{0}.0.{1}0' -f $g.Major, [int]$us } else { $NA }
$entries[$jh] = [pscustomobject]@{
Home = $jh; Kind = 'Removed'; Dirs = @(); Exists = $false; Version = $ver
InstalledBy = 'Removed (folder no longer exists)'; InstalledOn = $NA
Uninstall = $NA; Runs = New-Object System.Collections.ArrayList
}
}
$pfByKey[$key].Owner = $entries[$jh]
}
}
}
}
}
}
}
# javapath and Windows-folder copies run the same build as an installed Java: credit the run to it
foreach ($r in $pfByKey.Values) {
$o = $r.Owner
if ($o -and $o.Kind -eq 'Folder' -and $o.Version -ne $NA) {
$twin = @($entries.Values | Where-Object { $_.Kind -eq 'Install' -and $_.Version -eq $o.Version })
if ($twin.Count -eq 1) { $r.Owner = $twin[0]; $r.Via = ' (javapath)' }
}
}
foreach ($r in $pfByKey.Values) { if ($r.Owner) { [void]$r.Owner.Runs.Add($r) } }
# ---------- Output ----------
$today = (Get-Date).Date
$rows = New-Object System.Collections.ArrayList
foreach ($e in $entries.Values) {
if ($e.Runs.Count -eq 0 -and ($e.Kind -eq 'Tanium' -or $e.Kind -eq 'Folder') -and -not ($e.Exists -and $e.InstalledBy -match '^(Copy in Windows folder|Tanium)')) { continue }
$last = $NA; $days = $NA; $names = 'None'
if ($e.Runs.Count -gt 0) {
$max = ($e.Runs | Sort-Object LastRun -Descending | Select-Object -First 1).LastRun
$last = $max.ToString('yyyy-MM-dd'); $days = [int]($today - $max.Date).TotalDays
$names = (($e.Runs | ForEach-Object { $_.Launcher.ToLowerInvariant().Replace('.exe', '') + $_.Via }) | Sort-Object -Unique) -join ', '
}
if ($pfState -ne 'On') { $usage = "Unknown ($pfState)" }
elseif ($e.Runs.Count -eq 0) { $usage = 'No runs recorded'; $last = 'Never' }
else {
if ($e.Kind -eq 'Removed') { $usage = 'Ran before removal' }
elseif ($e.InstalledOn -ne $NA -and $last -eq $e.InstalledOn) { $usage = 'Ran only on install day' }
elseif ($e.InstalledOn -ne $NA -and $last -lt $e.InstalledOn) { $usage = 'Last ran before this install' }
else { $usage = 'Ran after install' }
}
[void]$rows.Add((Out-Row @($e.Home, $e.Version, $e.InstalledBy, $e.InstalledOn, $last, $days, $usage, $names, $e.Uninstall)))
}
foreach ($r in $pfByKey.Values | Where-Object { -not $_.Owner }) {
$days = [int]($today - $r.LastRun.Date).TotalDays
[void]$rows.Add((Out-Row @('Not matched', $NA, 'Not matched', $NA, $r.LastRun.ToString('yyyy-MM-dd'), $days,
'Ran from a folder that was not found', $r.Launcher.ToLowerInvariant().Replace('.exe', ''), $NA)))
}
if ($incomplete) { [void]$rows.Add((Out-Row @('Scan incomplete', $NA, $NA, $NA, $NA, $NA, 'Folder search stopped at the time limit', $NA, $NA))) }
if ($rows.Count -eq 0) { Out-Row @('None', $NA, $NA, $NA, $NA, $NA, $NA, $NA, $NA) }
else { $rows | Sort-Object -Unique }
}
catch {
Out-Row @('Error: Script failed', $NA, $NA, $NA, $NA, $NA, $_.Exception.Message, $NA, $NA)
}
Step 7: Save
Scroll to the bottom of the page and click Save. If the console reports that the name already exists, cancel and edit the existing sensor.
Step 8: Check it on one machine
- Go to Interact.
- In the question bar, enter
Get Java - Install Usage - Prefetch from all machines with Computer Name contains "<hostname>", replacing<hostname>with a workstation you know has Java. - Check that you see 9 columns. If everything is in one column with
|inside the value, the column split was not saved. Edit the sensor and recheck Step 4.
Reading the results
Columns
| Column | What it tells you |
|---|---|
| Java Home | The Java folder, for example C:\Program Files\Java\jre1.8.0_511. For bundled Java it's the application's Java folder, for example C:\Program Files\ExampleApp\jre |
| Version | The file version of java.exe, for example 8.0.5110.33 for Java 8 Update 511 build 33, or 11.0.32.1. This is the same format Installed Applications and most vulnerability scanners show |
| Installed By | The name in Apps and Features, or one of the values in Installed By values |
| Installed On | The install date from Apps and Features, or the date the folder was created |
| Last Run | The last day any Java program in this folder ran, Never, or N/A when Prefetch can't be used |
| Days Ago | Days since Last Run. Use it to filter, for example Days Ago less than 30 |
| Usage | A summary, see Usage values |
| Launchers | Which Java programs ran: java, javaw, javaws, jp2launcher. (javapath) after a name means it ran through Oracle's shortcut folder |
| Uninstall Command | For MSI installs, a ready silent uninstall: MsiExec.exe /X{GUID} /qn /norestart. None registered when there is no uninstaller |
Example row (values are illustrative): C:\Program Files\Java\jre1.8.0_511|8.0.5110.33|Java 8 Update 511 (64-bit)|2026-10-28|2026-10-28|9|Ran only on install day|javaw, javaws, jp2launcher|MsiExec.exe /X{00000000-0000-0000-0000-000000000000} /qn /norestart
Usage values
| Usage | Meaning | What to do |
|---|---|---|
No runs recorded |
No Java program in this folder has run as far as Prefetch remembers | Safe to remove, unless it's bundled with an application |
Ran only on install day |
It ran only on the day it was installed. This is the installer, not a person | Safe to remove |
Last ran before this install |
Its last run is older than its install date, usually because it was reinstalled or repaired | Treat as not used |
Ran after install |
Something ran it after it was installed | In use. Check Days Ago and Launchers, and talk to the user before removing it |
Ran before removal |
A Java that has already been removed | Nothing to do |
Ran from a folder that was not found |
A Java program ran from a folder the search didn't reach | Use Java - Runtime Inventory - Dependencies on that machine to find it |
Unknown (Prefetch not recording) |
Prefetch is switched on but no file is newer than 3 days. Common on Windows Server | Don't use this sensor to decide. Check Used By in Java - Runtime Inventory - Dependencies |
Unknown (Prefetch disabled) |
Prefetch is turned off | Same as above |
Installed By values
| Installed By | Meaning |
|---|---|
The Apps and Features name, for example Java 8 Update 511 (64-bit) |
A normal install. Remove it with the Uninstall Command or the Uninstall MSI package |
No installer entry |
Java with no uninstaller: either copied or unzipped by someone, or bundled inside an application. If the folder sits inside an application's folder, it belongs to that application. Don't delete it; update the application |
Copy in Windows folder |
Old Java installers copied java.exe into C:\Windows\System32 or SysWOW64. These usually go away when that Java is uninstalled |
Oracle javapath (runs the default Java) |
Oracle's shortcut folder. When only one installed Java has the same version, its runs are credited to that Java instead |
Tanium Comply (do not remove) |
The Java that Tanium Comply uses. Leave it alone |
Removed (folder no longer exists) |
Java that ran here but has since been removed |
Using it in a Java cleanup
-
Ask the question with Computer Name, so every row is tied to a machine:
-
Workstations: remove installed Java whose Usage is
No runs recorded,Ran only on install day, orLast ran before this install, and that your vulnerability scanner flags. Keep the current patched version. Contact the user for anyRan after installrow before changing it. - Servers: Usage is usually
Unknown (Prefetch not recording). Ask Java - Runtime Inventory - Dependencies instead and use its Used By column. Servers usually run Java as a Windows service, and that sensor reads the service settings. - Bundled Java (
No installer entryinside an application folder): send the list to the application owners. Only an application update fixes it. - Copied Java (
No installer entryin a folder such asC:\Tools\jdk-17.0.12orC:\Downloads\jdk1.8.0_131): there is no uninstaller. Confirm nothing uses it with Java - Runtime Inventory - Dependencies, then delete the folder with a package. - Ask again after removal. Removed Java changes to
Ran before removal, or drops off if it never ran.
For a large result set, export the results to CSV and sort by Usage and Installed By.
No runs recorded is not proof on its own
The sensor only sees Java started through java.exe, javaw.exe, javaws.exe, or jp2launcher.exe. Some services load Java through their own program instead, for example a Tomcat service (Tomcat9.exe) or an application's own launcher. Those leave no Java Prefetch file. On workstations this is rare. On servers, always check Used By in Java - Runtime Inventory - Dependencies before removing anything.
Troubleshooting
| Result | Meaning | What to do |
|---|---|---|
| All values in one column | Split into multiple columns or the \| delimiter was not saved |
Edit the sensor and recheck Step 4 |
Unknown (Prefetch not recording) on workstations |
The SysMain service is stopped or Prefetch was turned off by a tuning script | Use Java - Runtime Inventory - Dependencies for those machines |
| Scan incomplete row | The folder search hit its 15-second limit | Ask again. The list for that machine may be missing some Java |
TSE-Error: Sensor evaluation timed out |
The script didn't finish in time, usually on a busy server with many folders | Ask again |
Same Java shows as Ran after install on many machines on the same date |
An update or deployment ran on that date | Check the date against your Java deployment schedule |
Not Applicable on Linux or Mac |
Expected | Those platforms are not covered |
Limits
- Windows only. Linux and macOS have no Prefetch.
- Prefetch remembers about 1,024 programs. On a busy machine, the record of something that hasn't run in a long time can be dropped. That shows as
No runs recorded, which still means it hasn't run recently. - Java started through a renamed program is not seen. See the warning in Using it in a Java cleanup.
- Removed Java is named only for Oracle Java 6, 7, and 8. A removed Java 11 or newer shows as
Ran from a folder that was not found, because those folder names carry the full version. - Per-user installs show
No installer entry. The sensor doesn't read installer entries from user profiles. - Last Run is a date, not a time, so results from many machines merge into fewer rows.