深入理解java虚拟机 精华总结(面试)奥门永利误乐域

之前看过,很多会遗忘,标记一下,温故知新。(明天的我一定会感谢现在努力的自己。
)    

 

一、运行时数据区域

一、运行时数据区域

Java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、本地方法栈、堆、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区

Java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、本地方法栈、堆、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区

奥门永利误乐域 1

1.1 程序计数器

 

程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemotyError情况的区域

1.1 程序计数器

1.2 Java虚拟机栈

程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemotyError情况的区域

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

1.2 Java虚拟机栈

栈内存就是虚拟机栈,或者说是虚拟机栈中局部变量表的部分

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编辑期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(refrence)类型和returnAddress类型(指向了一条字节码指令的地址)

栈内存就是虚拟机栈,或者说是虚拟机栈中局部变量表的部分

其中64位长度的long和double类型的数据会占用两个局部变量空间,其余的数据类型只占用1个。

局部变量表存放了编辑器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(refrence)类型和returnAddress类型(指向了一条字节码指令的地址)

Java虚拟机规范对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机扩展时无法申请到足够的内存,就会跑出OutOfMemoryError异常

其中64位长度的long和double类型的数据会占用两个局部变量空间,其余的数据类型只占用1个。

1.3 本地方法栈

Java虚拟机规范对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机扩展时无法申请到足够的内存,就会跑出OutOfMemoryError异常

本地方法栈和虚拟机栈发挥的作用是非常类似的,他们的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务

1.3 本地方法栈

本地方法栈区域也会抛出StackOverflowError和OutOfMemoryErroy异常

本地方法栈和虚拟机栈发挥的作用是非常类似的,他们的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务

1.4 Java堆

本地方法栈区域也会抛出StackOverflowError和OutOfMemoryErroy异常

堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例和数组都在堆上分配

1.4 Java堆

Java堆是垃圾收集器管理的主要区域。Java堆细分为新生代和老年代

堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例和数组都在堆上分配

不管怎样,划分的目的都是为了更好的回收内存,或者更快地分配内存

Java堆是垃圾收集器管理的主要区域。Java堆细分为新生代和老年代

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有完成实例分配,并且堆也无法在扩展时将会抛出OutOfMemoryError异常

不管怎样,划分的目的都是为了更好的回收内存,或者更快地分配内存

1.5 方法区

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有完成实例分配,并且堆也无法再扩展时将会抛出OutOfMemoryError异常

方法区它用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

1.5 方法区

除了Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载

方法区它用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

当方法区无法满足内存分配需求时,将抛出OutOfMemoryErroy异常

除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载

1.6 运行时常量池

当方法区无法满足内存分配需求时,将抛出OutOfMemoryErroy异常

它是方法区的一部分。Class文件中除了有关的版本、字段、方法、接口等描述信息外、还有一项信息是常量池,用于存放编辑期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

1.6 运行时常量池

Java语言并不要求常量一定只有编辑期才能产生,也就是可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法

它是方法区的一部分。Class文件中除了有关的版本、字段、方法、接口等描述信息外、还有一项信息是常量池,用于存放编辑期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

当常量池无法再申请到内存时会抛出OutOfMemoryError异常

Java语言并不要求常量一定只有编辑期才能产生,也就是可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法

 

当常量池无法再申请到内存时会抛出OutOfMemoryError异常

二、hotspot虚拟机对象

 

2.1 对象的创建

二、hotspot虚拟机对象

1.检查 

2.1 对象的创建

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

1.检查 

2.分配内存 

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

接下来将为新生对象分配内存,为对象分配内存空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。

2.分配内存 

假设Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”

接下来将为新生对象分配内存,为对象分配内存空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”

假设Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”

选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”

3. Init

选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

执行new指令之后会接着执行Init方法,进行初始化,这样一个对象才算产生出来

3. Init

2.2 对象的内存布局

执行new指令之后会接着执行Init方法,进行初始化,这样一个对象才算产生出来

