记录正在执行虚拟机字节码的指令地址(如果正在执⾏的是本地⽅法则为空)
Java虚拟机栈是线程是线程所独有的,它的生命周期与线程相同(随着线程的产生而产生,随着线程的消亡而消亡)
每个Java方法在执行时的同时会创建一个栈帧用户存储局部变量,操作上栈,常量池引用信息等。从 ⽅法调⽤直⾄执⾏完成的过程,对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程(就是要执行一个方法,将该方法的栈帧压入栈顶,方法执行完成后,方法的栈帧出栈)在JVM中,栈帧的操作只有两种出栈和入栈
Java虚拟机规范允许虚拟机栈的大小固定不变或者动态扩展
固定情况下: 如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,则抛出StackOverflowError异常
可动态扩展情况下: 尝试扩展的时候无法申请到足够的内存;或者在创建新的线程的时候没有足够的内存去创建对应的虚拟机栈,则会抛出OutOfMemoryError异常
设置虚拟栈的大小:
java -Xss<size> # java -Xss2M HackTheJava
本地方法栈与Java虚拟机栈类似,它们之间的区别不过本地方法栈为本地方法栈使用
本地方法栈一般是使用其它语言(C,C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理
在JVM启动时建立,它是Java程序最主要的内存工作区域,堆空间是所有线程共享的,这是一块与Java应用密切相关的内存空间。
所有对象都在这里分配内存,是垃圾回收的主要区域(“GC堆”)
Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略
现代的垃圾收集器基本都是采⽤分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出OutOfMemoryError 异常
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定⼀个程序的堆内存⼤⼩,第⼀个参数设置初始值,第 ⼆个参数设置最⼤值
java -Xms<size> -Xmx<size> HackTheJava # java -Xms1M -Xmx2M HackTheJava
⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 和堆⼀样不需要连续的内存,并且可以动态扩展,动态扩展失败⼀样会抛出 OutOfMemoryError 异常。 对这块区域进⾏垃圾回收的主要⽬标是对常量池的回收和对类的卸载,但是⼀般⽐较难实现
HotSpot 虚拟机把它当成永久代来进⾏垃圾回收。但很难确定永久代的⼤⼩,因为它受到很多因素影 响,并且每次 Full GC 之后永久代的⼤⼩都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更 容易管理⽅法区,从 JDK 1.8 开始,移除永久代,并把⽅法区移⾄元空间,它位于本地内存中,⽽不是 虚拟机内存中
⽅法区是⼀个 JVM 规范,永久代与元空间都是其⼀种实现⽅式。在 JDK 1.8 之后,原来永久代的数据 被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放⼊堆中
运⾏时常量池是⽅法区的⼀部分
Class ⽂件中的常量池(编译器⽣成的字⾯量和符号引⽤)会在类加载后被放⼊这个区域
除了在编译期⽣成的常量,还允许动态⽣成,例如 String 类的 intern()
在 JDK 1.4 中新引⼊了 NIO 类,它可以使⽤ Native 函数库直接分配堆外内存,然后通过 Java 堆⾥的 DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼性能,因为避免了 在堆内存和堆外内存来回拷⻉数据
垃圾收集主要针对堆和方法区进行的,程序计数器,Java虚拟机栈、本地方法栈,都属于线程私有的,只存在线程的生命周期内,线程结束之后就会消失,因此不需要对这三给区域进行垃圾收集
给对象添加一个引用计数器,对象每被引用一次计数器加1,引用失效时(引用不使用)计数器减1。引用计数器为0的对象可被回收
弊端: 当两个对象出现循环引用时,此时引用计数器永远都不会变为0,所以不会被回收。正是因为循环引用的存在,所以Java虚拟机不使用引用计数器算法
扫描堆中对象,一GC Roots为起点扫描,可达的对象都是存活的,不可达的对象都是可回收的
Java 虚拟机使⽤该算法来判断对象是否可被回收,GC Roots ⼀般包含以下内容:
因为⽅法区主要存放永久代对象,⽽永久代对象的回收率⽐新⽣代低很多,所以在⽅法区上进⾏回收性 价⽐不⾼。 主要是对常量池的回收和对类的卸载
为了避免内存溢出,在⼤量使⽤反射和动态代理的场景都需要虚拟机具备类卸载功能
类的卸载条件很多,需要满⾜以下三个条件,并且满⾜了条件也不⼀定会被卸载:
类似 C++ 的析构函数,⽤于关闭外部资源。但是 try-finally 等⽅式可以做得更好,并且该⽅法运⾏代价 很⾼,不确定性⼤,⽆法保证各个对象的调⽤顺序,因此最好不要使⽤
当⼀个对象可被回收时,如果需要执⾏该对象的 finalize() ⽅法,那么就有可能在该⽅法中让对象重新被 引⽤,从⽽实现⾃救。⾃救只能进⾏⼀次,如果回收的对象之前调⽤了 finalize() ⽅法⾃救,后⾯回收时 不会再调⽤该⽅法
⽆论是通过引⽤计数算法判断对象的引⽤数量,还是通过可达性分析算法判断对象是否可达,判定对象 是否可被回收都与引⽤有关
Java提供了4种引用类型:强引用、软引用、弱引用、虚引用
强引用: 被强引⽤关联的对象不会被回收,使⽤ new ⼀个新对象的⽅式来创建强引⽤
Object o = new Object();
软引用: 被软引⽤关联的对象只有在内存不够的情况下才会被回收,使⽤ SoftReference 类来创建软引⽤
Object o = new Object(); SoftReference<Object> sfo = new SoftReference<Object>(o); o = null; //使对象只被软引用关联
弱引用: 被弱引⽤关联的对象⼀定会被回收,也就是说它只能存活到下⼀次垃圾回收发⽣之前,使⽤ WeakReference 类来创建弱引⽤
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null;
虚引用: ⼜称为幽灵引⽤或者幻影引⽤,⼀个对象是否有虚引⽤的存在,不会对其⽣存时间造成影响,也⽆法通 过虚引⽤得到⼀个对象。 为⼀个对象设置虚引⽤的唯⼀⽬的是能在这个对象被回收时收到⼀个系统通知。 使⽤ PhantomReference 来创建虚引⽤
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj, null); obj = null;
在标记阶段,程序会检查每个对象是否为回收对象,如果是回收对象,则程序会在对象头部打上标记
在清除阶段,会进行对象回收并取消标记位,另外,还会判断回收后的分块与前⼀个空闲分块是否连 续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链 表,之后进⾏分配时只需要遍历这个空闲链表,就可以找到分块
缺点:
标记和清除的效率都不高
清除之后会产生大量不连续的内存碎片,导致无法给大对象分配内存
让所有存活的内存都向一端移动,任何直接清理掉端边界以外的内存
优点:
不会产生内存碎片
缺点:
需要移动大量的对象,效率比较低
将内存空间分为大小相同的两块,使用其中的一块,当这一块的内存用完了就将还存货的对象复制到另一块内存空间中,然后再把使用过的那一块进行内存空间的清理
**缺点:**只使用了一半的内存,需要大内存
现在商业虚拟机都是使用这种回收方式回收新生代的对象,当并不是将新生代内存区化为大小相同的两块,而是⽽是⼀块较⼤ 的 Eden 空间和两块较⼩的 Survivor 空间默认比例为8:1:1。每次使⽤ Eden 和其中⼀块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另⼀块 Survivor 上,最后清理 Eden 和使⽤过的那⼀块 Survivor。如此循环
现在商业虚拟机使用的是分代收集方法,它根据对象的生存周期将内存划分为好几个区域,不同区域采用不同的收集算法
一般将堆划分为新生代和老年代
新生代:使用复制算法
老年代:标记 - 清除或标记 - 整理算法
单线程与多线程:单线程指的是垃圾收集器只使⽤⼀个线程,⽽多线程使⽤多个线程
串⾏与并⾏:串⾏指的是垃圾收集器与⽤户程序交替执⾏,这意味着在执⾏垃圾收集的时候需要停 顿⽤户程序;并⾏指的是垃圾收集器和⽤户程序同时执⾏。除了 CMS 和 G1 之外,其它垃圾收集 器都是以串⾏的⽅式执⾏
1、serial
串行收集,复制算法,单线程,stop the world
评价:简单实用,可配合SerialOld
stop the world: 在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
2、ParNew
并发收集,复制算法,多线程,stop the world
评价:serial的多线程版本,可配合CMS
3、Parallel Scavenge
并行收集,复制算法,多线程,stop the world
评价:关注吞吐量的垃圾收集器,可配合Parallel Old
1、Serial Old
串行收集,标记整理,单线程,stop the world。
评价:简单实用
2、Parallel Old
并行收集,标记整理,stop the wold
评价:一般就和PS配合,用于关注吞吐的场景
3、CMS
注重最短时间停顿,CMS垃圾收集器是最早提出的并发垃圾收集器,收集垃圾线程和用户线程同时工作,采用标记清除算法。该收集器分为初始标记、并发标记、重新标记、并发清除四个步骤
初始标记: 标记与 GCRoots能直接关联的对象,速度很快,stop the world(暂停其它线程)
并发标记: 进⾏ GC Roots Tracing 的过程,由前阶段标记的对象出发,所有可达到的对象都需标记。在整个回收过程中耗时最长 不用 stop the world
重新标记: 修正在并发标记期间因用户程序继续运作而导致标记 产生变动的那一部分对象进行标记,stop the world
并发清除: 采用标记 - 清除算法,定点清除内存而不影响其它内存,所以并发清除,不用 stop the world
在整个过程中耗时最⻓的并发标记和并发清除过程中,收集器线程都可以与⽤户线程⼀起⼯作,不需要进⾏停顿(stop the world)
CMS的缺点:
吞吐量低:低停顿的时间是以牺牲吞吐量为代价的,导致CPU的利用率不高
无法处理悬浮的垃圾
标记清除算法会产生大量的内存碎片
堆被分为年轻代和老年代,其它的垃圾收集器收集的范围是整个新生代或者整个老年代,而G1收集器是将新生代和老年代一起收集
G1把新生代和老年代划分为多个大小相等的独立区域(Region),新生代和老年代不在物理隔离
RememberSets: 又叫Rsets是每个region中都有的一份存储空间,用于存储本region的对象被其他region对象的引用记录
CollectionSets: 又叫Csets是一次GC中需要被清理的regions集合,注意G1每次GC不是全部region都参与的,可能只清理少数几个,这几个就被叫做Csets
每个 Region 都有⼀个 Remembered Set,⽤来记录该 Region 对象的引⽤对象所在的 Region。通过使 ⽤ Remembered Set,在做可达性分析的时候就可以避免全堆扫描
YGC
年轻代GC,使用复制算法,stop the world。将E和S(from)区复制到S(to)区,S(to)区一开始是没有被标识的,是free Region
Mixed GC
G1对于老年代的GC比较特殊,本质上不是只针对老年代,也有部分年轻代,所以又叫MixGC
Mixed GC步骤:
初次标记: 标记GC Roots能够直接关联的Region,但是与CMS不同的是,这里不止标记O区
RootRegion扫描: 扫描GCRoots所在Region到old区的引用
并发标记: 类似CMS,但是标记的是整个堆,而不是只有O区。这期间如果发现某个region所有对象都是’垃圾’则标记为X
重新标记: 类似CMS,但也是整个堆,并且上一步中的X区被删除。另外采用了初始标记阶段的SATB,重新标记的速度变快
复制/清理: 选择所有Y区reigons和’对象存活率较低’的O区regions组成Csets,进行复制清理
Minor GC: 回收新生代,因为新生代的存活时间很短,因此Minor GC 会频繁的执行,执行速度一般也比较快
Full GC: 回收老年代和新生代,老年代的对象存活时间长,因此Full GC很少被执行,执行速率也比Minor GC 慢很多
1、对象优先在Eden分配
大多数情况下,对象在新生代的Eden中被分配内存,如果内存不够时,会进行Minor GC 操作
2、大对象直接进入老年代
大对象指的是需要连续内存空间的对象,典型的代表就是很长的字符串和数组
经常出现⼤对象会提前触发垃圾收集以获取⾜够的连续空间分配给⼤对象
//设置成为大对象,进入老年代的阀值,当大于该值,则视为大对象进入老年代 -XX:PretenureSizeThreshold=8M
3、长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出⽣并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到⼀定年龄则移动到⽼年代中,jdk6之前默认年龄阀值为15,jdk6后长期存活年龄未必是15
-XX:MaxTenuringThreshold 15
4、动态对象年龄判定
虚拟机中并不是永远要求对象年龄到达阈值才能进入老年代的。如果在Survivor中相同年龄所有对象⼤⼩的总和⼤于 Survivor 空间的⼀半,则年龄⼤于或等于该年龄的对象可以直接进 ⼊⽼年代
5、 空间分配担保
创建对象时Eden区域内存不够,则会向老年代去借内存,即空间分配担保
# 开启空间分配担保 -XX:+HandlePromotionFailture
1、调用System.gc()
只是建议虚拟机执行Full GC操作,但是虚拟机不一定真正去执行。不建议使用这种操作,而是让虚拟机自己去管理内存
-XX:+ DisableExplicitGC # 来禁止RMI调用System.gc
2、老年代空间不足
老年代空间不足的常见场景为大对象进入老年代和长期存活的对象进入老年代
为了避免以上这些情况导致触发Full GC,应当尽量不要创建大的对象和大的数组,还可以设置新生代的大小,让对象尽可能的在新生代被回收,还可以设置年龄阈值,将阈值设大,让对象进入老年代的年龄变大,让对象更多时间待在新生代
3、 空间分配担保失败
使⽤复制算法的 Minor GC 需要⽼年代的内存空间作担保,如果担保失败会执⾏⼀次 Full GC
4、JDK 1.7 及以前的永久代空间不⾜
在 JDK 1.7 及以前,HotSpot 虚拟机中的⽅法区是⽤永久代实现的,永久代中存放的为⼀些 Class 的信 息、常量、静态变量等数据
当系统中要加载的类、反射的类和调⽤的⽅法较多时,永久代可能会被占满,在未配置为采⽤ CMS GC 的情况下也会执⾏ Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError
5、Concurrent Mode Failure(并发模式故障)
执行cms和GC的工程中同时有对象需要放入老年代中,而此时老年代内存空间不足(可能是 GC 过程中浮动垃 圾过多导致暂时性的空间不⾜),便会报Concurrent Mode Failure错误并执行Full GC操作
类是在运⾏期间第⼀次使⽤时动态加载的,⽽不是⼀次性加载所有类。因为如果⼀次性加载,那么会占⽤很多的内存
包括一下7个阶段:
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)
类的加载过程有加载、验证、准备、解析、初始化5个步骤
加载
在加载阶段,虚拟机需要做3件事
其中二进制字节流可以从以下方法中获取
验证
确保Class文件的字节流中包含信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备
准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存,类变量指的是类中的静态变量,不包括实例变量,实例变量会随对象的初始化分配到堆中
初始值指的是一些数据类型的默认值,基本数据类型的初始值如下(引用类型的初始值为null)
例如
private static int a = 12 //在准备阶段过后 a被初始化为0 而不是12
有一种特殊情况,被final修饰的静态变量在准备阶段过后 初始化值就为当前赋的值
private final static int a = 12 //在准备阶段后 a被初始化为12
解析
解析过程是虚拟机将常量池中符号的引用替换为直接引用的过程
符号引用
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了⽀持 Java 的动态绑定
符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标不一定加载到内存当中
直接引用: 直接引用可以是直接指向目标的指针,直接引用与虚拟机的内存布局有关。如果有了直接引用,那么引用目标必定存在内存中
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANTS_InterfaceMethodref_info四种类型常量
初始化
初始化阶段才真正开始执⾏类中定义的 Java 程序代码。初始化阶段是虚拟机执⾏类构造器 () ⽅ 法的过程。在准备阶段,类变量已经赋过⼀次系统要求的初始值,⽽在初始化阶段,根据程序员通过程 序制定的主观计划去初始化类变量和其它资源
主动引用
虚拟机规范中并没有强制约束何时进⾏加载,但是规范严格规定了有且只有下列五种情况必须对类进⾏ 初始化(加载、验证、准备都会随之发⽣)
被动引用
以上 5 种场景中的⾏为称为对⼀个类进⾏主动引⽤。除此之外,所有引⽤类的⽅式都不会触发初始化, 称为被动引⽤。被动引⽤的常⻅例⼦包括
System.out.println(Father.value);
Father[] fathers = new Father[10];
System.out.println(Son.SON);
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都有一个独立的类名称空间
这⾥的相等,包括类的 Class 对象的 equals() ⽅法、isAssignableFrom() ⽅法、isInstance() ⽅法的返回 结果为 true,也包括使⽤ instanceof 关键字做对象所属关系判定结果为 true
从Java虚拟机的角度来讲,只存在两种不同的类加载器
启动类加载器(Bootstrap ClassLoader): 使用C++实现,是虚拟机自身的一部分
**所有其它类的加载器: **使用Java实现,独立于虚拟机,继承抽象类java.lang.ClassLoader
从Java开发人员角度来讲,类加载器可以划分的更细一些
启动类加载器(Bootstrap ClassLoader): 加载/lib下的jar包和类。C++编写
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 \lib ⽬录中的, 或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照⽂件名识别,如 rt.jar,名字不符合的类库即使放在 lib ⽬录中也不会被加载)类库加载到虚拟机内存中。启动类加 载器⽆法被 Java 程序直接引⽤,⽤户在编写⾃定义类加载器时,如果需要把加载请求委派给启动 类加载器,直接使⽤ null 代替即可
扩展类加载器(Extension ClassLoader): /lib/ext目录下的jar包和类。java编写
这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使⽤扩展类加 载器
应⽤程序类加载器(Application ClassLoader): 加载当前classPath下的jar包和类。java编写
这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() ⽅法的返回值,因此⼀般称为系统类加载器。它负责加 载⽤户类路径(ClassPath)上所指定的类库,开发者可以直接使⽤这个类加载器,如果应⽤程序 中没有⾃定义过⾃⼰的类加载器,⼀般情况下这个就是程序中默认的类加载器
应⽤程序是由三种类加载器互相配合从⽽实现类加载,除此之外还可以加⼊⾃⼰定义的类加载器
双亲委派模型除了顶层的启动类加载器没有父亲类加载器,其它的加载器都有父亲类加载器,注意这里的父亲类加载器本身指继承关系而是指组合关系
工作过程:
如果一个类接收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父亲加载器去加载,每一层的类加载器都是如此。
因此所有的类加载请求最后都会被传递到顶层启动类加载器中加载,只要在父亲类加载器反馈给自己无法加载这个类请求(它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载
优点:
使得Java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
避免了多分同样的字节码加载
实现:
以下是抽象类 java.lang.ClassLoader 的代码⽚段,其中的 loadClass() ⽅法运⾏过程如下:先检查类是 否已经加载过,如果没有则让⽗类加载器去加载。当⽗类加载器加载失败时抛出 ClassNotFoundException,此时尝试⾃⼰去加载
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }