If you look how vectors are stored, this is understandable. A Vector{Int} is really a struct:
julia> dump(Vector{Int})
mutable struct Vector{Int64} <: DenseVector{Int64}
ref::MemoryRef{Int64}
size::Tuple{Int64}
And a MemoryRef{Int} is also a struct:
julia> dump(MemoryRef{Int})
struct MemoryRef{Int64} <: Ref{Int64}
ptr_or_offset::Ptr{Nothing}
mem::Memory{Int64}
And so is Memory{Int}:
julia> dump(Memory{Int})
mutable struct Memory{Int64} <: DenseVector{Int64}
const length::Int64
const ptr::Ptr{Nothing}
In addition comes the actual memory for the data, i.e. the chunk of memory pointed to by the ptr in the Memory struct. The structs also have their type information encoded, at least a pointer to a static DataType struct, that’s 8 bytes.
When you create a as a = [2], the data buffer has only room for 1 Int, but if you push more to the vector it’s reallocated to make room for more:
julia> a = [2]
1-element Vector{Int64}:
2
julia> a.ref.mem.length
1
julia> @allocated push!(a, 1)
96
julia> a.ref.mem.length
8
julia> @allocated push!(a, 1)
0
julia> a.ref.mem.length
8
Removing from the front of the vector just changes the ptr_or_offset in the MemoryRef:
julia> a.ref.ptr_or_offset
Ptr{Nothing}(0x00007f73096751c0)
julia> popfirst!(a)
2
julia> a.ref.ptr_or_offset
Ptr{Nothing}(0x00007f73096751c8)
It’s also possible to get hold of the DataType pointer which is at the word before the Vector{Int} object, but the lower 4 bits are used temporarily by the garbage collector, and the rest doubles as different types of data, so this is highly unsafe, and easily gives segfaults:
julia> unsafe_load(Ptr{DataType}(unsafe_load(Ptr{UInt}(pointer_from_objref(a)), 0) & ~UInt(0xf)))
Array{Int64, 1}