在HotSpot虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头、实例数据和对齐填充

2.2 对象的内存布局

对象头包括两部分:

在HotSpot虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头、实例数据和对齐填充

a) 储存对象自身的运行时数据,如哈希码、GC分带年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

对象头包括两部分:

b) 另一部分是指类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例

a) 储存对象自身的运行时数据,如哈希码、GC分带年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

2.3 对象的访问定位

b) 另一部分是指类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例

  1. 使用句柄访问

2.3 对象的访问定位

Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址

  1. 使用句柄访问

优势:reference中存储的是稳点的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址

奥门永利误乐域, 

优势:reference中存储的是稳点的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

 

 

 

 奥门永利误乐域 2

  1. 使用直接指针访问

 

Java堆对象的布局就必须考虑如何访问类型数据的相关信息,而refreence中存储的直接就是对象的地址

 Java对象类型数据应该是指class类信息,定义了一个类的元数据、它包含的成员等,而对象实例数据是指基于某个类new出来的一个或多个具体实例,实例要访问它对应的类信息时,通过一个指针指向另一个保存类型信息的内存区。

 

  1. 使用直接指针访问

优势:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

Java堆对象的布局就必须考虑如何访问类型数据的相关信息,而reference中存储的直接就是对象的地址

 

 

三、OutOfMemoryError 异常

优势:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

3.1 Java堆溢出

 奥门永利误乐域 3

Java堆用于存储对象实例,只要不断的创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在数量到达最大堆的容量限制后就会产生内存溢出异常

 

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置

三、OutOfMemoryError 异常

如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

3.1 Java堆溢出

3.2 虚拟机栈和本地方法栈溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在数量到达最大堆的容量限制后就会产生内存溢出异常

对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError

如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

3.2 虚拟机栈和本地方法栈溢出

在单线程下,无论由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常

对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

如果是多线程导致的内存溢出,与栈空间是否足够大并不存在任何联系,这个时候每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。解决的时候是在不能减少线程数或更换64为的虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError

3.3 方法区和运行时常量池溢出

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用

在单线程下,无论由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常

由于常量池分配在永久代中,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。

如果是多线程导致的内存溢出,与栈空间是否足够大并不存在任何联系,这个时候每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。解决的时候是在不能减少线程数或更换64位的虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程

Intern():

3.3 方法区和运行时常量池溢出

JDK1.6 intern方法会把首次遇到的字符串实例复制到永久代,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是一个引用

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用

JDK1.7 intern()方法的实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个

由于常量池分配在永久代中,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。

 

Intern():

四、垃圾收集

JDK1.6 intern方法会把首次遇到的字符串实例复制到永久代,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是一个引用

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了

JDK1.7 intern()方法的实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个

1.判断对象存活

 

4.1.1 引用计数器法

四、垃圾收集

给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了

4.1.2 可达性分析算法

1.判断对象存活

通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的

4.1.1 引用计数器法

Java语言中GC Roots的对象包括下面几种:

给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的

1.虚拟机栈(栈帧中的本地变量表)中引用的对象

4.1.2 可达性分析算法

2.方法区中类静态属性引用的对象

通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的

3.方法区中常量引用的对象

Java语言中GC Roots的对象包括下面几种:

4.本地方法栈JNI(Native方法)引用的对象

1.虚拟机栈(栈帧中的本地变量表)中引用的对象

2.引用

2.方法区中类静态属性引用的对象

强引用就是在程序代码之中普遍存在的,类似Object obj = new Object() 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

3.方法区中常量引用的对象

软引用用来描述一些还有用但并非必须的元素。对于它在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存才会抛出内存溢出异常

4.本地方法栈JNI(Native方法)引用的对象

弱引用用来描述非必须对象的,但是它的强度比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象

2.引用

虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

强引用就是在程序代码之中普遍存在的,类似Object obj = new Object() 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

3.Finalize方法

