类的加载过程
1.加载
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java. lang.Class对象,作为方法区这个类的各种数据的访问入口
2.链接
验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备(Prepare)
- 为类变量分配内存并且设置该类变量的默认初始值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、 CONSTANT Fieldref info、 CONSTANT Methodref info等 。
初始化
- 初始化阶段就是执行类构造器方法<clinit> ()的过程。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- <clinit>()不同于类的构造器。(关联: 构造器是虚拟机视角下的<init>()
- 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit> ()已经执行完毕。
- 虚拟机必须保证个类的<clinit> ()方法在多线程下被同步加锁。
类加载器的分类
JVM支持两种类型的类加载器,分别为引导类加载器(BootstrapClassLoader)和自定义类加载器(User-Defined ClassLoader) 。
从概念上来讲,自定义类加载器般指的是程序中由开发人员自定义的一类.类加载器,但是Java虛拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3 个,如下所示:
虚拟机自带的加载器
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA_ HOME/jre/lib/rt.jar、resources. jar或sun . boot. class.path路径下的内容) ,用于提供JVM自身需要的类
- 并不继承自java. lang.ClassLoader, 没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun. misc. Launcher$ExtClassLoader实现。
- 派生于ClassLoader类父类加载器为启动类加载器
- 从java .ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创
- 建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun . misc. Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java. class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemClassLoader ()方法可以获取到该类加载器
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子载器才会尝试自己去加载,这就是双亲委派模式。
优势:
- 防止类名重复
- 保护程序安全,防止核心API被随意篡改
沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法就是因为加载的是rt. jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
运行时数据区
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的, 这些与线程对应的数据区域会随着线程开始和结束而创建和销毀。
- 每个线程:独立包括程序计数器、虚拟机栈、本地方法栈。
- 线程间共享:堆、堆外空间(永久代或元空间、代码缓存)
程序计数器(PC寄存器)
JVM中的程序计数寄存器(Program Counter Register) 中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
作用:
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
PC寄存器中即没有GC也没有OOM
使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
为什么使用PC寄存器记录当前线程的执行地址呢?
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
虚拟机栈
栈是运行时的单位,而堆是存储的单位
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次的Java方法调用。
➢是线程私有的
生命周期和线程一致
作用
主管Java程序的运行,它保存方法的局部变量(8种基本类型、对象的引用)、部分结果,并参与方法的调用和返回。
栈的特点(优点)
1.栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
2.JVM直接对Java栈的操作只有两个:
➢每个方法执行,伴随着进栈(入栈、压栈)
➢执行结束后的出栈工作
3.对于栈来说不存在垃圾回收问题
栈可能出现的异常
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
调整栈的大小参数设置:-Xss256k
栈中存储什么?
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
- 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame) 。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈运行原理
- JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame) ,与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class) 。
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
- 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local variables)
- 操作数栈(operand stack) (或表达式栈 )
- 动态链接(Dynamic Linking) ( 或指向运行时常量池的方法引用)
- 方法返回地址(Return Address) (或方法正常退出或者异常退出的定义) .
- 一些附加信息
局部变量表(Local Variables)
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量;这些数据类型包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。 在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
关于Slot得理解
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
局部变量表,最基本的存储单元是Slot (变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类 型的变量。
在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long和double)占用两个slot。
- byte、short、char在存储前被转换为int,boolean 也被转换为int,0表示false,非0表示true。
- long和double则占据两个Slot。
操作数栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一中明明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好的,为max stack的值。
栈中的任何一个元素都是可以任意的Java数据类型。
➢32bit的类型占用一个栈单位深度
➢64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push) 和出栈(pop)操作来完成一次数据访问。
动态链接(Dynamic Linking或指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指令
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference) 保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
存放调用该方法的pc寄存器的值。
一个方法的结束,有两种方式:
➢正常执行完成
➢出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
本地方法库
为什么要使用Native Method ?
与Java环境外交互:
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
与操作系统交互
JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
Sun's Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。 jre大部分是用Java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread的setPriority() 方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用win32 SetPriority() API。 这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library) 提供,然后被JVM调用。
本地方法栈
堆
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
➢堆内存的大小是可以调节的。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(ThreadLocal Allocation Buffer, TLAB) 。
设置堆空间大小与OOM
年轻代和老年代
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)
其中年轻代又可以划分为Eden空间Survivor0空间和Survivor1空间(有时也叫做from区、to区)。
对象分配策略
Minor GC 、Major GC与Full GC
堆空间分代思想
内存分配策略
对象分配过程:TLAB
方法区
- 方法区(Method Area) 与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java. lang. OutOfMemoryError:PermGen space 或者java. lang. OutOfMemoryError: Metaspace
- 关闭JVM就会释放这个区域的内存。
设置方法区内存的大小
- 元数据区大小可以使用参数-XX :MetaspaceSize和-XX :MaxMetaspaceSize指定,替代上述原有的两个参数。
- 默认值依赖于平台。windows下,-XX:MetaspaceSize是21M, -XX :MaxMetaspaceSize的值是-1,即没有限制。
- 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机-样会抛出异常OutOfMemoryError: Metaspace
- -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX : MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
- 如果初始化的高水位线设置过低,上述高水位线调整情况会发 生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX : MetaspaceSize设置为一一个相对较高的值。
如何解决OOM?
- 要解决00M异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer) 对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)。
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
- 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms) ,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
方法区(Method Area)存储什么?
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:
- 它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
运行时常量池
方法区的演进细节
永久代为什么要被元空间替换?
随着Java8 的到来,HotSpot VM中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间( Metaspace )。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
这项改动是很有必要的,原因有:
1)为永久代设置空间大小是很难确定的。
在某些场景下,如果动态加载类过多,容易产生Perm区的00M。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
"Exception in thread +ubbo client x.x connector'java.lang.OutOfMemoryError: PermGen space"
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
2)对永久代进行调优是很困难的。
StringTable为什么要调整?
jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
对象的实例化
创建对象的方式
- new
- Class的newInstance():反射的方法,只能调用空参的构造器,权限必须是public
- Constructor的newInstance(XXX):反射的方式,可以调用空参、带参的构造器,权限没有要求
- 使用clone():不调用任何构造器,当前类需要实现Cloneable接口,实现clone()
- 使用反序列化:从文件中、从网络中获取一个对象的二进制流
- 第三方库Objenesis
创建对象的步骤
1.判断对象对应的类是否加载、链接、初始化
虚拟机遇到一条new指令 ,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类
的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。( 即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象
2.为对象分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
如果内存规整:指针碰撞
- 如果内存是规整的,那么虚拟机将采用的是指针碰撞法( Bump The Pointer )来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact (整理)过程的收集器时,使用指针碰撞。
如果内存不规整:虚拟机需要维护一个列表
- 空闲列表分配:如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表( Free List ) "。
3.处理并发安全问题
- 采用CAS失败重试、区域加锁保证更新的原子性
- 每个线程预先分配一块TLAB ——通过-XX:+/-UseTLAB参数来设定
4.初始化分配到的空间
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
- 在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
- 因此一般来说(由字节码中是否跟随有invokespecial指令所决定), new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
5.设置对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、 锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
6.执行init方法进行初始化
在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构
造方法,并把堆内对象的首地址赋值给引用变量。
因此一般来说(由字节码中是否跟随有invokespecial指令所决定) , new指令之后会接着就是执
行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
对象的内存布局
StringTable
1.String的基本特性
- String:字符串,使用一对""引起来表示。
- String声明为final的,不可被继承
- String实现了Serializable接口:表示字符串是支持序列化的。
- 实现了Comparable接口:表示String可以比较大小
- String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为byte []
垃圾回收器
什么是垃圾(Garbage)?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
垃圾回收算法
1.垃圾标记阶段的算法之引用计数算法
垃圾标记阶段:对象存活判断
- 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
- 那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
- 判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
方式一:引用计数器
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
➢它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
➢每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
➢引用计数器有一-个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
java中并没有使用引用计数算法
2.垃圾标记阶段的算法之可达性分析算法
方式二:可达性分析(或根搜索算法、追踪性垃圾收集)
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。
所谓"GC Roots"根集 合就是一组必须活跃的引用。
基本思路:
➢可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
➢使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
➢如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
➢在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots
在Java语言中,GC Roots包括以下几类元素:
虚拟机栈中引用的对象
➢比如:各个线程被调用的方法中使用到的参数、局部变量等。
本地方法栈内JNI (通常说的本地方法)引用的对象
方法区中类静态属性引用的对象.
➢比如: Java类的引用类型静态变量
方法区中常量引用的对象
➢比如:字符串常量池(String Table)里的引用
所有被同步锁synchronized持有的对象
Java虚拟机内部的引用。
➢基本数据类型对应的Class对象,一些常驻的异常对象(如:Nul lPointerException、OutOfMemoryError),系统类加载器。
反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了这些固定的GC Roots集 合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC )
➢如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时。候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
小技巧:
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
对象的finalization机制
Java语言提供了对象终止( finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象finalize()方法。
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
➢在finalize() 时可能会导致对象复活。
➢finalize() 方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,
若不发生GC,则finalize() 方法将没有执行机会。
➢一个糟糕的finalize ()会严重影响GC的性能。
从功能上来说,finalize ()方法与C+ +中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize ()方法在本质上不同于C++中的析构函数。
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一-般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
➢可触及的:从根节点开始,可以到达这个对象。
➢可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
➢不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可 触及的对象不可能被复活,因为finalize() 只会被调用一次。
以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
垃圾清除阶段
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是标记-清除算法( Mark-Sweep)、复制算法(Copying)、标记-压缩算法(Mark-Compact )。
标记-清除(Mark-Sweep)算法
执行过程:
当堆中的有效内存空间(available memory) 被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
●标记: Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
●清除: Collector对堆 内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点:
- 效率不算高
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
复制(Copying)算法
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
特别的:
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
标记-压缩(整理)算法
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact) 算法由此诞生。
执行过程
- 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
- 之后,清理边界外所有的空间。
优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点:
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,需要全程暂停用户应用程序。即: STW
分代收集算法
以HotSpot中的CMS回收器为例,CMS是基于Mark- Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial 0ld回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。
增量收集算法
上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the World的状态。在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting) 算法的诞生。
基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关Gc产生的停顿也越长。为了更好地控制Gc产生的停顿时间,将-块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
垃圾回收相关概念的概述
1.System.gc()的理解
- 在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC, 同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
- 然而System.gc()调用附带一个 免责声明,无法保证对垃圾收集器的调用。
- JVM实现者可以通过system.gc ()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。
2.内存溢出与内存泄漏
内存溢出(OOM)
- 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
- 由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。
- 大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
- javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存泄漏(Memory Leak)
- 也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
- 但实际情况很多时候一- 些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,也可以叫做宽泛意义上的“内存泄漏”。
- 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现0utOfMemory异常,导致程序崩溃。
- 注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
举例:
1、单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
2、一些提供close的资源未关闭导致内存泄漏数据库连接(dataSourse. getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的。
3.Stop The World
Stop-The-World,简称STW,指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
- 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
- 分析工作必须在一个能确保一致性的快照中进行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
4.垃圾回收的并行与并发
并发(Concurrent)
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
并发不是真正意义_上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
并行(Parallel)
- 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)。
- 其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
- 适合科学计算,后台处理等弱交互场景
垃圾回收器
现在标准:在最大吞吐量优先的情况下,降低停顿时间。
七款经典的垃圾回收器:
- 串行回收器: Serial、Serial 0ld
- 并行回收器: ParNew、Parallel Scavenge、Parallel Old
- 并发回收器: CMS、G1
七款经典收集器与垃圾分代中间的关系:
- 新生代收集器: Serial、 ParNew、 Parallel Scavenge;
- 老年代收集器: Serial 0ld、Parallel 0ld、CMS;
- 整堆收集器: G1;
如何查看默认的垃圾回收器:
-XX:+PrintCommandLineFlags: 查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID
Serial回收器:串行回收
Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之 前回收新生代唯一的选择。
Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。
Serial收集器采用复制算法、串行回收和" Stop-the-World"机制的方式执行内存回收。
除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial 0ld收集器。Serial 0ld收集器同样也采用了串行回收和"stop the World" 机制,只不过内存回收算法使用的是标记-压缩算法。
➢Serial 0ld是运行在Client模式下默认的老年代的垃圾回收器
➢Serial 0ld在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用②作为老年代CMS收集器的后备垃圾收集方案。
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。
ParNew回收器:并行回收
Parallel Scavenge回收器:吞吐量优先
HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavlenge收 集器同样也采用了复制算法、并行回收和"Stop the World"机制。
那么Parallel收集器的出现是否多此一举?
- 和ParNew收集器不同,Parallel Scavenge收 集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
- 自适应调节策略也是Parallel Scavenge 与ParNew一个重要区别。
参数配置:
- -XX:MaxGCPauseMillis设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
- -XX: GCTimeRatio垃圾收集时间占总时间的比例(= 1 /(N + 1))。用于衡量吞吐量的大小。取值范围(0,100),默认值99.
- -XX: +UseAdaptiveSizePplicy设 置Parallel Scavenge收集器具有自适应调节策略
CMS回收器:低延迟
CMS的工作原理
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
- 初始标记(Initial-Mark) 阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
- 并发标记(Concurrent-Mark) 阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(Remark) 阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
- 并发清除( Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
CMS的优点:并发收集、低延迟
CMS的弊端:
1)会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
2) CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
3) CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure ”失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
CMS收集器可以设置的参数
-XX: +UseConcMarkSweepGc 手动指定使用CMS收集器执行内存回收任务。
- 开启该参数后会自动将-XX: +UseParNewGC打开。即: ParNew (Young区用) +CMS (0ld区用) +Serial 0ld的组合。
-XX:CMSlnitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
- JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
- 如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
-XX: +UseCMsCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理。
-XX:ParallelCMSThreads 设置CMS的线程数量。
➢CMS默认启动的线程数是(ParallelGCThreads+3) /4,ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到cMs收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
G1回收器:区域化分代式
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
G1回收器的特点(优势)
并行与并发
- 并行性: G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STWi
- 并发性: G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
分代收集
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(Region) , 这些区域中包含了逻辑上的年轻代和老年代。
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
空间整合
- CMS:“标记-清除"算法、内存碎片、若干次GC后进行一次碎片整理
- G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。 尤其是当Java堆非常大的时候,G1的优势更加明显。
G1回收器的缺点
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint) 还是程序运行时的额外执行负载(Overload)都要比CMS要高。
从经验上来说,在小内存应用,上CMS的表现大概率会优于G1,而G1在大内存应用_上则发挥其优势。平衡点在6-8GB之间。
G1回收器的参数设置
G1回收器的应用场景
G1回收器垃圾回收过程
应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程; G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂 停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
标记完成马上开始混合回收过程。对于一个混合回收期,G1GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他Gc不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
举个例子: 一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31 个小时整个堆的使用率会达到45号,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
G1回收过程一:年轻代GC
G1回收过程二:并发标记过程
G1回收过程三:混合回收
G1回收过程四:Full GC
垃圾回收器总结
缓存一致性协议和伪共享
伪共享:
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。
@sun.misc.Contended:可以解决伪共享
成员变量(非静态变量)的赋值过程:①默认初始化---->②显示初始化---->③构造器初始化------>④对象赋值
虚拟机的基石:Class文件
字节码文件里面是什么?
源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM指令,而不是像C、C++经由编译器直接生成机器码。
什么是字节码指令(byte code)?
Java虚拟机的指令由一个字节长度的、代表某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
Class类的本质
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class文件是一组以8卫字节为基础单位的二进制流。
Class文件格式
Class的结构不像XML等描述语言,由于它没有任何分割符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许被改变。
Class文件格式采用一种类似于C语言结构体的方式经行数据存储,这种结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型。以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号苏,无符号数可以用来秒速数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型。所有的表都习惯性以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。由于表没有固定长度。所以通常会在其前面加上数说明。