Java
内存区域
HotSpot在JDK1.8之前方法区就是永久代,永久代就是方法区。
JDK1.8后删除了永久代,改为元空间,元空间在直接内存中。方法区就是元空间,元空间就是方法区。
创建一个线程,JVM就会为其分配一个私有内存空间,其中包括PC、虚拟机栈和本地方法栈
PC
用来指示下一个执行的字节码指令,基于这一点就能实现代码的控制流程
为了确保每个线程切换回来都能从上次的位置继续运行,PC必须是线程私有的,切换出去时需保存各自的PC
虚拟机栈
虚拟机栈中一个栈帧压入就对应一个方法的调用,栈帧弹出就对应方法返回。栈帧中包含:局部变量表、操作数栈、动态链接、方法出口信息。
局部变量表也就是常说的栈内存,用来存储基本类型和引用
HotSpot不支持动态扩展虚拟机栈,在创建线程时就确定了虚拟机栈的最大深度,如果申请不了这么多内存,就会抛出OOM错误,如果线程在运行时调用了很多方法,到达了栈的最大深度,就会抛出SOF错误
本地方法栈
和虚拟机栈相同,区别仅在于虚拟机栈中是java方法,本地方法栈中是native方法,但HotSpot中已经将二者合而为一了。
堆
JVM中最大的一块内存空间,所有对象实例和数组都在这里分配内存,所有线程共享堆内存。
JDK1.7开始默认开启了逃逸分析,如果一个对象只在一个线程中被引用了,则该对象可以直接在栈上分配内存空间。
堆也叫GC堆,是垃圾回收的主要区域。为了便于垃圾回收,JDK1.8之前将堆分为三个部分:
- 新生代
- 老年代
- 永久代
而1.8之后将永久代删除了,取而代之的是元空间,元空间则在直接内存中。
此外,新生代还细分为eden、from survivor(s0)和to survivor(s1)
当新对象实例产生时,年龄为0,首先被分配在eden里,在一次gc后,如果还存活,就被扔到survivor里,并且年龄+1,当年龄增加到一定程度后就被扔到老年代里。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
方法区
存放被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.8之前是永久代,属于堆内存。1.8之后是元空间,属于直接内存。
永久代受JVM创建时分配的最大堆内存限制,而元空间则受系统内存限制,可以存储更多。
常量池
分为字符串常量池和运行时常量池
1.8之后字符串常量池在堆中,而运行时常量池在元空间
直接内存
在JVM进程的内存空间之外,属于系统内存。
JVM可通过native方法对其进行直接操作,而无需在使用时将其拷贝到JVM内存区。
对象创建过程
类加载检查:
JVM遇到一条new指令时,先检查能不能在常量池中定位到该类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有就要先进行类加载。类已被加载就通过检查。
分配内存:
类加载检查通过后JVM为新对象分配内存,对象所需的内存大小在类加载完成后就确定了,JVM会在堆中按照指针碰撞或空闲列表的方式为对象划分出一块空间,选择哪种方式会根据垃圾收集器的算法而定。此外,内存分配还要保证线程安全,JVM采用CAS+失败重试或TLAB的方式保证线程安全。
CAS+失败重试:乐观锁的一种实现,每次占用资源不加锁,而是不断尝试占用。
TLAB:线程创建时预先在堆中给线程分配一块内存,称为TLAB,专门用来存放该线程运行过程中创建的对象,而TLAB满了时,采用上述CAS在堆的其它内存中分配
初始化零值:
将对象的字段设为默认零值,不包括对象头
设置对象头:
在对象头中设置这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄、是否启用偏向锁等信息
执行init方法:
初始化对象,即按照程序员写的构造方法给对象进行初始化。
GC
内存分配与回收
新对象优先被分配在eden里,年龄设置为0,经历第一次gc后如果还存活,就被扔到survivor中,并且年龄+1,随后每经历一次gc如果还存活就年龄+1,如果年龄超过了上限(默认15),就被扔到老年代中。通过-XX:MaxTenuringThreshold设置上限。
此外,JVM还有动态年龄判定机制:如果在survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到年龄超过上线。
如果是过大的对象,为了避免其来回复制,可以在创建时直接扔到老年代里。通过-XX:PretenureSizeThreshold设置,只支持Serial和ParNew。
hotspot中gc分为两类:部分收集partial gc和全收集full gc。
部分收集又分minor gc、major gc和mixed gc
minor gc:只对新生代进行回收
major gc:只对老年代进行回收(有时也指代full gc)
mixed gc:对整个新生代和部分老年代进行回收
全收集full gc:对整个堆和方法区(hotspot中的元空间)进行回收
判别对象、常量、类死亡的方法
- 对象
引用计数法:
对象中设置一个引用计数器,每当该对象被引用时,引用计数器就会+1,失去一个引用时就会-1。引用计数器为0时就代表已经死亡,不会再被引用了。这种判别方式有个缺点,如果两个对象互相引用,但又没有外界对它们的引用,则它们引用计数都为1,会一直存在,但没有意义。
可达性分析法:
设置一组对象为gc roots,如果一个对象没有能到达任何一个gc root的引用链,则判别这个对象死亡。一般一个线程启动后并列创建的一组对象会构成gc roots,gc roots内部引用的对象就是非gc root。
- 常量
没有被任何对象引用时就是废弃的
- 类
同时满足如下三点就是无用的类
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类 的方法。
引用类型都有哪些?
强引用
Object strong = new Object();
一个对象如果被引用,且最高级别是强引用,就不会被回收。
软引用
SoftReference<Object> soft = new SoftReference<>(new Object());
一个对象如果被引用,且最高级别是软引用,发生gc时内存足够就不会被回收,内存不够就会被回收。
弱引用
WeakReference<Object> weak = new WeakReference<>(new Object());
一个对象如果被引用,且最高级别是弱引用,发生gc时不管内存够不够都会被回收。
虚引用
ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantom = new PhantomReference<Object>(new Object(), queue);
虚引用创建时必须搭配ReferenceQueue。
一个对象如果被引用,且最高级别是虚引用,就等于没有被引用,发生gc时不管内存够不够都会被回收。
虚引用看起来和弱引用没啥区别,只是必须搭配ReferenceQueue。
用虚引用的目的一般是跟踪对象被回收的活动。
ReferenceQueue
软引用、弱引用和虚引用在创建时都可以关联一个ReferenceQueue,其中虚引用必须关联,其余两个可选关联。
关联了ReferenceQueue的引用所引用的对象在被回收内存之前,这个引用会被JVM加入到关联的ReferenceQueue中。通过这样的机制,我们就能通过监听该队列,在对象内存被回收前进行一些自定义处理。
在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
GC算法有哪些?
标记-清除 :先标记上所有存活的对象,再一次性回收掉没被标记的,这是最基础的算法,后面的几个都是对其效率和空间碎片问题做了优化。 碎片问题会导致都是小空隙,装不下大对象,而如果将对象整理起来就会空出更大的空隙。
复制 :将内存分为两块,只用其中一块,当用的这一块满了后,还是对其中存活的对象进行标记,然后将这些被标记的逐个复制到另一块内存,最后将剩余的死亡对象一次性回收,说白了就是两块内存来回倒。由于一次gc需要处理的总内存变小了,效率也就提升了。
标记-整理 :和标记-清除一样,先标记存活对象,然后将它们堆到一端,最后回收掉末端以外的对象。
分代收集 :新生代死亡对象比较多,一般用复制算法。老年代死亡对象比较少,一般用标记-清除或标记-整理算法
类加载流程
加载
将字节流读入JVM内存,在方法区存储为一个数据结构,同时创建一个对应的Class对象供程序访问。
验证
一共4步:
文件格式验证
验证读进来的字节流是否符合Class标准格式。
元数据验证
格式对了之后,验证一下里边的数据合不合理。
比如:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
字节码验证
分析类的方法体(Class文件中的Code属性),确保方法在运行时不会危害虚拟机。
符号引用验证(发生在解析阶段)
检查常量池中引用的外部类是否存在,是否可以正常访问。
准备
类变量(静态变量)分配内存并设置初值
其中如果是final修饰的,意味着在Class文件中,该字段的属性表中存在ConstantValue属性,此时初值设置为代码里写的。
如果不是,就设置为零值,等到初始化阶段再赋值。
解析
把符号引用(地址无关)转化为直接引用(地址相关)
初始化
执行clinit方法,这里要注意不是构造方法,而是执行静态语句,包括静态变量赋值和静态块