软引用用来描述一些还有用但并非必须的元素。对于它在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存才会抛出内存溢出异常

任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了

弱引用用来描述非必须对象的,但是它的强度比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象

4.3.1 回收方法区

虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

3.Finalize方法

废弃常量:假如一个字符串abc已经进入了常量池中,如果当前系统没有任何一个String对象abc,也就是没有任何Stirng对象引用常量池的abc常量,也没有其他地方引用的这个字面量,这个时候发生内存回收这个常量就会被清理出常量池

任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了

无用的类:

4.3.1 回收方法区

1.该类所有的实例都已经被回收,就是Java堆中不存在该类的任何实例

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

2.加载该类的ClassLoader已经被回收

废弃常量:假如一个字符串abc已经进入了常量池中,如果当前系统没有任何一个String对象abc,也就是没有任何Stirng对象引用常量池的abc常量,也没有其他地方引用的这个字面量,这个时候发生内存回收这个常量就会被清理出常量池

3.该类对用的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

无用的类:

4.垃圾收集算法

1.该类所有的实例都已经被回收,就是Java堆中不存在该类的任何实例

4.4.1 标记—清除算法

2.加载该类的ClassLoader已经被回收

算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象、

3.该类对用的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

4.垃圾收集算法

4.4.2 复制算法

4.4.1 标记—清除算法

他将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可

算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象、

不足:将内存缩小为了原来的一半

不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

实际中我们并不需要按照1:1比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor

4.4.2 复制算法

当另一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代

他将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可

4.4.3 标记整理算法

不足:将内存缩小为了原来的一半

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

实际中我们并不需要按照1:1比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor

4.4.4 分代收集算法

当另一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代

只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收

4.4.3 标记整理算法

5.垃圾收集器

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

a)Serial收集器:

4.4.4 分代收集算法

这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个COU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它手机结束

只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收

b)ParNew 收集器:

5.垃圾收集器

Serial收集器的多线程版本,除了使用了多线程进行收集之外,其余行为和Serial收集器一样

a)Serial收集器:

并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个COU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束

并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上

b)ParNew 收集器:

c)Parallel Scavenge 

Serial收集器的多线程版本,除了使用了多线程进行收集之外,其余行为和Serial收集器一样

收集器是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程收集器。

并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上

d)Serial Old 收集器:

c)Parallel Scavenge 

是Serial收集器的老年代版本,是一个单线程收集器,使用标记整理算法

收集器是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程收集器。

e)Parallel Old 收集器:

吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

Parallel Old是Paraller Seavenge收集器的老年代版本,使用多线程和标记整理算法

d)Serial Old 收集器:

f)CMS收集器:

是Serial收集器的老年代版本,是一个单线程收集器,使用标记整理算法

CMS收集器是基于标记清除算法实现的,整个过程分为4个步骤:

e)Parallel Old 收集器:

1.初始标记2.并发标记3.重新标记4.并发清除

Parallel Old是Paraller Seavenge收集器的老年代版本,使用多线程和标记整理算法

优点:并发收集、低停顿

f)CMS收集器:

缺点:

CMS收集器是基于标记清除算法实现的,整个过程分为4个步骤:

1.CMS收集器对CPU资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/4,

1.初始标记2.并发标记3.重新标记4.并发清除

2.CMS收集器无法处理浮动垃圾,可能出现Failure失败而导致一次Full G场地产生

优点:并发收集、低停顿

3.CMS是基于标记清除算法实现的

缺点:

g)G1收集器:

1.CMS收集器对CPU资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/4,

它是一款面向服务器应用的垃圾收集器

2.CMS收集器无法处理浮动垃圾,可能出现Failure失败而导致一次Full G场地产生

1.并行与并发:利用多CPU缩短STOP-The-World停顿的时间

3.CMS是基于标记清除算法实现的

2.分代收集

g)G1收集器:

3.空间整合:不会产生内存碎片

它是一款面向服务器应用的垃圾收集器

4.可预测的停顿

