JVM内存区域总共分为两种类型
特征
线程私有区域:依赖用户的线程创建而创建、销毁而销毁,因用户每次访问都会独立开启一个线程,跟本地的线程相对应(用白话文讲就是同生共死或朝生夕死);
线程共享区域:它是随着虚拟机的开启而创建,关闭而销毁;
名词解释
程序计数器
用户每次访问都会独立开启一个线程,程序计数器会记录每次当前执行代码的行号指示器
本地方法栈
本地方法栈是用来区别虚拟机调用外部的执行方法,而本地方法栈则为Native修饰,那么该方法是一个C栈,但 HotSpot VM蒋本地的方法区和虚拟机栈合二为一
虚拟机栈
当我们执行这个方法时同时会创建一个栈帧(Stack Frame)用来存储局部变量表、操作数帧、动态链接、方法出口信息;每一个方法从调用直到执行完成的过程,就对应着一个栈帧入栈到出栈的过程
堆(heap)
创建的数组和对象都放入java堆中,当然也是垃圾收集器重要回收的地方, VM主要采用分代收集算法,主要产生在新生代(Yong GC)(Eden 区From 和To)和 老年代
方法区
用来存储于类、运行常量池、静态的变量和编译编译后的代码等数据,java VM会把这些信息收集到方法区,即用java堆的永久代来实现方法区,这样就可以实现VM 像堆内存一样管理方法区的内存
Java堆内存可以分为:新生代(Enden、SurvivorFrom和SurvivorTo)老年代、元空间
新生代
是用来存放new的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
Enden
用来存放新的对象出生地(当新创建对象很大时,会直接分配到老年代),当新生代内存不够时会触动MinorGc,会对新生产生垃圾回收
SurvivorFrom
上一次 GC 的幸存者,作为这一次 GC 的被扫描者
SurvivorTo
保留了一次 MinorGC 过程中的幸存者
采用的算法
MinorGC 的过程(复制->清空->互换)
复制算法
执行流程
老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行
了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
JAVA8 与元数据
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
1.引用计数法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么个对象就是可回收对象。
相互引用的时候,内存不能回收;
所谓引用计数法,每个对象额外保存一个计数属性,如果有一个对象引用了它,那么该属性会加1,例,
A a = new A();
A a2 = a;
上面这段代码会在堆中生成一个A的对象实例,且a、a2都指向了该对象,那么该对象的计数属性便是2,又如,
A a = new A();
A a2 = a;
a = null;
a2 = null;
这时a、a2均指向了null,那么A的对象实例的计数属性则为0,按照引用计数法的定义这时该实例可以被回收。
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
上面的代码在堆中会有一个A的实例一个B的实例,且计数属性均为1,执行了第3、4两行代码后,两个实例的引用计数均为2,执行了5、6两行代码后两个实例的计数属性均为1,这时a、b均指向了null,但是堆中的两个实例的计数属性的值却不为0,那么这两个实例无法回收,存在内存泄漏的风险;
2.可达性分析
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对⻬填充(Padding)
给定⼀个具体的类,请分析对象的内存占用!
每个long类型的字段占⽤8字节,3个long字段占⽤24字节。byte 字段占⽤1个字节。
1.标记清除算法( Mark-Sweep )
分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图
从图中我们就可以发现,该算法最大的问题是:内存碎片化严重,效率低后续可能发生大对象不能找到可利用空间的问题
2. 复制算法( copying )
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是:可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
3. 标记整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:
加载→验证→准备→解析→初始化
1. 加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口;
2. 验证
确保这个类符合要求
3. 准备
加载一些静态变量和常量
4. 解析
把编译class文件进行解析
5.初始化
初始化默认对象属性信息
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
1. 新生代采用复制算法
目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都需要频繁回收对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用 Eden 空间和其中的一块Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
2. 老年代采用复制算法
而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。
在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
Csdn : JAVA内存泄漏和内存溢出的区别和联系_马帅的博客的博客-CSDN博客_java内存泄漏和内存溢出
1. 内存泄漏 memory leak
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
2、内存溢出 out of memory
指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
3、二者的关系
内存泄漏的堆积最终会导致内存溢出
内存溢出:就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
内存泄漏:是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错
4、内存泄漏的分类(按发生方式来分类)
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4.隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
5、内存溢出的原因及解决方法:
1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3. 代码中存在死循环或循环产生过多重复的对象实体;
4. 使用的第三方软件中的BUG;
5. 启动参数内存值设定的过小
(2)内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
5.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
栈上分配:就是把没发生逃逸的对象,在栈帧分配空间。(一般对象分配空间是在堆)逃逸
逃逸分析(个人理解):就是方法内的对象,可以被方法外所访问
栈上分配:就是把没发生逃逸的对象,在栈分配空间。(一般对象分配空间是在堆)逃逸
二者联系:jvm根据对象是否发生逃逸,会分配到不同(堆或栈)的存储空间。
如果对象发生逃逸,那会分配到堆中。(因为对象发生了逃逸,就代表这个对象可以被外部访问,换句话说,就是可以共享,能共享数据的,无非就是堆或方法区,这里就是堆。)
如果对象没发生逃逸,那会分配到栈中。(因为对象没发生逃逸,那就代表这个对象不能被外部访问,换句话说,就是不可共享,这里就是栈。)
那我们再想深一层,为什么会有逃逸分析,有栈上分配这些东西?
当然是为了主体的考虑,主体就是jvm,或者直接说为了GC考虑也不为过。大家想想,GC主要回收的对象是堆和方法区。GC不会对栈、程序计数器这些进行回收的,因为没东西可以回收。
1.Linux 命令使用 top命令
2.执⾏top -p 2732单独监控该进程
在第2步的监控界⾯输⼊H,获取当前进程下的所有线程信息
4.执⾏jstack 2732对当前的进程做dump,输出所有的线程信息
同时将第4步得到的线程⼗进制编号2734转成16进制(AAE),在堆栈信息⾥⾯去找对应线程内容
内存泄漏的特点:
对象一直被引用,GC不能被回收,导致内存泄漏,一直内存泄露没有问题,多次内存泄露可能会引起内存溢出
我们可以通过Jstack 查看老年代是否被回收,比如第一次是200M,第二次是300M
Jstack 后台回收的时候执行的时候,看到FGC大于YGC回收;
-Xmx 最大堆内存配置 -Xmx=2G
-Xms 最小堆内存配置 -Xms=2G
最大堆内存和最小堆内存要配置一样,因当Xms设置较小时,占用老年代空间比较大,老年代内存满了会产生FULL GC
新生代和老年代的比例 默认比例是1:2,最好配置中1:1控制
-Xss 最大栈内存配置
新生代配置8:1:1
收集器的一些配置
线程共享:
堆:new 对象和数组,栈上分配对象
方法区 :class对象、静态变量 常量池
线程私有:
程序计数器:某一个线程执行的行号位置
本地方法栈:调用外部方法 native 关键词修饰的 变量
虚拟机栈:每一个方法属于一个栈帧,而栈帧里面包括
引用计数法
可达性分析
复制算法
分代算法
标记算法
标记整理算法
Serial 串联
parNew 多线程
Parallel Scavenge 并发变
cms 四个阶段
初始标记:
并发标记:
重新标记:
并发删除:
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。
加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:
public static int v = 8080;
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器
但是注意如果声明为:
public static final int v = 8080;
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:
1. CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info
等类型的常量。
符号引用
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
类构造器
初始化阶段是执行类构造器
量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子
的
器可以不为这个类生成
注意以下几种情况不会执行类初始化:
类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提供了 3 种类加载器:
启动类加载器(Bootstrap ClassLoader)
扩展类加载器(Extension ClassLoader)
应用程序类加载器(Application ClassLoader):
JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。
双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的
⽗加载器进⾏加载。这样⼀层层向上传递,直到祖先们都⽆法胜任,它才会真正的加载。
需要根据系统的配置来确定,要给操作系统和JVM其他内存区域(栈、⽅法区)留下⼀定的剩余空间。
推荐配置系统或容器⾥可⽤内存的 70 %~80%最好。
假设物理内存是8G,设置多⼤堆内存⽐较合适?
系统有 8G 物理内存,系统⾃⼰可能会⽤掉⼀点,⼤概还有 7.5G 可以⽤。
那么建议配置-Xmx6g (7.5g*0.8=6g)
Java8版本的Hotspot JVM
,默认情况下使用的是并行垃圾收集器(Parallel GC)。其他⼚商提供的JDK8基本上也默认使⽤并行垃圾收集器。
并行垃圾收集,是指使⽤多个GC worker 线程并行地执行垃圾收集,能充分利用多核CPU的能力,缩短垃圾收集的暂停时间。
除了单线程的GC,其他的垃圾收集器,比如 PS,CMS, G1等新的垃圾收集器都使用了多个线程来并行执行GC⼯作
因为GC过程中,所有应⽤线程都需要暂停之后才能执⾏GC,这时候就称为STW,或者叫做GC暂停。
安全点
用户线程暂停,GC线程要开始⼯作,但是要确保⽤户线程暂停的这行字节码指令是不会导致引⽤关系的
变化。所以JVM会在字节码指令中,选⼀些指令,作为“安全点”,比如方法调用、循环跳转、异常跳转
等,⼀般是这些指令才会产生安全点。
为什么它叫安全点,是这样的,GC时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是
主动是中断。
主动式中断是设置⼀个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个
标志,⼀旦发现中断标志为True,就会在自己最近的“安全点”上主动中断挂起。
安全区域
为什么需要安全区域?
要是业务线程都不执行(业务线程处于Sleep或者是Blocked状态),那么程序就没办法进⼊安全点,对
于这种情况,就必须引⼊安全区域。
安全区域是指能够确保在某⼀段代码⽚段之中,引⽤关系不会发⽣变化,因此,在这个区域中任意地方
开始垃圾收集都是安全的。我们也可以把安全区城看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间⾥JVM要发
起GC就不必去管这个线程了。
当线程要离开安全区域时,它要JVM是否已经完成了(根节点枚举,或者其他GC 中需要暂停⽤户线程
的阶段)
1、如果完成了,那线程就当作没事发生过,继续执行。
2、否则它就必须⼀直等待,直到收到可以离开安全区域的信号为止。
三色标记法是⼀种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW(Stop The World),从而达到清除 JVM 内存垃圾的目的。
三色标记法将对象的颜色分为了黑、灰、白三种颜⾊。
黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了;
灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用 ;
白色:表示对象没有被垃圾收集器访问过,即表示不可达;
三⾊标记的漏标问题
C被漏标了(不是垃圾的当做垃圾)
CMS的解决⽅案:Incremental Update(重新扫描)
G1的解决⽅案:SATB(snapshot-at-the-beginning)