JVM如何加载.class文件

alt text
JVM主要由Class Loader Runtime Data Area Executuon Engine Native Interface这四个部分组成,主要通过Class Loader将符合特定格式的class文件加载到内存里,并通过Excution Engine去解析class文件里面的字节码,并提交给操作系统去执行
Native Interface:融和不同开发语言的原生库为java所用, java的执行性能在绝大多数情况下并没有c或c++高,主流的jvm也是基于c++去实现的,因此在涉及到需要较高执行性能的运算操作的时候,是需要在java里去直接调用他们的,本着不重复造轮子的原则,在实际生产中某个库,如果已经用到别的语言来进行开发了,我们就不需要去再开发一套,而是希望java对这些库进行调用。为了满足这个需求,jvm在内存中专门开辟了一块区域处理标记为native的代码,具体做法是native method stack中登记native方法,在excution执行时,加载native liabraies

jvm内存模型

线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享:MetaSpace,Java堆

程序计数器

较小的一块内存空间,可以看做是当前线程所执行的字节码行号指示器(逻辑),在虚拟机的概念模型里,字节码解释工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,包括分支循环异常处理跳转,线程恢复等基础功能都需要这个计数器来完成,由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻一个处理器只会执行一条线程中指令,因此,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我门称这类内存为线程私有的内存,如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,计数器的值为Undefined,由于只是记录行号,程序计数器不必担心内存泄漏的问题

java虚拟机栈

局部变量(包括方法的参数)
java方法执行的内存模型,每个方法被执行时都会创建一个栈帧,单个线程每个方法执行的栈帧包含了方法运行期间的数据结构用于存储局部变量表,操作数站,动态连接,方法出口等。每个方法执行中对应虚拟机栈帧从入栈到出栈的过程,当方法调用结束时,帧才会被销毁。所以栈的内存不用通过gc去回收,它会自动释放

  • 局部变量表:这是一组变量值存储空间,主要用于存放方法参数和方法内定义的局部变量。
  • 操作数栈:这是一种数据结构,用于存储中间结果和运算指令。
  • 动态连接:将符号引用转换为直接引用。就比如调用一个方法,方法名就是一个符号引用,动态链接获取方法区中的该方法的入口地址。
  • 方法出口:它是方法执行完后返回的地址。

本地方法栈

与虚拟机栈相似,主要作用于标注了native的方法

方法区

.class字节码文件(包括所有的方法,静态变量)

方法地址,成员变量,new java对象,数组元素
java堆是java虚拟机管理的内存最大的一块
2、java堆是被所有线程共享的
3、java堆的主要作用是存放对象实例
4、ava堆是垃圾收集器管理的主要区域
5、收集器基本都采用分代收集算法
6、java堆可以分为新生代和老年代,再细致还可以分为Eden区,FromSurivor区,ToSurivor区

递归为什么会引发java.lang.StackOverflowError

发生的情形:

  • 调用(没有结束递归条件的)递归函数
  • 创建大量的线程或方法,使得栈帧超量
    递归每调用自身一次就会创建一个栈帧,栈所能容纳的栈帧是固定的,当调用方法或者线程时,jvm会向栈中压入一个栈帧,当栈帧数量超过一定数量则会发生错误

OutOfMemoryError

发生的原因:当程序运行过程中,无法申请到足够的内存就会发生错误
发生的情形:
栈内存溢出:创建了太多线程,没有足够的空间为一个新的线程分配空间
堆内存溢出:创建了太多对象,没有足够的空间为一个新的对象分配空间
方法区内存溢出:创建了Java类的相关信息,没有足够的空间为新定义的变量分配空间
元空间(MetaSpace )与永久代(PermGen )的区别

元空间使用本机内存(堆中),而永久代使用的是jvm的内存

MetaSpace没有了字符串常量池,在jdk7的时候移动到了堆中,因为可能会出现永久代内存不够的情况。
字符串常量池存在永久代中,容易出现性能问题和内存溢出
类和方法的信息大小难易确定,给永久代的大小指定带来困难
永久代会为GC带来不必要的复杂性
方便HotSpot与其他JVM如Jrockit的集成

  • 元空间:如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出OutOfMemoryError:Metaspace。异常
  • 永久代:达内存配置上限抛出 OutOfMemoryError:PermGen space 异常

