JVM 笔记
对象的创建
虚拟机遇到一个new指令时:
- 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查该符号引用的代表的类是否被加载、解析和初始化过。
- 为对象分配内存,对象所需内存大小在类加载后就可确定。
- 分配内存后,将分配的内存空间初始化为零值。
- 对对象进行必要的设置,例如该对象是那个类的实例、对象的哈希码、对象的GC分代年龄等,这些信息都在对象头。
- 执行方法,把对象按照程序员的意愿进行初始化。
对象的内存结构
- HotSpot虚拟机中,对象在内存的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(padding)。
对象头包括两部分信息:
- 第一部分包括:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 第二部分是类型指针,及对象指向它的类元数据的指针,用来确定是哪个类的实例。
垃圾回收与内存分配策略
确定可回收对象的方法
- 引用计数算法 (因为很难解决对象之间相互循环引用的问题,所以很少虚拟机使用该算法)
- 可达性分析算法
可达性分析法
思路:通过一系列称为“GC Roots”的对象作为起点开始向下搜索,如果没有被任何引用链引用的对象就被判为可回收对象。
可作为“GC Roots”的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
不可达对象一定会被会回收吗?
不可达对象也不是一定会被回收,回收的过程至少要执行两次标记的过程,第一次标记并筛选对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者该方法已经被虚拟机执行过一次就不需要。要执行的对象会被放置到一个F-Queue队列,稍后GC将对F-Queue中的对象进行对二次小规模的标记,对象如果在finalize()方法中重新与引用链上的任何一个对象建立关联就不会被回收,但是不建议这样做,因为不确定性大,无法保证各个对象的调用顺序。
垃圾收集算法
标记-清除算法
分为标记和清除两个阶段
缺点:
- 标记和清除两个过程效率都不高。
- 标记清除后会产生大量不连续的内存碎片。
复制算法
将内存分为两个大小相等的两块,每次只使用其中一块,效率高,也不用考虑内存碎片。很多虚拟机采用该方法回收新生代。因为新生代中的对象98%都是”朝生夕死“的,所以不需要按1:1的比例来划分内存,而是将内存分为1块较大的Eden空间和2块较小的Survivor空间From Survivor和To Survivor,每次使用Eden和其中一块Survivor,回收时,将Eden和Survivor中还活着的对象一次性复制到另一块Survivor空间上,HotSpot 默认Eden:Survivor = 8:1 。如果另一块Survivor空间不够存活下来的对象,,将直接复制到老年代。
缺点: 内存缩小到原来的一半,代价高。
标记-整理算法
标记和整理两个阶段,整理不是直接清理,而是让活着的对象都向一端移动,然后再直接清理掉端边界以外的内存。
分代收集算法
分为新生代和老年代,新生代次采用复制算法,而老年代采用标记-清理或者标记-整理算法。
内存分配
- 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,Full GC经常伴随着至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍。
- 大对象直接进入老年代。
- 长期存活的对象将进入老年代,默认Minor GC 15次之后还存活的对象会被晋升到老年代中。
- 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
Minor GC和Full GC分别什么时候执行
只要老年代的连续空间大于新生代对象的综合大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
类的加载
类加载的生命周期
类被加载到虚拟机内存到卸载出内存,整个生生命周期包括:
加载(Loading)->验证(Verification)->准备(Preparation)->解析(Resolution)->初始化(Initialization)->使用(Using)->卸载(Unloading)。
其中验证、准备和解析3个部分被统称为连接(Linking)。
类的加载过程
加载(Loading)
- 通过类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
为类变量(被static修饰的变量)分配内存并设置初始值(零值)
解析
解析阶段是虚拟机将常量池内的符号引用替换成为直接引用的过程。
初始化
初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,顺序是由它们在源文件中的顺序决定。
类加载器
对于任意一个类,都需要由加载它的类加载器和这个类的本身一同确立其在Java虚拟机中的唯一性。比较两个类是否相等,它们必须是同一个类加载器加载的才有意义,否则它们必定不相等。
- 启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录中的类库,不能被Java程序直接引用。
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的类库,开发者可以直接使用扩张类加载器。
- 应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,也是默认的类加载器。
双亲委派模型
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器反馈无法完成加载请求时,子加载器才会尝试自己去加载。
泛型与类型擦除
泛型的本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和泛型方法的创建中,分别称为泛型类、泛型接口、泛型方法。
泛型实现方法称为类型擦除,基于这种方式实现的泛型称为伪泛型。
Java内存模型与线程
Java内存模型
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量。
volatile
变量定义为volatile之后具备两种特性:
- 保证此变量对所有线程的可见性。
- 禁止指令重排序优化(通过插入内存屏障指令保证处理器不发生乱序执行)。
线程
实现线程主要有三种方式: 1.使用内核线程实现 2.使用用户线程实现 3.使用用户线程加轻量级进程混合实现。
轻量级进程是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用,而系统调用的代价相对较高,需要在用户态(User Mode)和内核态2(Kernel Mode)中来回切换。每个轻量级的进程都需要有一个内核线程支持。
Java线程
一条Java线程就映射到一条轻量级进程之中。
线程安全的实现方法
- 互斥同步:同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。而互斥是实现同步的一种手段,临界区、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。
-
synchronized 最基本的互斥手段,它的原理是synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。
2.非阻塞同步 基于冲突检测的的乐观并发策略,就是先进行操作,如果没有其它线程争用共享数据,那操作就成功了,如果共享数据有争用,产生冲突了,那就再采取其它的补偿措施(最常用的就是不断重试)。
-
CAS(比较并交换):当内存位置的值符合旧的预期值时,新值才会更新内存变量。存在ABA问题。
锁优化
- 自旋锁和自适应自旋:互斥同步对性能的最大影响时阻塞的实现,因为挂起和唤醒线程都需要由用户态切换到内核态才能完成,需要耗费处理器很多的时间。自旋就是让后面请求锁的线程忙循环(自旋)等待一下。
- 锁消除:值虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
- 锁粗化:例如不要在一个循环体中反复对同一个对象进行反复加锁解锁
- 轻量级锁:使用CAS操作来避免使用互斥量的开销,如果有两个线程同时竞争,则升级为重量级锁。
- 偏向锁:目的时消除数据在无竞争的情况下的同步原语。
对象头不同状态下的结构

