Something that none of the other answer's touch on is that x === y does not mean "Do x and y have the same address in memory?" as your questions suggests. Rather, as the documention for === states, what the x === y operation does is to:
Determine whether x and y are identical, in the sense that no program could distinguish them.
This is the “egal“ predicate, as introduced in Henry Baker's famous paper "Equal Rights for Functional Objects or, The More Things Change, The More They Are the Same". This paper has the insight that when comparing two objects, you don’t really care where they happen to live in memory, which is an implementation detail that could change. Instead what you really want to know is if there's some program you could write that would distinguish between them, which depends only on the semantics of the language. The paper then goes on to explore what that means for mutable and immutable data structures.
If two objects are immutable and have the same type and value, then you cannot tell them apart, even if there happen to be two copies of the value somewhere in the memory of the program. Therefore they are === (i.e. egal). It doesn't matter whether 1 originates from two different assignments or not, as your examples show. Your intuition seems to be that when someone writes a = 1 and later b = 1 a new 1 object is created each time 1 appears. But there could equally be a single globally shared 1 object which the literal syntax 1 fetches a reference to. In fact, this is case: small integer values are allocated statically and the same instance is used any time a small integer literal occurs. This is true in other languages as well, such as Python:
# Python
>>> a = 1
>>> b = 1
>>> a is b
True
Languages do this if they regularly need “boxed” instances of integer values because allocating many boxed copies over and over again would be inefficient. Instead it’s standard to have a static “cache” of small boxed integers. However, there are too many integers to cache them all, so for large enough integers this doesn't happen:
# Python
>>> a = 1234
>>> b = 1234
>>> a is b
False
The cutoff in the Python version I'm using (3.9) happens to be 256:
# Python
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
But of course, this could change if they decide to make the cache bigger or smaller, which is somewhat unfortunate. Even if it never changes it’s definitely a leakage of an implementation detail. This aside about languages caching small integers serves to show you that even if we based === on address like Python does 1 === 1 would still be true. It isn't based on address, however, it compares types and values for integers of any size:
julia> a = 9223372036854775807; # typemax(Int)
julia> b = 9223372036854775807; # typemax(Int)
julia> a === b
true
Why not compare integers by address? We don't want to force immutable objects to even have a well-defined notion of address. In a well optimized program, an integer value will often not be stored in memory at all. It may only ever exist in registers, or it might be eliminated entirely. If the same constant value is used in multiple places, you may want to avoid wasting multiple registers storing it and just keep it in a single register that you use multple times. If the compiler had to keep track of allocation semantics for integers, then it would prohibit all sorts of optimizations.
Next let's look at your last example:
julia> a = [1,2]
2-element Vector{Int64}:
1
2
julia> b = [1,2]
2-element Vector{Int64}:
1
2
julia> a !== b
true
Why does the same array literal syntax produce arrays that are !==? Why doesn't the same logic that we used for integers above apply? First, let's show that you can write a program that distinguishes a from b:
julia> a[1] = -1
-1
julia> a == b
false
We mutated a and didn't mutate b after which, they no longer have equal contents, so they are easily distinguishable. Thus they cannot be ===. Why couldn't we do the same thing with integers? Because they're immutable, so you cannot change their contents. This isn't just some incidental feature of mutable arrays, rather it is their defining feature: they are containers with identity independent of their contents, which must therefore be associated with some location in memory so that they can behave coherently when one part of a program changes their content and some other part of a program observes that change.
What about the middle example with tuples? Unlike arrays, tuples are immutable—like integers, they are their values, they have no identity apart from what they contain. Thus, (1, 2) === (1, 2) doesn't care if there's one tuple literal or two: they're indistinguishable, identifiable only by their contents, which is, in this case, the same. Here's a slight variation:
julia> a = (1, [2])
(1, [2])
julia> b = (1, [2])
(1, [2])
julia> a === b
false
Why are these not ===? They're immutable tuples with the same contents? Well, what does "the same" mean? The correct definition is to apply === on the contents recursively, and since the two arrays are not the same array even though they both currently hold the value 2, these tuples are not recursively indistinguishable. Or to put it more succinctly:
julia> a[2][1] = 0
0
julia> a
(1, [0])
julia> b
(1, [2])
julia> a == b
false
They're not === because we can reach into the array in each tuple and change its contents. Here's one final example:
julia> v = [2];
julia> a = (1, v)
(1, [2])
julia> b = (1, v)
(1, [2])
julia> a === b
true
Now the two separately constructed tuples containing mutable arrays are the same! That's because they both contain the same mutable vector as their second component. In other words:
- the tuples are
=== if their components are all recursively ===
1 and 1 are === because integers are immutable and they have the same type and value
v and v are === because they're the same vector
Hopefully that clears things up.