jvm三大性能调优参数-Xms -Xmx -Xss的含义

-Xss:规定了每个线程虚拟机栈(堆栈)的大小 一般256k足够
-Xms:堆的初始值 即该线程刚创建出的时候在java堆中的大小
-Xmx:堆能到达的最大值 当对象容量超过堆初始值就会扩容到最大值

-Xms512m 等价于 -XX:InitialHeapSize,设置jvm初始堆内存为512M
-Xmx2048m 等价于-XX:MaxHeapSize,设置jvm最大堆内存为2048M

java内存模型中堆和栈的区别——内存分配策略

静态存储:编译时确定每个数据目标在运行时的存储空间需求 ,这种不允许有可变数据的存在,也不许有嵌套递归的结构出现 因为会导致编译程序无法计算程序的存储空间
栈式存储:数据区需求在编译时未知,运行时模块入口前确定
堆式存储:编译时或运行时模块入口都无法确定,动态分配

java内存模型中堆和栈的区别

管理方式:jvm可以针对栈自身进行管理操作,内存空间可自动释放,堆需要gc
空间大小:栈比堆小 堆需要存储java对象等较多数据
碎片相关:栈产生的碎片远小于堆 gc回收不是时时存在,而且空间较大,内存碎片可能会逐渐累积。而栈会自动释放。
分配方式:栈支持静态和动态分配,而堆仅支持动态分配
效率:栈的效率比堆高 栈只有两个最简单的指令入栈和出栈 堆底层实现可能是双向链表的结构 所以在管理时比栈复杂很多

对象被判定为垃圾的标准

没有被其他对象引用

判定对象是否为垃圾的算法

引用计数算法:

通过判断对象的引用数量来决定对象是否可以被回收
每个对象实例都有一个引用计数器,该对象被引用则+1,当该对象的某个引用超过了生命周期或者被设置为一个新值时,该对象的引用计数便会-1
任何引用计数为0的对象实例可以被当做垃圾收集
优点:执行效率高,程序执行受影响较小 ,因为只需要过滤出计数为0 的对象即可,可以交织在程序运行中,垃圾回收时可以做到几乎不会打断程序的执行,因此对程序不能被打断的实时环境比较有利
缺点:由于实现过于简单,无法检测出循环引用的情况,导致内存泄漏,比如MyObject a1=new MyObject MyObject a1=new MyObject
a1.age=a2 a2.age=a1

可达性分析算法:

可达性分析算法思路就是通过判断对象的引用链是否可达来决定对象是否可以被回收,垃圾回收器会对内存中的整个对象图遍历,程序把所有一系列的称为 GC Roots的对象作为起点,往下节点搜索,搜索走过的路程称为引用链,当一个对象到 GC Roots没有任何引用链,即为不可达对象,则证明此对象不可用。
作为GC Roots 的对象包括下面几种(主要是前四种)
虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
方法区中类静态属性引用的对象;java 类的引用类型静态变量。
方法区中常量引用的对象;比如:字符串常量池里的引用。
活跃线程的引用对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。
所有被同步锁(synchronized 关键)持有的对象。
JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
JVM 实现中的“临时性”对象,跨代引用的对象。

垃圾回收算法

标记清除算法(mark and sweep):

将回收分为两个阶段
从根集合进行扫描,对存活的对象进行标记,所以用的是可达性算法来找到垃圾对象,标记完毕后会对堆内存从头到尾进行线性遍历
如果发现有对象没有被标识为可达对象,那么就将此对象占用的内存给回收了,并且将原来标记为可达的对象的标识给清除掉,以便进行下一次垃圾回收
标记清除算法的主要不足是,由于标记清除不需要进行对象的移动并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作

alt text
垃圾收集后呢内存中存在3个不连续的内存碎片块,假设一个方块代表一个单位的内容,如果有一个对象需要占用3个内存单位的话,那么就会导致Muter一直处于暂停状态,而collector呢一直在尝试进行垃圾收集直到out of memory

复制算法:

复制算法将可用的内存按容量按一定比例划分为两块或多个块并选择其中一块或者两块作为对象面,其他的则作为空闲面,对象主要是在对象面上创建的
当被定义为对象面的内存快用完了,就将还存活着的对象就是还不是垃圾的这些对象复制到其中一块空闲面上,然后再把已使用过的内存空间一次清理掉
这种算法适用于对象存活率低的场景,比如年轻代,这样使得每次都对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,推倒重建只需要移动堆顶指针,按顺序分配内容即可,实现简单运行高效
复制收集算法在应对对象存活率较高的情况时,就有点力不从心了,要进行较多的复制操作效率将会变低,更关键的是如果不想浪费50%的空间就需要有额外的空间进行分配安保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法

