JVM分为 堆区,栈区,方法区,初始化的对象放在堆里,引用放在栈里,Class信息常量放在方法区里。
方法区:主要存储类信息,常量池,编译后的代码等数据
堆:初始化的对象,成员变量(非static),对象实例和数组在堆上分配
栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操 作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个引用类型,所 以还是一个指向地址的指针
本地方法栈:主要为Native方法服务
程序计数器:记录当前线程的执行行号
堆里面分为了新生代和老年代(java8移除了永久代,采用Metaspace元空间),老年代占据2/3的堆空间,新生代占据1/3的空间。新生代包含Eden(8/10)和survival区,survial区分为from(1/10)和to(1/10)。内存回收时,如果采用复制算法,从from复制到to,经过一次或者多次的GC后,存活的对象移到老年代,当内存满时,触发Full GC,清理老年代。
Eden用来存放新生对象,当新生代满了之后触发YGC,把存活的对象复制到survival中,然后清理Eden,整理内存(这种方式避免内存碎片化),下一次GC时就会使用survival,当创建一个特别大的对象时候,直接放到老年代中。
new 一个对象
引用计数法:当一个对象被引用时候+1,不再引用-1,当为0时就会回收,但是JVM未采用,无法判定相互引用(A->B,B->A)
引用链法:通过GC Root对象(方法区中的静态变量引用对象)判断,能到达GC Root说明被引用,不能到达可以回收
(GC Root包含以下
)
代表线程中的某些位置,线程可以在此停顿,执行GC,因此 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始 执行 GC,
1. 循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入 safepoint)
2. 方法返回前
3. 调用方法的 call 之后
4. 抛出异常的位置
标记清除:最基础的收集算法,先标记出所有的要回收对象,标记完成后统一回收被标记的对象。
特点:效率不高,容易产生大量内存碎片化
地方:适合老年代的垃圾回收,比如CMS算法
标记整理:分为标记和整理两个阶段:首先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
特点:不会产生空间碎片,但是整理会花一定的时间。
地方:适合老年代进行垃圾收集,parallel Old(针对parallel scanvange gc的) gc和Serial old收集器就是采用该算法进行回收的。
复制算法:它先将可用的内存按容量划分为大小相同的两块,每次只是用其中的一块。当这块内存用完了,就将还存活着的对象复制到另一块上面,然后把已经使用过的内存空间一次清理掉。
特点:没有内存碎片,只要移动堆顶指针,按顺序分配内存即可。代价是将内存缩小位原来的一半。
地方:适合新生代区进行垃圾回收。serial new,parallel new和parallel scanvage
收集器,就是采用该算法进行回收的。
复制算法改进思路:由于新生代都是朝生夕死的,所以不需要1:1划分内存空间,可以将内存划分为一块较大的Eden和两块较小的Suvivor空间。每次使用Eden和其中一块Survivor。当回收的时候,将Eden和Survivor中还活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Suevivor空间。其中Eden和Suevivor的大小比例是8:1。缺点是需要老年代进行分配担保,如果第二块的Survovor空间不够的时候,需要对老年代进行垃圾回收,然后存储新生代的对象,这些新生代当然会直接进入来老年代。
优化收集方法的思路
分代收集算法
原理:根据对象存活的周期的不同将内存划分为几块,然后再选择合适的收集算法。
一般是把java堆分成新生代和老年代,这样就可以根据各个年待的特点采用最适合的收集算法。在新生代中,每次垃圾收集都会有大量的对象死去,只有少量存活,所以选用复制算法。老年代因为对象存活率高,没有额外空间对他进行分配担保,所以一般采用标记整理或者标记清除算法进行回收。
(1)串行的,也就是采用单线程(比较老了),分类:serial new(收集年轻代,复制算法)和serial old(收集老年代,标记整理),缺点:单线程,进行垃圾回收时暂时所有的用户线程。优点:实现简单。
(2)并行的,采用多线程,对于年轻代有两个: parallel new(简称ParNew)(参考serial new的多线程版本)和parallel scavenge;parallel scavenge是一个针对年轻代的垃圾回收器,采用复制算法,主要的优点是进行垃圾回收时不会停止用户线程(不会发生stop the world)
特点:
CMS 收集器是基于“标记—清除”算法实现的,经过多次标记才会被清除
G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间) 上来看是基于“复制”算法实现的
MinorGC:新生代内存不够时
Full GC:JVM内存不够时
jstack 可以看当前栈的情况,jmap 查看内存,jhat 进行 dump 堆的信息 mat
加载,验证,准备,解析,初始化 最后是使用和卸载
通过全限定名来加载生成的class对象到内存中,然后进行验证这个class文件,包括文件格式校验,元数据验证,字节码验证。
准备是对这个对象分配内存。解析是将符号引用转化为直接引用,初始化就是开始执行构造器代码
‘java虚拟机主要分为以下几个区:
方法区:
1. 有时候也称为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里
进行的GC主要是对方法区里的常量池和对类型的卸载
2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后
的代码等数据。
3. 该区域是被线程共享的。
4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池
具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量
池中。
虚拟机栈:
1. 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都
会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
2. 虚拟机栈是线程私有的,它的生命周期与线程相同。
3. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地
址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表
对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
4.操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索
引来访问,而是压栈和出栈的方式
5.每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了
支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接
引用。
本地方法栈
本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。
堆
java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这
里创建,因此该区域经常发生垃圾回收操作。
程序计数器
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码
指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内
存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。
垃圾回收器(garbage colector)决定回收某对象时,就会运行该 对象的 finalize() 方法 但是在 Java 中很不幸,如果内存总是充 足的,那么垃圾回收可能永远不会进行,也就是说 filalize() 可能 永远不被执行,显然指望它做收尾工作是靠不住的。 那么 finalize() 究竟是做什么的呢? 它最主要的用途是回收特殊渠道 申请的内存。Java 程序有垃圾回收器,所以一般情况下内存问题 不用程序员操心。但有一种 JNI(Java Native Interface)调用 non-Java 程序(C 或 C++), finalize() 的工作就是回收这部 分的内存。
在java中,程序员不需要主动区释放内存,而是虚拟机自动执行,在JVM中,有一个垃圾回收线程,她是低优先级的,正常情况下不执行,当虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描未被引用的对象,并将他添加到待回收的集合中去,进行回收。
1. 标记-清除:
这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被
回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不
高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在
分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
2. 复制算法:
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只
使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然
后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,
内存的代价太高,每次基本上都要浪费一般的内存。
于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为
8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。
每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然
后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对
象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)
3. 标记-整理
该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高
时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回
收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
4. 分代收集
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生
代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那
么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担
保,所以可以使用标记-整理 或者 标记-清除
java内存模型(JMM)是线程间通信的控制机制.JMM定义了主内存和线程之间抽象关系。
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地
内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是
JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬
件和编译器优化。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值, 会触发完全垃圾回收(Full GC)
(注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的 native 内存区)
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最
终形成可以被虚拟机直接使用的java类型
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类
去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接
引用。
2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的
实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)
来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过
ClassLoader.getSystemClassLoader()来获取它。
4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现
1. 对象优先在堆的Eden区分配。
2. 大对象直接进入老年代.
3. 长期存活的对象将直接进入老年代.
当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor Gc通
常发生在新生代的Eden区,在这个区的对象生存期短,往往发生Gc的频率较高,
回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC
的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor
GC这样可以加快老年代的回收速度。