1.并行与并发:利用多CPU缩短STOP-The-World停顿的时间

运作方式:初始标记,并发标记,最终标记,筛选回收

2.分代收集

6.内存分配与回收策略

3.空间整合:不会产生内存碎片

4.6.1 对象优先在Eden分配:

4.可预测的停顿

大多数情况对象在新生代Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

运作方式:初始标记,并发标记,最终标记,筛选回收

4.6.2 大对象直接进入老年代:

6.内存分配与回收策略

所谓大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免Eden区及两个Servivor之间发生大量的内存复制

4.6.1 对象优先在Eden分配:

4.6.3长期存活的对象将进入老年代

大多数情况对象在新生代Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

如果对象在Eden区出生并且尽力过一次Minor GC后仍然存活,并且能够被Servivor容纳,将被移动到Servivor空间中,并且把对象年龄设置成为1.对象在Servivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋级到老年代中

4.6.2 大对象直接进入老年代:

4.6.4动态对象年龄判定

所谓大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免Eden区及两个Servivor之间发生大量的内存复制

为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋级到老年代,如果在Servivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入到老年代,无须登到MaxTenuringThreshold中要求的年龄

4.6.3长期存活的对象将进入老年代

4.6.4 空间分配担保:

如果对象在Eden区出生并且尽力过一次Minor GC后仍然存活,并且能够被Servivor容纳,将被移动到Servivor空间中,并且把对象年龄设置成为1.对象在Servivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋级到老年代中

在发生Minor GC 之前,虚拟机会检查老年代最大可
用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor DC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许那么会继续检查老年代最大可用的连续空间是否大于晋级到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次MinorGC 是有风险的:如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC

4.6.4动态对象年龄判定

五、虚拟机类加载机制

为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋级到老年代,如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入到老年代,无须登到MaxTenuringThreshold中要求的年龄

虚拟机吧描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

4.6.4 空间分配担保:

在Java语言里面,类型的加载。连接和初始化过程都是在程序运行期间完成的

在发生Minor GC 之前,虚拟机会检查老年代最大可 用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor DC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许那么会继续检查老年代最大可用的连续空间是否大于晋级到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次MinorGC 是有风险的:如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC

5.1 类加载的时机

五、虚拟机类加载机制

类被加载到虚拟机内存中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以再初始化阶段之后再开始,这个是为了支持Java语言运行时绑定(也成为动态绑定或晚期绑定)

在Java语言里面,类型的加载。连接和初始化过程都是在程序运行期间完成的

虚拟机规范规定有且只有5种情况必须立即对类进行初始化:

5.1 类加载的时机

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候

类被加载到虚拟机内存中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以再初始化阶段之后再开始,这个是为了支持Java语言运行时绑定(也成为动态绑定或晚期绑定)

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

虚拟机规范规定有且只有5种情况必须立即对类进行初始化:

4.当虚拟机启动时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候

5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化

被动引用:

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

1.通过子类引用父类的静态字段,不会导致子类初始化

4.当虚拟机启动时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

    2.通过数组定义来引用类,不会触发此类的初始化

5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

被动引用:

接口的初始化:接口在初始化时,并不要求其父接口全部完成类初始化,只有在正整使用到父接口的时候(如引用接口中定义的常量)才会初始化

1.通过子类引用父类的静态字段,不会导致子类初始化

5.2 类加载的过程

   
2.通过数组定义来引用类,不会触发此类的初始化

5.2.1 加载

3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

1)通过一个类的全限定名类获取定义此类的二进制字节流

接口的初始化:接口在初始化时,并不要求其父接口全部完成类初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化

2)将这字节流所代表的静态存储结构转化为方法区运行时数据结构

5.2 类加载的过程

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

5.2.1 加载

怎么获取二进制字节流?

1)通过一个类的全限定名类获取定义此类的二进制字节流

1)从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础

发表评论

电子邮件地址不会被公开。 必填项已用*标注