记整理算法:

比较适用于老年代的对象回收,它采用标记清除算法一样的方式进行对象的标记,但在清除时不同在清除的过程中移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收,标记整理算法是在标记清除算法的基础上又进行了对象的移动,因此成本更高但是却解决了内存碎片的问题,适用于对象存活率极高的场景

分代收集算法(generational collector比较主流的一种垃圾回收算法):

将对内存进行进一步划分,不同的对象的生命周期及存活情况是不一样的,将不同生命周期的对象分配到堆中不同的区域,并对堆内存不同区域采用不同的策略进行回收,是可以提高这边垃圾回收执行效率
JDK七以前,Java对内存一般可以分为年轻代就是young generation
老年代old generation和永久代permanent generation这三个模块
而JDK8以后,包括JDK8版本中呢永久代便被去掉了只剩下了old generation,young generation及元空间
在JDK678乃至现有的版本中,年轻代和老年代呢均被保留了下来,年轻代的对象存活率低就会采用复制算法,而老年代存活率高就用标记清除算法或者标记整理算法
分代收集算法的GC分为两种:
第一种是minor GC:
发生在年轻代中的垃圾收集动作,所采用的是复制算法,使得在进行内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针按顺序分配即可而在回收的时候呢一次性将某个区域清空,简单粗暴高效。年轻代主要被分成两个区域,Eden区和两个survivor区,对象刚被创建出来的时候,其内存空间是被首先分配在Eden区的,当然若Eden区放不下新创建的对象的话,对象也有可能被直接放在survivor,甚至是老年代中,两个survivor区则分别被定义为from区和to区,并且哪个是from区,哪个是to区也不是固定的会随着垃圾回收的进行而相互转换,年轻代的目标就是尽可能快速的收集掉那些生命周期较短的对象,一般情况下会按照八比一比一的默认比例分为一个Eden区和两个survivor区,绝大部分对象在Eden区中生成,每次使用Eden区和其中的一块survivor,当进行垃圾回收时将Eden和survivor中的存活的对象一次性的复制到另一块survivor空间上,最后清理掉Eden区和刚刚用过的survivor空间,那当survivor空间不够用的时候,则需要依赖老年代进行分配的担保
对象在survival区每熬过一次minor GC其年龄呢就会被加一,当对象的年龄达到某个值时默认是15岁,Max Touring threshold来调整这个岁数这些对象呢会成为老年代但这也不是一定的对于一些较大的对象即需要分配一块较大的连续内存空间的对象,比如说我们这里Eden区装不下的对象或者说survive区装不下的对象呢就会进入到老年代
第二种Full GC:
与老年代相关,老年代的回收一般会伴随着年轻代的垃圾,收集老年代的内存,要比新生代的内存要大大概的比例是2:1,因此第二种方式被命名为Full GC,major GC呢通常是跟Full GC是等价的,Full GC的速度呢会比minor GC慢得多一般会慢10倍以上,不过一般情况下由于老年代里面的对象大多数是在survivor区域中熬过来的,他们是不会那么容易就死掉了的,因此负GC发生的次数不会有minor GC,
什么情况下会触发Full GC的场景:
1 老年代空间不足,如果创建一个大对象Eden区域当中放不下这个对象,就会直接保存在老年代中,如果老年代空间也不足就会触发负GC
2 永久代空间,这仅仅是针对JDK7以及以前的版本,当系统中需要加载的类
这样的方法很多,同时永久代当中没有足够的空间去存放我们的这些类啊的信息还有方法的信息,就会触发出一次Full GC,而JDK 8以后,由于取消了永久代
该条件不成立,这也是为什么要用元空间替代永久代的原因,为的是降低负GC的频率减少GC的负担提升起效率,或者元空间不足了
3 对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed:是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;
concurrent mode failure:是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)
4 如果之前统计所得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发Full GC。例如程序第一次触发GC后有6兆的对象晋升到老年代,当下一次minor GC发生时,首先先检查老年代的剩余空间是否大于6兆,如果小于6兆则执行Full GC
5 就是当我们在程序里面直接调用system.GC这个方法时候,就会显式直接触发Full GC,只是提醒虚拟机在这回收一下对象,回不回收呢还是虚拟机来决定
6 使用RMI来进行RPC或管理的JDK应用,每小时执行1次Full GC((可以在应用启动时添加一个定时任务来实现。注册RMI服务实现Remote接口的方法))

