Short answer: Yes, zram backing pages are automatically freed.
After checking by experiment (kernel 5.10.105), it seems that unused zram storage is automatically freed, even when the zram device is mounted without discard.
Summary: The script below runs a process that allocates a big chunk of memory.
zram usage (checked via zramctl) initially increases, and then goes back to baseline after stopping the process and evicting swapped pages.
# * I've run this on a freshly booted VM *
# zram is mounted with nodiscard to exclude any effects of
# `discard`.
sudo grep zram /etc/fstab
# /dev/zram0 none swap nodiscard,pri=5
# We have ~6 GiB of RAM
grep -i memtotal /proc/meminfo
# MemTotal: 6386852 kB
# Show zram usage.
# `DATA` is the total amount of uncompressed data currently stored in zram.
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 zstd 3.1G 4K 58B 4K 4 [SWAP]
# Start a process that allocates 10 GiB of RAM
stress-ng -- --vm-bytes $((10*1024**3)) --vm-keep --vm 1 &
# *Wait some time for the stress test command to be swapped out*
# zram usage has gone up from 4 KiB to 3.1 GiB
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 zstd 3.1G 3.1G 1.1G 1.2G 4 [SWAP]
# Stop stress test
kill %1
# zram usage decreased from 3.1 GiB to 0.3 GiB
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 zstd 3.1G 336.8M 48.6M 57.6M 4 [SWAP]
# Read the first byte of all memory pages of all processes.
# This evicts all non-kernel swapped pages without using `swapoff`, which might
# reset the zram device.
sudo ./read_all_mem_pages.rb
# Now zram usage is almost back to zero
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 zstd 3.1G 18.4M 3.5M 5.9M 4 [SWAP]
Source of read_all_mem_pages.rb:
#!/usr/bin/env ruby
def access_all_pages(pid)
name = File.basename(File.readlink("/proc/#{pid}/exe")) rescue return
puts "#{pid} (#{name})"
File.open("/proc/#{pid}/mem", 'r') do |mem|
for_each_mem_page(pid) do |page_address|
mem.seek(page_address)
mem.read(1) rescue nil
end
end
end
def for_each_mem_page(pid)
File.foreach("/proc/#{pid}/maps") do |line|
fields = line.split
range, dest = fields[0], fields[-1]
next if dest == "[vsyscall]"
start, end_ = range.split('-').map { |x| x.to_i(16) }
address = start
while address < end_
yield address
address += 4096
end
end
end
pids = Dir.children('/proc').grep(/^\d+$/).map(&:to_i)
pids.each { |pid| access_all_pages(pid) }