String.intern()
内存布局划分为三个部份:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包含两类信息:
主流访问方式有:句柄、直接指针
虚拟机划分出一块句柄池,reference保存句柄的地址,句柄保存对象实例数据和对象类型数据的指针。
好处在于当对象被移动的时候(GC的时候可能会发生),只会改变句柄中的实例数据指针,reference存放的句柄地址不用变更。
直接存储对象的地址,好处是只需要一次寻址就能访问到对象实例数据。
引用数+1则计数+1。简单的引用计数难以解决循环引用问题。
首先有一系列的GC Roots,从这些根出发,不可达的对象为已死。
固定作为GC Roots的对象包括:
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
关于虚引用,这里详细介绍一下。如上所述虚引用用来当发生GC并且对象内存被回收的时候收到通知,另一种在GC时收到通知的方法是重载对象的Object.finalize
方法,这个方法在对象被确认可以GC的时候会被调用,用户可以重载这个方法做点事情,比如释放资源,但是finalize有很多问题:
因此官方推荐使用PhatomReference或者Cleaner来实现对象被GC后的工作。并且第三点「对象复活问题」是「为什么不用弱引用通知对象被GC,而是使用虚引用」的原因:结合弱引用的定义,经过测试发现,当对象确定只被弱引用,并且发生GC时,referent会从弱引用中删除,且弱引用进入引用队列。而finalize如果将对象复活,但此时弱引用已经进入引用队列,因此不能用弱引用来实现对象GC的通知,因为对象被复活了,没有被GC掉。
用一句话概括就是,虚引用用于通知对象的内存真正被回收,换句话来说,我认为如果对象不会被复活的话,弱引用可以替代虚引用。
关于Object.finalize
方法,深入理解Java虚拟机一文中:
finalize()能做的所有工作,使用try-finally 或者其他方式都可以做得更好、 更及时,所以笔者建议大家完全可以忘掉Java语言里面的这个方法
跨代最直接的解决方法是扫描整个老年代,但是效率低下。依据跨代引用假说,只需要在新生代建立一个“记忆集”,将老年代划分为若干小块,标识出哪一块内存存在跨代引用,当发生minor GC的时候只需要将这些存在跨代引用的内存块加入GC Roots。
标记所有需要回收的对象,然后统一回收这些对象。有两个缺点:标记清除的效率随着待回收对象的增加而降低、空间碎片化
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后把原来那块内存一次清理掉。缺点是当存活的对象占大多数时,效率较低,并且能用的空间只有一半。
还有一种非针对新生代内存的标记复制,不需要根据1:1比例划分内存的Appel式回收,空间分为一块大的eden,两块小的survivor,平时只有eden和其中一块survivor工作,另一块survivor用于minor GC的时候存放存活的对象,然后一次性把eden和另一块survivor清空。如果survivor空间太小的话导致放不下存活对象,那么会触发分配担保机制,把这些存不下的对象直接进入老年代。
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记整理算法直接将存活对象往内存空间一端移动:
如果在老年代中使用这样的策略,那么每次GC都会移动大量存活对象,而且必须暂停用户应用程序,即"Stop The World"。但是如果不移动对象的话,势必要增加内存管理的复杂度(比如使用空闲链表),降低了分配内存的效率,但不移动对象的话,STW的时间会更短。
三色标记法:对象图中的对象标记成三种颜色
以上情况1为正常标记完成的情况,最终白色对象将会被回收。情况2,3为在收集器标记的过程中,用户程序并发地将灰色对象对白色对象的引用取消了,并增加黑色对象对该白色对象的引用,由于黑色对象及其所有引用不会再被扫描,因此该白色对象无法被扫描,最终被错误地回收。
因此错误地回收对象,需要同时满足以下两个条件:
只需要破坏其中任意一个条件,就不会发生对象被错误回收的情况,以下是两种解决方案:
增量更新和原始快照都通过写屏障实现。CMS使用增量更新,G1、Shenandoah使用原始快照。
基于标记-复制的新生代收集器,serial不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,即STW。它是所有收集器里额外内存消耗最小的,而且收集几十兆甚至一两百兆的新生代,STW完全可以控制在十几、几十毫秒,最多一 百多毫秒以内。
基于标记-复制的新生代收集器,ParNew收集器实质上是Serial收集器的多线程并行版本,并与作为老年代收集器的CMS配合使用。ParNew收集器是激活CMS后的默认新生代收集器。
也是基于标记-复制的新生代收集器。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,主要适合在后台运算而不需要太多交互的分析任务。可以通过指定参数开启自适应的调节策略,以提供最合适的停顿时间或者最大的吞吐量。
serial的老年版本,基于标记-整理算法的老年代收集器
Parallel Scavenge的老年版本,基于标记整理的老年代收集器。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
以最短STW时间为目标,基于标记-清除、增量更新的收集器,适合交互频繁型应用。运作过程包括四个步骤:
缺点:
目的是实现停顿时间模型:支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
在G1之前的其他收集器,收集目标要么是全体新生代(Minor GC)、要么是全体老年代(Major GC)、要么是整个Java堆(Full GC)。而G1是Mixed GC模式,面向整个堆,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
将连续的Java堆划分成多个大小相等的独立区域(region),region是最小回收单元,每一个Region都可以根据需要动态地扮演着新生代的Eden空间、Survivor空间,或者老年代空间。Region中还有一类特殊的Humongous区域,专门用来存储大对象(大小超过一半region大小的对象),G1的大多数行为都把Humongous Region作为老年代。
G1停顿时间模型的实现就建立在:它将Region作为单次回收的最小单元,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
具体地说,G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
G1收集器的运行过程大致为四个步骤:
只有并发标记没有STW,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。
G1 vs CMS:
加载、链接、初始化、使用、卸载
必须进行类初始化的情况有:
加载三步骤:
包括文件格式验证、元数据验证、字节码验证、符号引用验证。
如果代码事先经过验证,可以用参数关闭类加载过程中的验证,以缩短加载时间。
为类静态变量分配内存,并初始化为零值,在初始化阶段才会putstatic设置为用户指定的初始值。而对于final的静态变量,则会在准备阶段就会初始化成用户指定的初始值。
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
Java虚拟机才真正开始执行类中编写的Java程序代码,初始化阶段就是执行类构造器
public class Test {
static {
i = 0; // ok
System.out.print(i); // not ok
static int i = 1;
}
}
虚拟机保证了clinit在多线程环境下的同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的clinit方法,如果clinit有耗时操作,那么可能会造成多个线程阻塞。clinit最好耗时短或者提前加载。即同一个类加载器下,一个类型只会被初始化一次。
类加载过程中,「通过一个类的全限定名来获取描述该类的二进制字节流」这个动作放到虚拟机外部去实现,让应用程序自己决定如何获取所需的类,实现的代码称为类加载器,可以用于实现类层次划分、OSGi、程序热部署、代码加密等技术。
首先在Java虚拟机中任意一个类的唯一性由类加载器和类本身共同确定,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,只要加载它们的类加载器不同,那这两个类就必定不相等。
在虚拟机的视角,只有两种类加载器:
自JDK1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构。
JDK8及以前的版本绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载:
这里类加载器之间的关系通常不是继承,而是使用组合来复用父加载器的代码。
双亲委派模型的工作过程:类加载器将收到的类加载请求先交给父加载器完成,最终传送到启动类加载器。如果父加载器无法完成这个请求(它的搜索范围内没有找到所需的类),子加载器才会尝试自己去完成加载。
双亲委派模型这种工作模式的好处是,具备了一种带有优先级的层次关系,比如java.lang.Object是由启动类加载器加载的,无论哪个类加载器来加载这个类,最终都由启动类加载器加载,那么即使用户自己也编写了一个名为java.lang.Object的类,运行时加载的也是rt.jar里面的那个java.lang.Object,保证了Java类型体系中最基础的行为,避免造成混乱。
如果不想破坏双亲委派模型,那么自定义加载器的时候,重写ClassLoader
的findClass
方法即可。如果想打破的话,那么需要重写loadClass
方法,目的是重写加载类的流程。
比如 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader
来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理:
CommonClassLoader
是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离CatalinaClassLoader
用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类SharedClassLoader
作为 WebAppClassLoader
的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、MybatisWebAppClassLoader
,各个 WebAppClassLoader
实例之间相互隔离,进而实现 Web 应用之间类的隔离但是单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。
比如应用程序中的业务类实现了Spring中的接口,这些接口属于Web 应用之间共享的,由SharedClassLoader加载,并且Spring内部需要访问这些业务实现类,但是SharedClassLoader无法找到业务类,因此就需要用到线程上下文类加载器(ThreadContextClassLoader),当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的,一般的格式如下:
try {
// 保存原始ClassLoader
ClassLoader curCl = Thread.currentThread().getContextClassLoader();
// 获取并设置所需的ClassLoader
ClassLoader cl = 比如WebAppClassLoader;
Thread.currentThread().setContextClassLoader(cl);
// 使用设置的ClassLoader加载一些类,处理一些框架内的逻辑等
loadClass();
} finally {
// 还原
Thread.currentThread().setContextClassLoader(curCl);
}