常用的调优参数:

-XX:SurvivorRatio: Eden和 Survivor的比值,默认8:1
-XX:NewRatio:老年代和年轻代内存大小比例
-XX:MaxTenuringThreshold:独享从年轻代晋升到老年代所经过GC次数的最大阈值

对象是如何晋升到老年代的:

1 长期存活的对象会进入到老年代,对象在新生代每经历一次minor GC依然存活则年龄加一若年龄超过一定限制默认是15岁的时候则被进升到老年代
2 survive区中或者Eden区中放不下的对象会分派担保进入到老年代
3 通过这个杠x x这个prettysize threshold去控制大对象的大小,如果超过这个size的对象一经生成立马放入老年代

stop the world

意味着JVM由于要执行GC而停止了应用程序的执行
并且这种情形呢会在任何一种GC算法中发生,当stop the world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成
事实上GC优化很多时候就是指减少stop the world发生的时间从而使系统具有高吞吐低停顿的特点

Safepoint

分析过程中对象引用关系不会发生变化的点
产生Safepoint的地方:方法调用;循环跳转;异常跳转
安全点数量得适中

JVM的运行模式

server jvm的初始堆空间会大一些。默认使用的是并行垃圾回收期 启动速度慢运行快 因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化
client 初始对堆空间小一些 使用串行的垃圾回收期 它的目标是为了让jvm的启动速度更快 但运行速度比server慢 采用的是轻量级的虚拟机,所以Server启动慢但稳定后速度比client要快
jvm根据硬件的操作系统自动选择使用server还是client的jvm

年轻代中常见的垃圾收集器

Serial:复制算法,Serial收集器是采用的是复制算法的单线程的垃圾,在它进行垃圾收集时,必须暂停其他所有的工作线程直到它收集结束,不过实际上到目前为Serial收集器依然是虚拟机运行在client模式下的默认年轻代收集器,因为它简单而高效
ParNew: 复制算法除了多线程进行垃圾回收外,其余的行为特点和Serial收集器是一样的它是Server模式下的虚拟机首选的年轻代收集器,在单个CPU环境中不会比Serial收集器有更好的效果,在多核执行下才有优势,除Serial外目前只有它能与CMS收集器配合工作;parallelScavenge:复制算法,比起关注用户线程停顿时间,更关注系统的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间)),在多核下执行才有优势,Sever莫斯下默认的年轻代收集器

老年代中常见的垃圾收集器

