虚拟机
GC发生在哪里
GC发生在JVM的堆里
堆(Heap),一个JVM只有一个堆内存,堆内存的大小是可以调节的
类加载器读取类文件后,一般会把什么东西放到堆中?类,方法,常量,变量,保存我们所有引用类型的真实对象
JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old
Generation),非堆内存就一个永久代(Permanent Generation)。
年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
一、新生代
新生代主要用来存放新生的对象。一般占据堆空间的1/3。在新生代中,保存着大量的刚刚创建的对象,但是大部分的对象都是朝生夕死,所以在新生代中会频繁的进行MinorGC,进行垃圾回收。新生代又细分为三个区:Eden区、SurvivorFrom、ServivorTo区,三个区的默认比例为:8:1:1。
Eden区:Java新创建的对象绝大部分会分配在Eden区(如果对象太大,则直接分配到老年代)。当Eden区内存不够的时候,就会触发MinorGC(新生代采用的是复制算法),对新生代进行一次垃圾回收。
SurvivorFrom区和To区:在GC开始的时候,对象只会存在于Eden区和名为From的Survivor区,To区是空的,一次MinorGc过后,Eden区和SurvivorFrom区存活的对象会移动到SurvivorTo区中,然后会清空Eden区和SurvivorFrom区,并对存活的对象的年龄+1,如果对象的年龄达到15,则直接分配到老年代。MinorGC完成后,SurvivorFrom区和SurvivorTo区的功能进行互换。下一次MinorGC时,会把SurvivorTo区和Eden区存活的对象放入SurvivorFrom区中,并计算对象存活的年龄。
二、老年代
老年代主要存放应用中生命周期长的内存对象。老年代比较稳定,不会频繁的进行MajorGC。而在MaiorGC之前才会先进行一次MinorGc,使得新生的对象进入老年代而导致空间不够才会触发。当无法找到足够大的连续空间分配给新创建的较大对象也会提前触发一次MajorGC进行垃圾回收腾出空间。
在老年代中,MajorGC采用了标记—清除算法:首先扫描一次所有老年代里的对象,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长。因为要扫描再回收。MajorGC会产生内存碎片,当老年代也没有内存分配给新来的对象的时候,就会抛出OOM(Out of Memory)异常。
谈一谈JVM的GC,包括几个垃圾回收算法
有四种GC算法
GC算法–引用计数法:
给每个对象分配一个计数器,引用一次就+1,当有对象没被引用时,就会被回收
GC算法–复制算法:
复制算法:可以把from区里面的内容复制到to区中
他将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
GC–标记清除法:
有用到的会被标记,然后没有被标记的会被清除
标记压缩算法:
其分为两个阶段标记阶段,和压缩阶段.其中标记阶段和标记清除算法的标记阶段是一样的.
对压缩算法来说,他的工作就是移动所有的可达对象到堆内存的同一区域中,使它们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存集中在一起,以防出现标记清除算法的弊端.
在压缩阶段,由于要移动可达对象,那么就要考虑移动对象时候的顺序问题,一般分为一下三种:
任意顺序,不考虑原先对象的排列顺序,也不考虑对象之间的引用关系,随意移动可达对象,这样可能会有内存访问的局部性问题.
线性顺序,在重新排列对象时,会考虑到对象之间的引用关系,例如对象A引用了对象V,那么就会尽量将对象A,B排列在一起.
滑动排序,按照原先的排列顺序滑动到堆的另一端.
现在大部分垃圾回收算法都是按照任意顺序或者是滑动顺序去实现的.
MinorGC和FullGC使用的分别是什么算法
MinorGC:是发生在新生代的GC, 采用了复制算法
FullGC: 是发生在老年代的GC, 一般也伴随着MinorGC, 它采用了标记-清除算法, 速度比Minor GC慢10倍以上
minorGC和majorGC分别什么时候发生
gc的主要作用是回收堆中的对象。通过可达性分析一个对象的引用是否存在,如果不存在,就可以被回收了。
GC目的:回收堆内存中不再使用的对象,释放资源
回收时间:当对象永久地失去引用后,系统会在合适的时候回收它所占的内存。
如果 Eden 空间占满了, 会触发 minor GC。
Minor(Scavenge) GC 后仍然存活的对象会被复制到 S0 (survivor 0)中去。
这样 Eden 就被清空可以分配给新的对象。
又触发了一次 Minor GC ,
S0 和 Eden 中存活的对象被复制到 S1 中,
并且 S0和 Eden 被清空。
在同一时刻, 只有 Eden 和一个 Survivor Space 同时被操作。
当每次对象从 Eden 复制到 Survivor Space 或者从 Survivor Space 中的一个复制到另外一个,
有一个计数器会自动增加值。
JVM会给对象增加一个年龄(age)的计数器,对象每“熬过”一次GC,年龄就要+1,待对象到达设置的阈值(默认为15岁)就会被移移动到老年代
也就是对象被引用超过15次就会被移动到老年代
JVM 会停止复制并把他们移到老年代中去.
同样的如果一个对象不能在 Eden 中被创建,
它会直接被创建在老年代中。
如果老年代的空间被占满会触发老年代的 GC, 也被称为 Full GC。
Full GC 是一个压缩处理过程, 所以它比 Minor GC 要慢很多。
综上,FULL GC 发生的原因有两种,①大对象分配时候引发老年代空间不足;②持续存活的对象转移到老年代引发的空间不足
有如下原因可能导致Full GC:
a) 年老代(Tenured)被写满;
b) 永久代(Perm)被写满;JDK1.8已经废弃了永久代
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;
Full GC:无官方定义,通常意义上而言指的是一次特殊GC的行为描述,这次GC会回收整个堆的内存,包含老年代,新生代,metaspace等。
但是实际情况中,我们主要看的是gc.log日志,其中也会发现在部分gc日志头中也有Full GC字眼,此处表示含义是在这次GC的全过程中,都是STW的状态,也就是说在这次GC的全过程中所有用户线程都是处于暂停的状态。
full gc就是全堆回收,新生代会回收,老年代也会垃圾回收,没有引用指向的对象就会被回收
MinorGC和FullGC其实就是年轻GC和老年GC的俗称
MajorGC:当老年代满时会触发Major GC,只有CMS收集器会有单独收集老年代的行为,其他收集器均无此行为。而针对新生代的Minor GC,各个收集器均支持。总之,单独发生收集行为的只有新生代,除了CMS收集器,都不支持单独回收老年代。因此MajorGC和fullGC通常来说是等价的
JVM full GC和Major GC的区别
Major GC
调用情况
用于回收老年代
执行major GC之前一定会执行一次Minor GC
当老年代空间不足的时候就会执行Major GC
Major GC比Minor GC慢上10倍
如果Major GC以后老年代空间还是不够用,就报OOM
Full GC
调用情况:
调用System.gc()的时候会执行
大对象转入
方法区空间不足
老年代的连续空间小于eden区中的对象的大小,如果还是小于,那么就查看是否开启了空间担保机制,如果没有开启,就直接进行full gc,如果开启了,就查看老年代最大连续可用空间是否大于历代晋升老年代对象的大小,如果大于,就执行Minor GC,但是这次Minor GC不一定是安全的,因为存在的要进入老年代的对象可能大于老年代中的连续空间(回收以后还是不够),如果小于,那么就直接进行full GC
如果Full GC以后老年代空间还是不够用,就报OOM了。
Major GC只是用于回收老年代(目前只有CMS会进行单独老年代的回收,所以是不是Major比较少用了?我也不知道,找不到详细说明的。),而Full GC用于回收全局。
JVM的GC
GC:垃圾回收
GC英文全称为Garbage Collection,即垃圾回收。
Java中的GC就是对内存的GC。
Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。
Java对象的分配,程序员可以通过new关键字,Class的new-Instance方法等来显示的分配;而对象的释放,程序员不能实时的进行释放,这就需要GC来完成。
JVM GC的种类
JVM常见的GC包括三种:Minor GC,Major GC与Full GC
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:
一种是部分收集(Partial GC)
一种是整堆收集(Full GC)
部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
老年代收集(Major GC/Old GC):只是老年代的垃圾收集
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集,目前,只有G1 GC会有这种行为
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
注意:JVM在进行GC时,并非每次都对所有区域(新生代,老年代,方法区)一起回收的,大部分时候回收的都是指新生代
GC的触发机制
年轻代GC(Minor GC)触发机制
触发机制:
当年轻代空间不足时,就会触发Minor GC,这里的年轻代空间不足指的是Eden区满,Survivor区满不会触发GC(每次Minor GC 会清理年轻代的内存)
因为Java对象大多具备朝生夕死的特新,所以Minor GC非常频繁,一般回收速度也比较快.
Minor GC会引发STW,暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行
老年代GC(Major GC/Full GC)触发机制
指发生在老年代的GC,对象从老年代消失时,我们说”Major GC或Full GC”发生了、
一般出现Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
触发机制:
也就是老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC
PS: Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长 如果Major GC后,内存还不足,就报OOM了
Major GC的速度一般会比Minor GC慢10倍以上
Full GC触发机制
触发Full GC执行的情况有如下五种:
调用System.gc(),系统建议执行Full GC,但是不必然执行
老年代空间不足
方法区空间不足
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
由Eden区,from区向to区复制时,对象大小大于to区可用内存,则把对象转存到老年代,并且老年代的可用内存小于该对象大小(那要是GC之后还不够呢?那还用说:OOM异常送上)
另外要特别注意: full GC是开发或调优中尽量要避免的
为什么需要把Java堆分代?
经研究,不同对象的生命周期不同,70%-99%的对象是临时对象
其实不分代完全可以,分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一个区域,当需要进行GC的时候就需要把所有的对象都进行遍历,GC的时候会暂停用户线程,那么这样的话,就非常消耗性能,然而大部分对象都是朝生夕死的,何不把活得久的朝生夕死的对象进行分代呢,这样的话,只需要对这些朝生夕死的对象进行回收就行了.总之,容易死的区域频繁回收,不容易死的区域减少回收.
扩展:分代回收机制的三个假说
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(GenerationalCollection)[插图]的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用。
假如要现在进行一次只局限于新生代区域内的收集,但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。
为了解决这个问题,就需要对分代收集理论添加第三条经验法则:
跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(称为“记忆集”,RememberedSet),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
所以说,虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
JVM各部分存放的分别是什么(JVM内存模型)
线程共享:堆,方法区
线程隔离:本地方法栈,虚拟机栈,程序计数器
JVM内存模型各部分分别存放的是什么?
程序计数器:主要存放代码执行的位置。分支、循环、跳转、异常处理、线程恢复等基础功能都需要一来这个计数器来完成。
虚拟机栈:用于保存栈帧,方法出口,局部变量,每当方法被调用时,都会产生一个栈帧用于保存局部变量表、操作数栈等
堆:存放对象实例和数组
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即编译器编译后的代码等数据
本地方法栈:存放本地(Native)方法
堆和栈的区别(原理):
堆:什么是堆?又该怎么理解呢?
①堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
·堆中某个节点的值总是不大于或不小于其父节点的值;
·堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
②堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。
③堆是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程。
④堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。
什么是“堆”,“栈”,“堆栈”,“队列”,它们的区别?
2
栈:什么是栈?又该怎么理解呢?
①栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。
②栈就是一个桶,后放进去的先拿出来,它下面本来有的东西要等它出来之后才能出来(先进后出)
③栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的Stack的大小。
一、堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2、堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
二、堆栈缓存方式区别:
1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
三、堆栈数据结构区别:
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。
那类的加载过程了解吗,一个类有可能被加载两次吗
可以,用不同的类加载器加载
每个类装载器都有自己的命名空间,其中维护着由它装载的类型。所以一个JAVA程序可以多次装载具有同一个全限定名的多个类型。这样一个类型的全限定名就不足以确定在一个JAVA虚拟机中的唯一性。因此,当多个类装载器都装载了同名的类型时,为了唯一表示该类型,还要在类型名称前加上装载该类型的类装载器来表示。
类加载的过程
从类的生命周期而言,一个类包括如下阶段:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,因为java支持运行时绑定。
1、加载
将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。
在加载阶段,虚拟机需要完成以下3件事情:
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2、链接
将Java类的二进制代码合并到JVM的运行状态之中。
• 验证
确保加载的类信息符合JVM规范,没有安全方面的问题。
• 准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意此时的设置初始值为默认值,具体赋值在初始化阶段完成。
• 解析
虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程。
3、初始化
初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
类加载器的代理模式
代理模式即是将指定类的加载交给其他的类加载器。常用双亲委托机制。
1、双亲委托机制
某个特定的类加载器接收到类加载的请求时,会将加载任务委托给自己的父类,直到最高级父类引导类加载器(bootstrap class loader),如果父类能够加载就加载,不能加载则返回到子类进行加载。如果都不能加载则报错。ClassNotFoundException
双亲委托机制是为了保证 Java 核心库的类型安全。这种机制保证不会出现用户自己能定义java.lang.Object类等的情况。例如,用户定义了java.lang.String,那么加载这个类时最高级父类会首先加载,发现核心类中也有这个类,那么就加载了核心类库,而自定义的永远都不会加载。
堆和栈分别存放什么:
方法区和堆都是线程共享的
堆:存放使用new创建的对象,全局变量,是线程共享的。
栈:用于存储局部变量表(形参也是局部变量)、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程,线程隔离。
栈和栈帧是什么关系?
栈帧:
1、一个栈中可以有多个栈帧,栈帧随着方法的调用而创建,随着方法的结束而消亡。
该栈帧中存储该方法中的变量,原则上各个栈帧之间的数据是不能共享的,但是在方法间调用时,jvm会将一方法的返回值赋值给调用它的栈帧中。每一个方法调用,就是一个压栈的过程,每个方法的结束就是一个弹栈的过程。压栈都将会将该栈帧置于栈顶,每个栈不会同时操作多个栈帧,只会操作栈顶,当栈顶操作结束时,会将该栈帧弹出,同时会释放该栈帧内存,其下一个栈帧将变为栈顶。栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
JDK1.8做了哪些变化?
(JDK1.7已经将原本位于永久代的字符串常量池移到堆中了,但是永久代的概念还存在,JDK1.8才彻底废除永久代,进而用元空间代替)
JDK7和JDK8的JVM内存模型的总结
1、方法区变化
这里介绍的是JDK1.8 JVM内存模型。1.8同1.7比,最大的差别就是:元数据区(元空间)取代了永久代,就是JDK8没有了PermSize相关的参数配置了。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
1)方法区与永久代的区别?
方法区只是JVM规范定义,而永久代为具体的实现,元空间也是方法区在jdk1.8中的一种实现。
2)为什么废除永久代?
- 官方文档:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
- PermGen很难调整,PermGen中类的元数据信息在每次FullGC的时候可能被收集,但成绩很难令人满意。
而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class总数,常量池的大小,方法的大小等。
并且永久代内存经常不够用发生内存泄露。
2、运行时常量池变化
在近三个JDK版本(1.6、1.7、1.8)中, 运行时常量池(Runtime Constant Pool)的所处区域一直在不断的变化,在JDK1.6时它是方法区的一部分;1.7又把他放到了堆内存中;1.8之后出现了元空间,它又回到了方法区。其实,这也说明了官方对“永久代”的优化从1.7就已经开始了。
贴一张 Java 8 的内存模型图:
永久代和元空间,JDK1.8为什么要使用元空间代替永久代?
Jdk8以后开始把类的元数据放在本地堆内存中,这一块区域就叫做Metaspace,该区域在jdk7及以前是属于永久带的,元空间和永久代都是用来存储class相关信息,包括class对象的Method,Field等,元空间和永久代其实都是方法区的实现,只是实现有所不同,所以说方法区其实只是一种JVM的规范
为什么要用元空间代替永久代:
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
元空间溢出?
元空间不属于Java虚拟机,使用的是本地内存,存放的是类及方法的一些信息,动态加载类或者频繁加载类信息,但是没有及时卸载,会导致元空间溢出
内存溢出和内存泄露(内存泄露的堆积会导致内存溢出)**
内存泄露(Memory Leak):程序在申请内存后,对象没有被GC所回收,它始终占用内存,内存泄漏的堆积最终会造成内存溢出。
内存溢出(Memory Overflow):内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件。通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出。
对象创建分配内存的两种方式(指针碰撞、空闲列表)、对象访问定位的两种方式(使用句柄、直接指针)
对象创建分配内存的两种方式
指针碰撞
假设Java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
对象访问定位的两种方式
通过句柄方式访问
在Java堆中分出一块内存进行存储句柄池,这样的话,在栈中存储的是句柄的地址
优点:
当对象移动的时候(垃圾回收的时候移动很普遍),这样值需要改变句柄中的指针,但是栈中的指针不需要变化,因为栈中存储的是句柄的地址
缺点:
需要进行二次定位,寻找两次指针,开销相对于更大一些
直接指针访问方式
Java栈直接与对象进行访问,在Java堆中对象帆布中必须考虑存储访问类型的数据的相关信息,因为没有了句柄了
优点:
速度快,不需要和句柄一样指针定位的开销
栈上分配与逃逸分析(JVM层面进行java性能优化的技巧)
栈上分配与逃逸分析是在JVM层面进行java性能优化的一个技巧
- 什么是栈上分配?
栈上分配主要是指在Java程序的执行过程中,在方法体中声明的变量以及创建的对象,将直接从该线程所使用的栈中分配空间。 一般而言,创建对象都是从堆中来分配的,这里是指在栈上来分配空间给新创建的对象。 - 什么是逃逸?
逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。
如果对象发生逃逸,那会分配到堆中。(因为对象发生了逃逸,就代表这个对象可以被外部访问,换句话说,就是可以共享,能共享数据的,无非就是堆或方法区,这里就是堆。)
如果对象没发生逃逸,那会分配到栈中。(因为对象没发生逃逸,那就代表这个对象不能被外部访问,换句话说,就是不可共享,这里就是栈。)
栈上分配与逃逸分析的关系
进行逃逸分析之后,产生的后果是所有的对象都将由栈上分配,而非从JVM内存模型中的堆来分配。
栈上分配可以提升代码性能,降低在多线程情况下的锁使用,但是会受限于其空间的大小。
分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
能在方法内创建对象,就不要再方法外创建对象。
判断对象是否存活的两种方式,引用计数法的缺点?(引用计数法、可达性分析法)**
引用计数算法
基本思想
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;
当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优缺点
优点:实现简单,判定效率高。
缺点:很难解决对象之间相互循环引用的问题。(因此主流的java虚拟机中没有选用此方法)比如两个对象相互持有对方的引用,除此之外,再无其他引用,实际上这两个对象已经不能再被访问,但是因为它们互相引用对方,导致引用计数器不为0,导致垃圾收集器不能回收它们。
可达性分析算法
通过一系列被称为”GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(即,对象不可达)时,则证明此对象不可用。
可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性应用的对象。
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
在主流的商用程序语言的主流实现中,都是通过可达性分析算法来判断对象是否存活的。
关于Object类的finalize()方法(jvm自动执行,无需手动调用,只能执行一次).
java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
jvm自动执行,无需手动调用,只能执行一次
JVM参数调优
-Xms、-Xmx、-Xss、-XX:NewRatio、-XX:SurvivorRatio、-XX:+PrintGCDetails、-HeapDumpOnOutOfMemory
发生OOM如何解决
首先尝试通过JVM参数调优扩大堆内存空间;再者dump出堆内存存储快照,使用JProfile工具进行分析
垃圾收集器(CMS问的居多,另外,如果谈及发生gc会给用户带来什么不好的体验,可以谈谈Stop the World)
Stop-The-World:
在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
类加载机制的过程,准备阶段做了哪些工作?(准备阶段会给类的静态变量分配内存(方法区)并赋初值,如果类的静态变量被final修饰,那么初始化的值就不是零值,而是声明的值)
准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;
静态变量a就会在准备阶段被赋默认值0。
对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666; 静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。
类的双亲委派模型定义,双亲委派模型的好处?如何破坏类的双亲委派模型?
一、基本概念
一个类是由加载它的类加载器和这个类本身来共同确定其在Java虚拟机中的唯一性。
二、什么是双亲委派模型
类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
双亲委派模型要求除了最顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,在双亲委派模型中是采用组合关系来复用父类加载器的相关代码的。
三、双亲委派模型的实现
双亲委派模型的实现是在java.lang.ClassLoader类的loadClass()方法中。
类加载时,JVM会首先检查当前是否已经被加载过该类,如果没有加载且父加载器非空则会调用父类加载器的loadClass()方法,否则默认使用启动类加载器作为父加载器。如果父加载器无法完成加载(抛出ClassNotFoundException异常)则调用自己的findClass()方法进行加载。
四、双亲委派模型的优势
1、避免类的重复加载
每次进行类加载时,都尽可能由顶层的加载器进行加载,保证父类加载器已经加载过的类,不会被子类再加载一次,同一个类都由同一个类加载器进行加载,避免了类的重复加载。
2、防止系统类被恶意修改
通过双亲委派模型机制,能保证系统类由Bootstrap ClassLoader进行加载,用户即使定义了与系统类相同的类,也不会进行加载,保证了安全性。
五、如何破坏双亲委派模型
某些情况下,需要由子类加载器去加载class文件,这时就需要破坏双亲委派模型。要破坏双亲委派模型,可以通过重写ClassLoader类的loadClass()方法实现。
由于Java中所有类都默认继承Object类,但由于JDK的保护机制,系统类不能由自定义类加载器完成加载,需要对加载的类做判断,如果类名是java开头,则由拓展类加载器或引导类加载器进行加载。
典型的打破双亲委派模型的例子有Tomcat与OSGI,这部分需要详细了解可以参考源码。
JVM的垃圾回收器有哪些?
CMS是重点
Serial:这是一个单线程收集器。但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
ParNew收集器:ParNew收集器其实就是Serial收集器的多线程版本。
Parallel Scavenge收集器:Parallel Scavenge收集器是一个新生代收集器,使用复制算法,又是并行的多线程收集器。
最大的特点是:Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下虚拟机使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器
CMS(Concurrent Mark Sweep)收集器
是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
CMS是老年代垃圾收集器,在收集过程中可以与用户线程并发操作。它可以与Serial收集器和Par New收集器搭配使用。CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。可以通过JVM启动参数:-XX:+UseConcMarkSweepGC来开启CMS。
CMS收集过程
CMS 处理过程有七个步骤:
初始标记(CMS-initial-mark) ,会导致stw;
并发标记(CMS-concurrent-mark),与用户线程同时运行;
预清理(CMS-concurrent-preclean),与用户线程同时运行;
可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
重新标记(CMS-remark) ,会导致swt;
并发清除(CMS-concurrent-sweep),与用户线程同时运行;
并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
其运行流程图如下所示:
- CMS收集器只收集老年代,其以吞吐量为代价换取收集速度。
- CMS收集过程分为:初始标记、并发标记、预清理阶段、可终止预清理、重新标记和并发清理阶段。其中初始标记和重新标记是STW的。CMS大部分时间都花费在重新标记阶段,可以让虚拟机先进行一次YoungGC,减少停顿时间。CMS无法解决”浮动垃圾”问题。
- 由于CMS的收集线程和用户线程并发,可能在收集过程中出现”concurrent mode
failure”,解决方法是让CMS尽早GC。在一定次数的Full
GC之后让CMS对内存做一次压缩,减少内存碎片,防止年轻代对象晋升到老年代时因为内存碎片问题导致晋升失败。
内存碎片问题
CMS是基于标记-清除算法的,CMS只会删除无用对象,不会对内存做压缩,会造成内存碎片,这时候我们需要用到这个参数:
-XX:CMSFullGCsBeforeCompaction=n
意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩。
G1收集器
是当今收集器技术发展的最前沿成果之一。是一款面向服务端应用的垃圾收集器。
特点:
①并行与并发
能充分利用多CPU,多核环境下的硬件优势,缩短Stop-The-World停顿的时间,同时可以通过并发的方式让Java程序继续执行
②分代收集
可以不需要其他收集器的配合管理整个堆,但是仍采用不同的方式去处理分代的对象。
③空间整合
G1从整体上来看,采用基于“标记-整理”算法实现收集器
G1从局部上来看,采用基于“复制”算法实现。
④可预测停顿
使用G1收集器时,Java堆内存布局与其他收集器有很大差别,它将整个Java堆划分成为多个大小相等的独立区域。
什么是是可达性分析算法?
现代虚拟机基本都是采用可达性分析算法来判断对象是否存活,可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾对象,会被 GC 回收。
GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢?
的标记算法我们可以了解为一个可达性算法,所以所有的可达性算法都会有起点,那么这个起点就是GC Root。
也就是需要通过GC Root 找出所有活的对象,那么剩下所有的没有标记的对象就是需要回收的对象。
GC Root 的特点
当前时刻存活的对象
可作为GC Root的对象:
所有正在运行的线程的栈上的引用变量。所有的全局变量。所有ClassLoader等等
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
CMS收集器的四个阶段,各个阶段做了什么,哪两个过程工作线程会停止工作?
- 初始标记, 需要 STW (Stop The World)
标记那些直接被GC root引用或被年轻代存活对象所引用的所有对象
- 并发标记
在这个阶段Garbage Collector会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到GC ROOTS遍历查找。并发标记阶段,它会与用户的应用程序并发运行。并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用
在上面的图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化,如标黑的那个对象
其实总的来说就是在“初始标记”阶段的基础上标记老年代可达的,被引用的对象
- 重新标记, 需要 STW (Stop The World)
修正并发标记期间对象的标记变动
这个阶段的目标是标记老年代所有的存活对象,由于之前的阶段是并发执行的,GC线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。 - 并发清除
这个阶段主要是清除那些没有标记的对象并且回收空间
“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
从GC日志可以得到什么信息?
来看日志:
1) 2020-03-17T19:03:20.118+0800: 6665.102:
2)[GC (Allocation Failure) 2020-03-17T19:03:20.118+0800: 6665.102:
3)[ParNew Desired survivor size 8716288 bytes, new threshold 6 (max 6)
4)- age 1: 6826872 bytes, 6826872 total
5)- age 2: 1060216 bytes, 7887088 total
6): 149828K->8895K(153344K), 0.0361997 secs]
7)6272826K->6139400K(8371584K),0.0363166 secs]
8) [Times: user=0.07 sys=0.00, real=0.03 secs]
第一行,为日志输出的时间。
第二行,表明了进行了一次 GC 回收,注意,由于这里没有 Full 关键字 ,表明是一次 Minor GC,并指明了 GC 的时间。 Allocation Failure则表示GC的原因是在年轻代中没有了足够的空间来存储数据了。
第三行,ParNew 同样指明了本次 GC 是发生在年轻代,并且使用的是ParNew垃圾收集器。该收集器采用复制算法回收内存,期间会停止其他工作线程,即Stop The World。
第三、四、五行,表示每次年轻代 GC 之后打印 survivor 区域内对象的年龄分布, threshold则表示设置的晋升老年代的年龄阈值为6。
第六行,分别表示GC前年轻代的使用容量,GC 后该区域使用容量,括号内是该区域的总容量。最后是该内存区域GC耗时,单位是秒。
第七行,分别表示堆内存在垃圾回收之间的大小、堆内存在垃圾回收之后的大小,堆区的总大小。
可以看到在 GC 后,回收对象占比很少。
第八行,显示三个耗时,分别是用户态耗时、内核态耗时、总耗时。
从以上信息我们可以分析得出以下结论:GC的时间,GC发生的位置,当前年轻代使用的容量,对内存回收前后的大小,耗时
本次 GC 新生代减少了: 149828 – 8895 = 140933K。
堆内存区域共减少了: 6272826 – 6139400 = 133426K。
再把两个等号后的结果相减: 140933 – 133426 = 7507K
说明该次共有7507K(7.3M)内存从年轻代移到了老年代,可以看出来数量并不多,说明都是生命周期短的对象,只是这种对象有很多。
类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
- 根类加载器(bootstrap class loader):负责加载存储在<JAVA_HOME>/jre/lib目录中核心Java类
- 扩展类加载器(extensions classloader):用来在<JAVA_HOME>/jre/lib/ext,或java.ext.dirs中指明的目录中加载 Java的扩展库
- 系统类加载器(system classloader):根据 Java应用程序的类路径(java.class.path或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的
产生OOM的几种原因
一般来说只要知道前面常见的三四种可以了
1.堆溢出
这种场景最为常见,报错信息:
java.lang.OutOfMemoryError: Java heap space
原因
- 代码中可能存在大对象分配
2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。解决方法1、检查是否存在大对象的分配,最有可能的是大数组分配 - 通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
- 如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存 4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable
对象,也有可能是框架内部提供的,考虑其存在的必要性
2.永久代/元空间溢出
报错信息:
java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
原因
方法区空间已满
永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:字符串常量由永久代转移到堆中和永久代相关的JVM参数已移除可能原因有如下几种:
- 在Java7之前,频繁的错误使用String.intern()方法
- 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
- 应用长时间运行,没有重启
3.GC overhead limit exceeded
这个异常比较的罕见,报错信息:
java.lang.OutOfMemoryError:GC overhead limit exceeded
原因
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
解决方法
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现
java.lang.OutOfMemoryError: Java heap space。 - dump内存,检查是否存在内存泄露,如果没有,加大内存。
4.方法栈溢出
报错信息:
java.lang.OutOfMemoryError : unable to create new native Thread
原因
出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。
解决方法
-
通过 -Xss 降低的每个线程栈大小的容量
-
线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制: /proc/sys/kernel/pid_max
/proc/sys/kernel/thread-max maxuserprocess(ulimit -u)
/proc/sys/vm/maxmapcount
5.非常规溢出
下面这些OOM异常,可能大部分的同学都没有碰到过,但还是需要了解一下
分配超大数组
报错信息 :
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。
解决方法
就是检查你的代码中是否有创建超大数组的地方。
swap溢出
报错信息 :
java.lang.OutOfMemoryError: Out of swap space
这种情况一般是操作系统导致的,可能的原因有:
- swap 分区大小分配不足;
- 其他进程消耗了所有的内存。
解决方案:
- 其它服务进程可以选择性的拆分出去
- 加大swap分区大小,或者加大机器内存大小
6.本地方法溢出
报错信息 :
java.lang.OutOfMemoryError: stack_trace_with_native_method
本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。这个异常出现的概率极低,只能通过操作系统本地工具进行诊断,难度有点大,还是放弃为妙。