什么情况下会发生栈内存溢出。
(递归调用)
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
(-Xss调整JVM栈大小)
(不断new对象)
如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
(-Xmx调整堆大小)
详解JVM内存模型
JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)
运行时数据区域:
- Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
- 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁
程序计数器:
- 线程私有,它的生命周期与线程相同(可以看做是当前线程所执行的字节码的行号指示器)
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
- 如果正在执行的是Natvie方法,这个计数器值则为空(undefined)
- 程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以此区域不会出现OutOfMemoryError的情况
Java虚拟机栈:
-
线程私有的,它的生命周期与线程相同(虚拟机栈描述的是Java方法执行的内存模型)
-
每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息
-
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
-
局部变量表存放了编译期可知的各种基本数据类型,对象引用(reference类型)
-
根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
-
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小
该区域可能抛出以下异常:
-
当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
本地方法栈:
- 与虚拟机栈非常相似,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务
- 有的虚拟机(譬如Sun HotSpot 虚拟机)直接把本地方法栈和虚拟机栈合二为一
- 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆:
- 被所有线程共享,在虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例都在这里分配内存
- Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块
- Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”
- 由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代
- 新生代又有Eden空间、From Survivor空间、To Survivor空间三部分
- Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常
方法区:
- 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 不需要连续的内存,并且可以动态扩展,动态扩展失败会抛出 OutOfMemoryError 异常
- 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代(Permanent Generation)来进行垃圾回收
- 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”
- 运行时常量池(Runtime Constant Pool)是方法区的一部分
- Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域
- 除了在编译期生成的常量,还允许动态生成(例如 String 类的 intern()),这部分常量也会被放入运行时常量池
元空间:
- 元空间是java8废弃永久代(PermGen),而新增部分
废弃原因
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
好处
- 方便分配管理,因为直接内存空间比较充足,便于回收
- 因为永久代本来回收垃圾的事件发生概率很低,直接从JVM中拿出可以提高回收效率。
JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor
- Eden和Survivor的比例是8:1
- 首先说如果没有Survivor区会出现什么情况:每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc(通常伴随着MinorGC,可以看做Full GC),比较耗时。
- 如果只有1个Survivor区,那当Eden区满了之后,就会复制对象到Survivor区,容易产生内存碎片化。严重影响性能。
- 所以使用2个Survivor区,始终保持有一个空的Survivor区,可以避免内存碎片化。
- Survivor减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历多次Minor GC还能在新生代中存活的对象,才会被送到老年代
Minor GC是发生在新生代中的垃圾收集,采用的复制算法
新生代中每次使用的空间不超过90%,主要用来存放新生的对象
Minor GC每次收集后Eden区和一块Survivor区都被清空
老年代中使用Full GC,采用的标记-清除算法
类需要同时满足下面3个条件才能算是“无用的类”:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代
- 对象诞生即新生代->eden(对象优先在新生代区中分配,若没有足够空间,Minor GC)
- 在进行minor gc过程中,如果依旧存活,移动到from,变成Survivor,进行标记。
- 当一个长期存活的对象(存活默认超过15次都没有被回收掉),就会进入老年代
- 大对象(需要大量连续内存空间)直接进入老年态
你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。
- Serial收集器:单线程的收集器,收集垃圾时,必须stop the world,使用复制算法
- ParNew收集器:Serial收集器的多线程版本,也需要stop the world,复制算法
- Parallel Scavenge收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。 如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%
- Serial Old收集器:是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
- Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
CMS:
- 初始标记:此时标记需要用户线程停下来
- 并发标记:此时标记可以和用户线程一起运行
- 重新标记:此时标记需要用户线程停下来,主要母的是为了对并发标记的垃圾进行审核
- 并发清除:与用户线程一起与运行进行垃圾清除
缺点:
- CMS收集器对cpu资源非常敏感
- CMS收集器无法清除浮动垃圾
- CMS基于标记清除的算法实现的,所以内存碎片会产生过多
G1收集器:
- 初始标记:标记GC Root能直接关联的对象,并且修改TAMS的值,让下一阶段的用户进行并发运行是,能够正确运用Region创建新对象,这阶段需要停顿,但停顿时间很短
- 并发标记:从GC Root开始对堆进行可达性分析,找出存活的对象,这段耗时较长,但可以与用户线程并发执行
- 最终标记是为了修正在并发标记阶段因用户程序继续运作导致标记产生变动的那一部分的标记记录,虚拟机将这部分标记记录在线程Remembered Set中,这阶段需要停顿线程,但是可并行执行
- 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期待的GC停顿时间来制定回收计划,这个阶段也可以与用户线程并行执行,但由于只回收一部分的Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
JVM中有哪些类加载器
- 启动类加载器
- 扩展类加载器
- 应用程序类加载器
- 自定义类加载器
简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。
类加载器:
- 类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象
- 启动类加载器(Bootstrap ClassLoader): 由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中
- 扩展类加载器(Extension ClassLoader): 负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库
- 应用程序类加载器(Application ClassLoader)。 负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。 一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型:
双亲委派过程
- 如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成
- 每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载
为什么需要双亲委派模型
- 防止内存中出现多份同样的字节码,保证类之间比较结果的唯一性
怎么打破双亲委派模型
- 继承ClassLoader 类,重写loadClass和findClass方法
JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存。
- 重排序:jvm虚拟机允许在不影响代码最终结果的情况下,可以乱序执行
- 内存屏障:可以阻挡编译器的优化,也可以阻挡处理器的优化
- 主内存:所有线程共享的内存空间
- 工作内存:每个线程特有的内存空间
happens-before原则:
- 一个线程的A操作总是在B之前,那多线程的A操作肯定实在B之前
- monitor 再加锁的情况下,持有锁的肯定先执行
- volatile修饰的情况下,写先于读发生
- 线程启动在一起之前 strat
- 线程死亡在一切之后 end
- 线程操作在一切线程中断之前
- 一个对象构造函数的结束都该对象的finalizer的开始之前
- 传递性,如果A肯定在B之前,B肯定在C之前,那A肯定是在C之前
JVM的引用类型有哪些?
- 强引用
- 软引用
- 弱引用
- 虚引用
强引用、软引用、弱引用、虚引用的区别?
强引用
- 特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用
- 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题
- 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了(具体回收时机还是要看垃圾收集策略)
强引用是造成java内存泄露的主要原因之一
软引用
- 特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。
- 只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
- 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中
- 我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收
- 如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象
软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存
弱引用
- 弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短
- 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
- 由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象
- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中
弱应用同样可用于内存敏感的缓存
虚引用
- 特点:虚引用也叫幻象引用,通过PhantomReference类来实现
- 无法通过虚引用访问对象的任何属性或函数
- 幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制
- 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
- 虚引用必须和引用队列 (ReferenceQueue)联合使用
- 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中
- 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收
- 如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动
可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知
ReferenceQueue 引用队列
- 对象在被回收之前要被引用队列保存一下。GC之前对象不放在队列中,GC之后才对象放入队列中
- 【通过开启线程监听该引用队列的变化情况】就可以在对象被回收时采取相应的动作
- 由于虚引用的唯一目的就是能在这个对象被垃圾收集器回收时能收到系统通知,因而创建虚引用时必须要关联一个引用队列,而软引用和弱引用则不是必须的
- 对于软引用和弱引用,当执行第一次垃圾回收时,就会将软引用或弱引用对象添加到其关联的引用队列中,然后其finalize函数才会被执行(如果没复写则不会被执行)
- 而对于虚引用,如果被引用对象没有复写finalize方法,则是在第一垃圾回收将该类销毁之后,才会将虚拟引用对象添加到引用队列
- 如果被引用对象复写了finalize方法,则是当执行完第二次垃圾回收之后,才会将虚引用对象添加到其关联的引用队列
- 一个对象的finalize()方法只会被调用一次,而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,
该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize()
不要使用finalize()方法
说一下jdk的对空间的内存划分是怎样的?
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 直接内存
- 方法区
- 元空间(jdk8)
GC的回收流程是怎样的?
判断对象已“死”
- 引用计数法
- 可达性算法
垃圾回收
- 在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收
说说垃圾回收期的一些常见算法?
Mark-Sweep(标记清除算法)
- 该算法适合存活对象较多,可回收对象较少的场景
- 执行过程是需要进行两遍的扫描,第一遍定位出非垃圾对象,第二遍扫描是将垃圾对象进行清理,执行效率相对偏低,并且容易产生碎片。
Copying(复制算法)
- 该算法适合存活对象较少的时候
- 将内存一分为二,每次回收都将非垃圾对象的从前一半内存拷贝的另一半内存中
- 整个过程只扫描一次,不会产生碎片,但是存在空间的浪费,因为只有一半的内存进行对象分配
Mark-Compact(标记整理算法)
- 在回收垃圾对象的时候同时进行整理的算法
- 同样需要进行两次扫描,第一次定位非垃圾对象,第二遍进行非垃圾对象的移动和整理
- 优点是不会产生碎片,内存利用率高,不像复制算法只利用了一半的内存。
引用计数法(Reference Counting)
- 对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1
- 当引用失效时,引用计数器就减 1
- 只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用
- 循环引用的问题:A引用B,B引用A(java中不使用)
在JVM中,如何判断一个对象是否死亡?
引用计数法
- 给对象增加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1,任何时刻计数器为0的对象就是可能不再被使用的,即该对象就称为可被回收的对象(Python采用的是引用计数法)
可达性分析法
- 通过一系列的"GC Roots"对象作为起点进行搜索,如果在"GC Roots"和一个对象之间没有可达路径,则称该对象是不可达的。
- 要确定这个对象是垃圾,至少要经过两次标记过程;在第一次标记之后会进行一次筛选,筛选的条件是此对象有没有覆写 Object类的finalize() 方法,如果覆写了并且没执行过,那就执行finalize方法,如果在执行过程中,该对象和GC Roots对象可达则可逃离一次死亡,如果没有覆写或者覆写了已经被执行过了,那么该对象直接死亡。
能做GC Roots的对象
- 常量引用的对象
- 静态变量引用的对象
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
请解释StackOverflowError和OutOfMemeryError的区别?
StackOverflowError
- 当线程请求的内存大小大于所配置的初始化大小,将抛出StackOverflowError
- 递归调用
- 请求栈深度大于最大深度
OutOfMemoryError
- 如果JVM内存大小是可扩展的,当然一般都是可以扩展的,当自动扩展到计算机本身内存大小时会抛出OutOfMemoryError
- 不断new对象
- 扩展栈时内存不足
类的生命周期
加载
- 通过一个类的全限定名类获取定义此类的二进制字节流
- 将字节流的所代表的静态存储结构转化成方法区运行时的数据结构
- 在内存中生成一个代表这个类的java.lang.Class的对象,做作为方法区的这个类各种数据访问的入口
连接
- 验证(确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机本身)
- 准备(给类变量赋予初始值的阶段。这里的初始值是指变量默认的值,并不是用户赋予的初始值)
- 解析(执行时无此步) 虚拟机将常量池内的符号引用替换为直接引用。会把该类所引用的其他类全部加载进来( 引用方式:继承、实现接口、域变量、方法定义、方法中定义的本地变量)
初始化
有且仅有 5 种情况必须立即对类进行“初始化”(主动引用)
- 在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发初始化
- 对类进行反射调用时,如果类还没有初始化,则需要先触发初始化
- 初始化一个类时,如果其父类还没有初始化,则需要先初始化父类
- 虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化
几种(被动引用)
- 通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化
- 通过数组定义来引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
使用
- 对象实例化
- 垃圾收集
- 对象终结
卸载
- JVM执行垃圾回收
JVM调优
调优工具
- jps:虚拟机进程状况工具
- jstat:虚拟机统计信息监视工具
- jmap:Java内存印象工具
- jhat:虚拟机堆转储快照分析工具
- jstack:Java堆栈跟踪工具
- jinfo:Java配置信息工具
- jConsole: JMX的可视化管理工具
命令调优
- xms
- xmx
JVM本身就是为低延时,高并发,大吞吐的服务设计和优化的
我们更应该注重服务本身调优
在没有特别巨大的数据量支撑的情况下对JVM进行调优就是瞎几把调