serial Old收集器:标记整理算法,单线程收集,进行垃圾收集时,必须暂停所有工作线程,简单高效,Client模式下默认的老年代收集器,在 JDK 5 以及之前的版本中与 ParallelScavenge 收集器搭配使用,作为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
paralle Old收集器:标记整理算法,多线程并发收集,在注重吞吐量以及CPU资源敏感的场合都可以优先考虑parallelScavenge加Parallel old收集器
CMS收集器:标记清除算法, 几乎占据着JVM老年代垃圾收集起的半壁江山,它垃圾回收线程几乎能与用户线程做到同时工作,几乎是因为它还是不能做到完全不需要stop the world,只是他尽可能的缩短了停顿时间,如果应用程序对停顿比较敏感并且在应用程序运行的时候,可以提供更大的内存和更多的CPU,也就是更牛逼的硬件,那么使用CMS来收集会给你带来好处,还有如果在JVM中有相对较多存活时间较长的对象会更适合使用CMS
它的整个垃圾回收过程: 六步
第一步初始化标记:在这个阶段中需要虚拟机停顿正在执行的任务,
即前面说的stopthe world,这个过程从垃圾回收的根对象开始只扫描到能够和根对象直接关联的对象并做标记,所以这个过程虽然暂停了整个JVM,但是很快就完成了。
第二次就会到并发标记:
这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记
并发标记阶段,用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿
接下来进行并发预清理:并发预清理阶段原来是并发的,在这个阶段虚拟机查找在执行并发标记阶段新进入老年代的对象,可能会有一些对象从新生代进升到老年代或者有些对象被分配到老年代,通过重新扫描,减少下一个阶段重新标记的工作因为下一个阶段会stop the world
下一个阶段重新标记:
这个阶段会暂停虚拟机,收集现成扫描在CMS堆中剩余的对象,扫描从根对象开始向下追溯并处理对象关联,这步会相对较慢一些,
接下来就会进行并发清理:即清理垃圾对象,这个阶段收集现成和应用程序线程是并发执行的。
并发清理完成了就到并发重置了:这个阶段是最后的阶段重置CMS收集器的数据结构,等待下一次垃圾回收
这6个步骤中,初始标记和重新标记是需要短暂的stop the world的,并发标记的过程实际上就是和用户现成同时工作,也就是一边丢垃圾一边打扫,这样就会带来一个问题,如果垃圾的产生是在标记后发生
那么这次垃圾就只有等到下次再回收,当然等到垃圾标记了过后垃圾自然不会和用户现成产生冲突,而清理过程就能和用户现成同时处理了,对于此垃圾回收器有一个比较显著且不可避免的一个问题,就是它所采用的是标记清除算法,也就是说它不会移动存活的对象,这样就会带来内存空间碎片化的问题,如果出现需要分配一个连续的较大的内存空间则只能触发一次GC了

  • 主要优点:并发收集、低停顿。
  • 对 CPU 资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。从 JDK9 开始,CMS 收集器已被弃用。

Garbage First收集器:既用于年轻代,也用于老年代的收集器
hospot开发团队赋予garbage first的使命是未来可以替换掉JDK5中发布的CMS收集器,与其他g c收集器相比garbage first有如下的特点:
第一个是并发和并行使用多个CPU来缩短stop the world的停留时间
与用户现成呢并发执行
第二个是分代收集,它独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间熬过多次GC的旧对象,以获得更好的收集效果
第三个是空间整合,是基于标记整理算法,这样就解决了内存碎片的问题
第四个是可预测的停顿,就是它能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为m毫秒的时间片段内,消耗在垃圾收集上的时间不得超过m毫秒,这些都是可以设置的
在garbage first之前的垃圾收集器,收集的范围都是整个年轻代
或者老年代,garbage first不再是这样,使用garbage first收集器时
java堆的内存布局与其他收集器有很大的差别,它会将整个java堆划分为多个大小相等的独立区域region,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,他们都是一部分,可以是不连续的region的集合,这就意味着在分配空间时不需要一个连续的内存空间即不需要在JVM启动时决定哪些region是属于老年代哪些属于年轻代,因为随着时间推移,年轻代region被回收以后就会变为可用状态,这时也可以把它分配成老年代
garbage first年轻代收集器是并行stop the world收集器,和其他的hospot GC一样,当一个年轻代GC发生时,整个年轻代会被回收
garbage first的老年代收集器有所不同,它在老年代不需要整个老年代进行回收,只有一部分region被调用garbage first GC的年轻贷由Eden region和survivor region来组成,当一个JVM分配Eden region失败后就会触发一个年轻代回收,这意味着Eden区间满了,然后GC开始释放空间。第一个年轻代收集器会移动所有的存储对象从Eden region到survivor region,这就是copy to survivor的过程

就会发现还有excellent GC和ZGC
为什么老年代中的这个CMS收集器不能与年轻代中parallelScavenge
收集器兼容,CMS是hospot在JDK 5推出的第一款真正意义上的并发
收集器,第一次实现了让垃圾收集现成与用户现成同时工作,CMS作为老年代收集器但却无法与JDK4已经存在的这个新生代的收集器parallelScavenge配合工作因为parallelScavenge以及garbage first都没有使用传统的GC收集器代码框架而是另外的独立实现的那其余的这几种收集器共用了部分的框架代码,因此呢它们能够结合在一起相互使用
Object的finalize()方法的作用是否与C++的析构函数作用相同:
与C++的析构函数不同,析构函数调用确定,而它的是不确定的
将未被引用的对象放置于F-Queue队列
方法执行随时可能会被终止
给予对象最后一次重生的机会

java跨平台原因

新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS整堆回收器:
G1新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;
老年代回收器一般采用的是标记-整理的算法进行垃圾回收。