目录
JVM组成
什么是程序计数器
程序计数器是线程私有的,每个线程一份,内部保存的是字节码的行号。用于记录正在执行的字节码指令的地址。
作用:
- 控制程序指令的进行,实现分支、跳转、异常等逻辑。
- 在多线程情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行哪一句指令并继续解释执行。
详细的介绍一下Java的堆
参考回答:
- 堆是一个线程共享的区域:主要用来保存对象实例、数组等,内存不足时会抛出OOM异常。
- 组成包括年轻代和老年代:年轻代被划分为3部分,Eden区和两个大小严格相等的Survivor区。老年代主要保存生命周期长的对象,一般是一些老的对象。
- JDK1.7和JDK1.8的区别,1.7中有一个永久代,用于表示方法区,存储类信息、静态变量、常量、编译后的代码。1.8中移除了永久代,把数据存储到了本地内存的元空间中,用于防止OOM。
----------------------------------------------------------------------------------------------------------
【堆】线程共享的区域:主要用来保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,抛出OutOfMemoryError异常。
【JDK1.7和JDK1.8堆内存的区别】:
JDK1.7中堆内存包含(方法区\永久代中),JDK1.8删除永久代,随后将方法区放到本地内存中,名称改为元空间。
方法区中存放运行时的类的信息等内容,随着程序运行时间的推进,加载的内容越来越来,堆内存可能达到上限,造成OOM,故1.8将永久代删除,改为元空间用于存储方法区,同时将其放于本地内存中。
Java程序内存=Java堆内存 + 本地内存。
JVM内存(Java堆内存):Java虚拟机在执行的时候会把管理的内存分配到不同区域,这些区域称为虚拟机内存。
本地内存: 对于虚拟机没有直接管理的物理内存,称为本地内存。
直接内存:直接内存不是虚拟机运行时数据区的一部分,直接内存是在Java堆外地、直接向系统申请的内存区域。直接内存不受JVM管理,但是系统内存是有限的,物理内存不足时会报OOM。常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高。
什么是虚拟机栈
什么是虚拟机栈:
垃圾回收是否涉及栈内存:
垃圾回收主要指的是堆内存,当栈帧弹栈以后,内存就会释放。
栈内存分配的越大越好吗?
不一定,默认的栈内存通常为1024k,栈内存如果过大会导致线程数变少。如机器总内存为512M,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的线程数减半。
创建一个线程占用多少空间:根据栈内存的分配原则,创建一个线程占用1024k也就是1M空间。
方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
栈内存溢出的情况:
- 栈帧过多导致栈内存溢出,如:递归调用。
- 栈帧过大导致内存溢出。默认栈帧大小为1024k也就是1M,如果栈帧过大也会导致过早达到内存上限。
堆和栈的区别
- 栈内存一般用来存储局部变量和方法调用,堆内存是用来存储Java对象和数组的。【方法逃逸:JVM通过逃逸分析,分析出新对象的作用范围,就可能将对象在栈上进行分配】
- 堆会GC垃圾回收,而栈不会。
- 栈内存是线程私有的,而堆内存是线程共有的。
- 两者异常错误不同,但栈内存或堆内存不足都会抛出异常。
-
- 栈内存空间不足:Java.lang.StackOverFlowError。
- 堆内存空间不足:Java.lang.OutOfMemoryError。
堆空间的分配策略
- 新创建的对象优先分配到Eden区。
- 大对象直接分配到老年代。
- 长期存活的对象进入到老年代。
- 动态对象年龄判断:为了能更好的的适应不同程序的内存状况,虚拟机并不是永远得要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
- 内存分配担保:当出现大量对象在Minor GC 后仍然存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代剩余空间不足就需要进行Full GC。
对于方法区的解释
- 方法区是各个线程共享的内存区域。方法区的具体实现:JDK1.7是永久代(和堆内存连续分配),JDK1.8是元空间(对于直接内存中)
- 主要用于存储类加载器加载后的信息、常量、静态变量、编译器编译后的代码等。
- 虚拟机启动的时候创建,关闭虚拟机时释放。【栈帧方法调用的时候创建,方法调用结束的时候销毁】
- 如果方法区中的内存无法满足分配请求,则会抛出OutOfMemory:JDK1.8中将方法区的实现元空间移动到本地内存中,受本地内存大小的限制。
IO和NIO拷贝数据的对比
IO数据拷贝流程:
NIO数据拷贝流程:
IO数据拷贝流程:需要先将数据拷贝到系统缓存区【内核缓冲区】,再将数据从系统缓冲区拷贝到Java缓冲区【用户缓冲区】。
NIO数据拷贝流程:使用一个直接内存直接替代系统内存缓冲区到Java缓冲区的拷贝操作,相关IO数据拷贝步骤少,故耗时短。
JVM内存结构
JVM内存机构包括五部分:JDK1.7
- 线程共享的:方法区和堆。
- 线程私有的:程序计数器、Java虚拟机栈、本地方法栈。
- 方法区:存储虚拟机加载的类信息、常量、静态变量以及编译器编译后的代码等数据。
-
- JDK1.7之前方法区位于堆中,JDK1.7把字符串常量池和静态变量从方法区(永久代)移到堆中,JDK1.8方法区由元空间替代位于本地内存中,存储类信息以及编译后的代码。
- 堆:堆内存主要用于存放对象和数组,它是JVM管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。在垃圾收集的层面上来看,由于现在收集器基本上都采用分代收集算法,因此堆还可以分为新生代(YoungGeneration)和老年代(OldGeneration),新生代还可以分为 Eden、From Survivor、To Survivor。
- 程序计数器:用于记录正在执行的字节码指令的地址。
- 虚拟机栈:每个线程运行时所需要的内存称为虚拟机栈,虚拟机栈由多个栈帧组成,对应着每次方法调用时所占用的内存,当前时刻下虚拟机栈中只能有一个活跃的栈帧。如果栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,会抛出StackOverFlowError的错误;如果栈的内存大小允许动态扩展,如果虚拟机在动态扩展栈时无法申请到足够的内存空间,会抛出OOM错误。
- 本地方法栈:本地方法栈与虚拟机栈的区别是:虚拟机栈执行的是Java方法;本地方法栈执行的是本地方法。
JVM去除永久代改用元空间替代的原因
- 字符串存放在永久代,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,容易出现性能问题和内存溢出,太小容易出现永久代溢出,太大则容易导致老年代溢出。【永久代和线程共享的堆是连续的,本质上都是占用的堆内存】
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。【永久代和老年代一起被GC的】
类加载器
什么是类加载器,类加载器有哪些
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而使Java程序能够启动起来。
类加载器的分类:
什么是双亲委派模型
双亲委派模型:每个Java实现的类加载器中保存了一个成员变量叫“父”类加载器。自底向上查找是否加载过,再由顶向下进行加载。
JVM为什么采用双亲委派机制:
(1):避免某一个类被重复加载,当父类已经加载过后则无需重复加载,保证唯一性。
(2):为了安全,保证类库API不会被修改。
类装载的执行过程
类从加载到虚拟机中开始,直到卸载为止,整个生命周期包括:加载、连接(验证、准备、解析)、初始化、使用、卸载。
加载:
验证:验证类是否符合JVM规范,安全性检查。
准备:为类变量分配内存并设置类变量初始值。【类变量:被static修饰的变量】
static变量是final的引用类型:引用类型也就是在堆中创建新的对象。并将其地址赋值给变量。
2、3条是===>static变量是final的基本类型,以及字符串常量,值已确定的情况。
4条是===>static变量是final的引用类型。
解析:把类中的符号引用转换为直接引用。
符号引用:在字节码文件中使用编号来访问常量池中的内容。
直接引用:不使用符号,而是使用内存地址进行访问具体的数据。
初始化:对类的静态变量,静态代码块执行初始化操作。
- 子类直接访问父类的静态变量,只触发父类的初始化。
使用:JVM从入口方法开始执行用户的程序代码。
卸载:使用完毕之后进行卸载。
GC
对象什么时候可以被垃圾器回收
如果一个或者多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如何定位垃圾:
- 引用计数法:一个对象被引用了一次,在当前的对象头上递增一次引用计数,如果这个对象的引用次数为0,代表这个对象可以被回收。
-
- 当对象间出现循环引用的话,引用计数法就会失效。
- 可达性分析:扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以被回收。
-
- 哪些对象可以做为GC Root:
- 虚拟机栈、本地方法栈中引用的对象。
- 方法区中类静态属性引用的对象,方法区中常量引用的对象。
JVM垃圾回收算法有哪些
- 标记清除算法:垃圾回收分为标记、清除两个步骤
-
- 根据可达性分析算法得出的垃圾进行标记。
- 对标记为可回收的内容进行垃圾回收。
- 优点:标记和清除速度较快;缺点:碎片化较为严重,内存不连贯。
- 复制算法:将内存区域分为大小相等的两部分,from和to,当触发垃圾回收时,将from中的非回收的垃圾复制到to区,随后将from和to区互换。
-
- 优点:在垃圾对象较多的情况下,效率较高;清除后,无内存碎片。
- 缺点:分配的两块内存空间,在同一时刻,只能使用一半,内存使用率较低。
- 标记整理算法:标记、整理两个步骤
-
- 优缺点同标记清除算法,解决了标记清除算法的碎片化问题。
- 标记整理算法相比标记清除算法多了一步,对象移动内存位置的步骤,其效率会有一定的影响。
JVM中的分代回收
为什么要进行分代:分代收集算法并没有什么新思想,只是根据对象存活周期的不同将内存分为几块,将Java堆划分为新生代和老年代,从而可以根据各个年代的特点选择合适的垃圾收集算法。如:在新生代,每次收集都会有大量对象死去,可以选择标记-复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾回收;而老年代对象的存活几率较高,而且没有额外的空间对他们进行分配保证,所以我们必须选择标记-清除算法或者标记-整理算法。
堆区域的划分:堆被划分为2部分,新生代和老年代【1:2】;对于新生代,内部又被分为三个区域。Eden区,幸存区Survivor(分为from和to区)【8:1:1】。
对象等代回收策略:
- 新创建的对象,都会先分配到Eden区。
- 当Eden区内存不足的时候,标记Eden区和from区的存活对象。【from区第一次回收时并没有内容】
- 将存活对象采用复制算法复制到to区,复制完成后,Eden区和from区的内存都得到释放。将from区和to区互换。
- 经过一段时候后,Eden区的内容又出现不足的,同样标记Eden区和from区的存活对象,继续执行复制算法。
- 当Survivor区的对象回收次数达到阈值(默认为15),会将Survivor区中的对象晋升到老年代。【幸存区内存不足或大对象都会提前晋升】。
MinorGC、MajorGC、FullGC的区别
STW:(Stop The World)暂停所有的应用线程,等待垃圾回收的完成。
- MinorGC(YoungGC):发生在新生代的垃圾回收,当新生代无法为新生对象分配内存空间的时候,会触发MinorGC。MinorGC频率很高,会触发STW,但是回收速度很快。
- MajorGC:清理Tenured区,用于回收老年代,出现MajorGC通常会出现至少一次MinorGC。
- FullGC:针对整个新生代、老年代、元空间(Metaspace,java8以上版本perm gen)的全局范围的GC。
触发时机:
- MinorGC:
- FullGC:
-
- 老年代内存不足:如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。
- 持久代内存不足: 如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC
- YGC出现promotion failure:promotion failure发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC.
- 统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间:在发生YGC是会判断,是否安全,这里的安全指的是,当前老年代空间可以容纳YGC晋升的对象的平均大小,如果不安全,就不会执行YGC,转而执行FullGC。
- 显示调用System.GC():这里调用了 System.gc 并不一定会立马就触发FullGC
JVM的垃圾回收器有哪些
- 串行垃圾回收器:使用单线程进行垃圾回收,堆内存较小,适合个人电脑。【垃圾回收时,只有一个线程在工作,并且Java应用中所有线程都要暂停,等待垃圾回收的完成】
-
- Serial GC 作用于新生代,使用复制算法。
- Serial Old GC 作用于老年代,使用标记整理算法。
- 并行垃圾回收器:多个线程在工作,并且Java应用中所有线程都要暂停,等待垃圾回收的完成。
-
- Parallel New:作用于新生代,使用复制算法。
- Parallel Old:作用于老年代,使用标记整理算法。
- CMS(并发)垃圾回收器:【JDK8默认使用】
-
- 初始标记:仅标记GC Root及其直接关联的对象。
- 并发标记:标记其余生存对象。
- 重新标记:处理并发标记期间发生变化的对象,确保所有的活动对象都被正确标记,以便在后面的清除阶段能够正确地回收垃圾对象。
- 并发清理:清理标记的GC。
- G1垃圾回收器【JDK9默认使用】
CMS垃圾收集器的优点和弊端
优点:并发收集、低延迟。
缺点:会产生内存碎片、CMS收集器对CPU资源敏感、CMS收集器无法处理浮动垃圾
- 会产生内存碎片:,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
- CMS收集器对CPU资源非常敏感:。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
- CMS收集器无法处理浮动垃圾。可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
CMS的浮动垃圾可能产生的问题:
在并发标记阶段以及并发清理阶段可能产生一定的浮动垃圾,CMS对这部分垃圾不进行处理的,只会等下一次GC的时候处理。 这就会产生一个问题,当老年代的内存空间存放不下这些浮动垃圾时,就会导致并发失败。这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
CMS的内存碎片产生的问题:
CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
- CMS存在一个默认的参数 “-XX:+UseCMSCompactAtFullCollection”,意思是在Full GC之后再次STW,停止工作线程,整理内存空间,将存活的对象移到一边。
- 还有一个参数是“-XX:+CMSFullGCsBeforeCompaction”,表示在进行多少次Full GC之后进行内存碎片整理,默认为0,即每次Full GC之后都进行内存碎片整理。
CMS垃圾收集器的浮动垃圾
- 在并发标记阶段本来可达的对象,由于用户线程的作用变得不可达了,即产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终导致这些新产生的垃圾对象没有被及时回收。
-
- 【重新标记阶段只会利用增量更新来解决因并发标记阶段产生的对象消失问题,而浮动垃圾的产生原因是因为已经被GC线程被标记为黑色了,但用户线程接着又取消了对它的所有引用,但是黑色对象并不会被重新扫描,所以重新标记是找不到浮动垃圾的,就算能解决,并发清理阶段一样会产生浮动垃圾,而并发清理已经是最后一个阶段了,所以浮动垃圾只能下次垃圾回收的时候才能被回收】
- 在并发清理阶段也可能产生新的垃圾,也被称为浮动垃圾。
重新标记阶段主要是对并发标记阶段所有标记的非可达对象进行标记,标记出在并发标记new的新对象,在初始标记阶段和并发标记阶段没有将其标记为可达的,从而产生致命性错误。
CMS垃圾收集器:重新标记和浮动垃圾的思考_重新标记阶段为什么不能处理浮动垃圾-CSDN博客
CMS并发标记阶段是否会标记所有的对象
不会,CMS会内置记录在并发标记期间那些被新建的对象或者有变动的对象,因此重新标记阶段不需要再重新标记所有对象,只对并发标记阶段改动过的对象做标记即可。
- 新建的对象。
- 有变动的对象,这里的有变动指的仅仅是原来不可达但是由于并发标记阶段线程之间的作用使不可达变为可达,此时在重新标记阶段需要对其标记,防止GC回收造成严重错误。
-
- 为什么不对原本可到->不可达的对象进行标记,此时需要从GCroot重新开始依次查找标记,相当于返回了并发标记,不符合CMS设计理念。
JVM的指针碰撞和空闲列表
指针碰撞
适用于堆内存完整的情况,已分配的内存和空闲内存分表在不同的一侧,通过一个指针指向分界点,当需要分配内存时,把指针往空闲的一端移动与对象大小相等的距离即可,用于Serial和ParNew等不会产生内存碎片的垃圾收集器。
空闲列表
适用于堆内存不完整的情况,已分配的内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的内存块信息,当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录,最常见的使用此方案的垃圾收集器就是CMS。
详细介绍一下G1垃圾回收器
初始标记、并发标记、重新标记、混合回收。
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Top at Mark Start)指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。【snapshot At The Beginning原始快照】
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1垃圾收集器从整理上看是标记整理,局部上看是复制。
- 应用于新生代和老年代,在JDK9之后默认使用G1垃圾回收器。
- 划分为多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备。
- 采用复制算法。
- 响应时间与吞吐量兼顾。
- 分成三个阶段:新生代回收(STW)、并发标记(重新标记STW)、混合收集。
- 如果并发失败,回收速度赶不上创建新对象的速度,会触发Full GC。
流程:
- 初始时所有区域都处于空闲状态。
- 创建一些对象,挑选出一些空闲区域作为Eden区存储这些对象。
- 当Eden区需要进行垃圾回收时,挑选出一个空闲区域作为幸存者区,用复制算法复制存活对象。【需要暂停用户线程】
- 随着时间进行,Eden区的内存又不足时,将Eden区以及之前幸存者区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升到老年代。
- 当老年代占用内存超过阈值【默认是45%】后, 触发并发标记。【无需暂停用户线程】
- 并发标记之后,会有重新标记阶段解决漏标问题。【需要暂停用户线程】
- 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来优先处理垃圾多的内存块的意思。)。
- 混合回收阶段,参与复制的有Eden、Survivor、old区。
CMS和G1的对比
G1运作步骤:
1、初始标记(stop the world事件 CPU停顿只处理垃圾);
2、并发标记(与用户线程并发执行);
3、最终标记(stop the world事件 ,CPU停顿处理垃圾);
4、筛选回收(stop the world事件 根据用户期望的GC停顿时间回收)
注意:CMS 在这一步不需要stop the world
与其他GC收集器相比,G1具备如下特点:
1、并行于并发。
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集。
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合。
与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿。
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
G1垃圾收集器如何实现可预测的停顿时间模型
G1实现可预测的停顿时间模型是基于Region布局和优先级队列。
Region布局:G1收集器将Java堆内存划分为多个大小相等的独立区域,每一个独立区域称为Region,每一个Region区域都可以根据需要,扮演新生代的Eden区、Survivor区或者老年代空间,随后G1收集器能够对扮演不同角色的Region采用不同的策略去处理。Region在逻辑上分代,在物理上不分代。
优先级队列:G1跟踪各个Region的回收价值【价值:回收所获得的空间大小以及回收所需时间的经验值】,并在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证了在有限的时间内获取尽可能高的收集效率,停顿时间默认200ms,用-XX:MaxGCPauseMillis设置。
G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。
强引用、软引用、弱引用、虚引用
软引用可以用于临时缓存。
杂记
字符串常量池
字符串常量池是JVM为了提升性能和减少内存消耗针对(String类)专门开辟的一块内存区域,主要目的是为了避免字符串的重复创建。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
JDK7及之前版本中永久代位于方法区,JDK7之后永久代由元空间替代且位于本地内存中。
永久代的垃圾收集是和老年代捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。
OOM及其解决方案
产生OOM的原因:
-
- 分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
- 应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
内存泄露:申请的内存在被使用完后没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了
内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
内存泄露和内存溢出的区别:
1、内存溢出:程序在分配内存的时候没有足够大的空间了。
2、内存泄漏:程序在申请内存之后,没有办法释放掉内存,它始终占用着内存,即被分配的对象可达但无用。内存泄露一般都是因为内存中有一块很大的对象,但是无法释放。 会导致内存溢出。
JVM出现OOM的几种情况:
- Java堆内存溢出(OOM):
-
- 产生原因①内存泄露;②堆大小设置不当。
- 解决办法:对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms、-Xmx等修改。
// 创建大量对象导致堆内存溢出
public class HeapOOM {
static class OOMObject {
// 假设这里有一些属性
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject()); // 不断创建对象并添加到list中
}
}
}
-
-
- 具体解决方法:①找出溢出对象;②定位到创建对象所在的代码。
-
-
-
-
- 设置虚拟机参数-XX:+HeapDumpOnOutOfMemoryError生成堆转储(HeapDump)文件,随后使用Eclipse Memory Analyzer (MAT)分析HeapDump, 从而识别内存泄露和查看内存消耗情况。
-
-
- java.lang.OutOfMemoryError: PermGen space —— > java 永久代溢出 ,即方法区溢出了。
- 出现OOM原因:一般出现于大量Class 或者 jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。另外,过多的常量,尤其是字符串,也会导致方法区溢出。
- 解决办法:此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。
- java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。
- 出现OOM原因:JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。
- 解决办法 :可以通过虚拟机参数 -Xss 来设置栈的大小。虚拟机参数:-Xss128k,
解释:设置虚拟机栈的大小为128kn
在单线程下,无论栈帧太大还是虚拟机栈容量太小,内存无法分配的时候都会抛出以上错误。
JDK常用命令有哪些
- jps:(JVM Process Status Tool)显示指定系统内所有HotSopt虚拟机进程。
- jstack:(Java堆栈跟踪工具)生成虚拟机当前时刻的线程快照(threaddump或javacore文件)。
-
- 线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
- jmap:生成虚拟机的堆转储(heapdump)快照。
- jhat:(Java Heap Analysis Tool)虚拟机堆转储快照分析工具。
-
- Sun JDK提供了jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看,不过实事求是地说,在实际工作中,除非真的没有别的工具可用,否则一般不会去直接使用jhat命令来分析demp文件,主要原因有二:意识一般不会在部署应用程序的服务器上直接分析dump文件,即使可以这样做,也会尽量将dump文件拷贝到其他机器上进行分析,因为分析工作时一个耗时且消耗硬件资源的过程,既然都要在其他机器上进行,就没必要收到命令行工具的限制了。另外一个原因是jhat的分析功能相对来说很简陋,VisualVM以及专门分析dump文件的Eclipse Memory Analyzer、IBM HeapAnalyzer等工具,都能实现比jhat更强大更专业的分析功能。
- jstat:JVM Statistics Minitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
- jinfo:Configuration Info for Java,显示虚拟机配置信息
参考链接
这篇文章是自己学习过程中记录的笔记,大部分参考黑马面试视频,也有其他人的博客没有学习时候没有记录链接,非常抱歉。