面试必掌握之JVM

高频面经汇总:https://blog.csdn.net/qq_40262372/article/details/116075528

四、JAVA 虚拟机(JVM)

4.1 介绍下 JAVA 内存区域(运行时数据区)

Java 虚拟机在执行Java 程序的过程中会把它管理的内存分成若干个不同的数据区域。JDK1.8 和之前的版本略有不同,下面会介绍到。

JDK1.8之前:

JDK1.8:

线程私有的:

  1. 虚拟机栈
  2. 本地方法栈
  3. 程序计数器

线程共有的:

  1. 方法区
  2. 直接内存(非运行时数据区的一部分)

程序计数器

程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码(.class)的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

总结来两点说:

  1. 字节码解释器通过改变程序计数器的大小来依次读取指令
  2. 在当前线程中,保存程序指令执行的位置,方便再次切回线程后继续执行。

注意:程序计数器是唯一一个不会出现 OOM 错误的内存区域,它随着线程的消亡而消亡, 随着线程的创建而创建。

JAVA 虚拟栈

与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈, 或者说是虚拟机栈中局部变量表部分。(实际上,Java 虚拟机栈是由一个一个栈帧组成,每个栈帧都有方法索引、输入输出参数、本地变量(局部变量)、引用、父帧、子帧),调用函数的所有需要的东西。

Java 虚 拟 机 会 出 现 两 种 异 常 :StackOverFlowError 和 OutOfMemoryError

StackOverFlowError:

如果 java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机的最大深度的时候,就会抛出栈溢出。

OutOfMemoryError:

如果 java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。

Java 虚拟机栈也是线程私有的,每一个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

如何调用方法/函数?

每一次函数调用都会有一个对应的栈帧压入 java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

Java 方法有两种返回方式:  

     1. return 方式

     2. 抛出异常

不管哪种返回方式都会导致栈帧被弹出。

本地方法栈

本地方法栈所发挥的作用非常相似,区别是:虚拟机栈是为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,里面包含一个方法所需要的东西(输入输出参数,方法引用,局部变量,父帧,子帧)

出现的错误也跟虚拟机栈一样。

我们在普通类中,写入了方法都需要写方法体的。但是加入了 Native 关键字就不用写方法体

因为带了 native 关键字的说明 java 的作用范围达不到了,他会去调用 c 语言的库会进入本地方法栈,调用本地方法接口 JNI

JNI 作用:拓展 JAVA 的使用,拓展其他的语言接口为 java 所用,比如:c/c++

为什么要有这个呢?这就要谈及历史了。当 Java 才出世的时候,C/C++横行程序界,所以为了抱大腿,所以 java 专门开一个栈去配合 C++演出。

在最终的执行中,都是本地方法栈去执行通过 JNI 接口。

堆是线程共享的的一片最大区域。类加载器读取了类文件后,一般会把上面东西放到堆中。    类,方法,常量,变量。

Java 世界中的对象几乎都在堆中创建与回收,这样的操作是对系统的开销非常大。所以JIT 编译和逃逸分析就出来。

JIT 编译:叫“即时编译”,某段代码第一次被执行时进行编译。说到编译器就要与解释器一起比较了。

当 java 文件成了字节码了,然后 jvm 里面一般是有解释器和编译器。

我们可以把解释器想做一个黑盒子,进入一堆代码,返回我们需要的结果。我们每次来了    程序调用解释器,解释器运行后返回结果,很有可能调用的相同的代码(比如循环),所以我     们编译器就起作用了,将热点程序编译成本地代码,我们就直接执行本地代码,就不用一条    条解释了。

本地代码又是什么呢?不同操作系统的机器码是不一样的。之所以 JAVA 能有那个响亮的口号”一次编译,到处运行”,是因为 JVM 里面的解释器。解释器将源代码解释成当前系统的机器码,不同版本的 JVM 对应着不同的系统。所以只要系统中下载了 JVM,其他系统的 java 随便运行。但是每条解释也没有编译好的代码快啊。所以就有了 jit 编译。

那么问题来了。到底选用解释器还是 jit 编译器。那就看是否为热点程序,如果只有一条那肯定是解释器快了。

判断是否热点程序的两个方法

  1.  基于采样的热点探测:周期检查每个线程的栈顶方法,经常出现的方法,就是“热点方法”
  2.  基于计数器的热点探测:为每个方法建立计数器,统计执行方法的次数,超过一定的次数就认为它是”热点方法” 。包含两个计数器:(1)方法调用计数器;(2)回边计数器。

逃逸分析:当一个对象在方法中被定义后,它可能被外部方法引用,例如作为调用传入传入参数传递到其他方法中,成为方法逃逸;有些可能被其他线程访问到,譬如赋值给类变量    或者在其他线程中访问的实例变量,称为线程逃逸。

逃逸分析能分析出一个对象会不会逃逸到方法或线程之外,如果不会发生逃逸,那么就会进行一些高效的优化:

  1. 栈上分配(针对方法逃逸):将不会逃逸的对象分配到栈上,对象随着方法的结束而结束,减少 GC 系统的压力
  2. 同步消除(针对线程逃逸):如果其他线程不访问该对象,那么我们可以把同步措施取消掉
  3. 标量替换:将对象的一个聚合产物分解成多个基本成员变量。这个方法不是创建对象了,直接创建对象里面的成员变量到栈上分配与读写。

Java 堆是 GC 系统的主要区域,现代收集器都是采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细分:Eden、From Survivor、To Survivor 空间。进一步的划分目的是为了更好地回收内存,或者更快分配内存。

JDK7 以及之前,堆内存通常被分为下面三个部分:

  1.  新生代内存
  2.  老生代
  3.  永生代

JDK8 版本之后方法区(HotSpot 的永久代)被彻底移除了,取而代之是元空间,元空间使用的是直接内存。因为(1)永久代被 JVM 设定了一个大小限制,而元空间直接使用的直接内存受本机可用内存的限制,但是内存溢出的可能性大大减少。(2)JDK8,合并 HotSpot 和 JRockit 的代码时,JRockit 没有永久代,合并后也没有必要开一个额外的设置。

大部分的情况,对象都会现在 Eden 区域分配,然后进行了一次新生代垃圾回收,如果对象还活着,就进入进入幸存区 0 或 1,并且年龄还会+1,当它的年龄增到一定程度(默认15 岁),就会到老年代。对象晋升到老年代的年龄阈值:可以通过  -XX:MaxTenuringThreshold来设置。

其中还有一个动态对象年龄判断,如果幸存区中相同年龄的对象所有大小之和超过了幸存区的空间一半,那么大于等于该年龄的对象直接进入老年区,无需等到默认年龄。

堆中的最容易出现的是 OOM,但是还是有多种形式:

方法区

方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区域。

静态变量(static)、常量(final)、类信息(构造方法、接口定义、Class 加载模板)、运行时的常量池存在方法区中,但是存在堆内存中的实例变量和方法区无关

调用方法的过程:

  1. 先将类模板加载,
  2. 将默认的成员加载到常量池,
  3. 调用方法
  4. 去堆进行赋值操作,如果没有赋值,从常量池取

运行时常量池

运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息,还有常量池表(用于存放编译期生成的各种字面值符号引用(检查对象是否引用))因为常量池是方法区的一部分,自然会收到方法区内存的限制,所以当常量池无法再申请到内存时抛出 OutOfMemoryError 错误

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误。

JDK1.4 中新加入的 NIO(New Input/Output)类,引入了一种基于通道缓存区的 I/O 方式, 它可以直接使用 Native 函数库直接分配堆外内存, 然后通过一个存储在 Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能, 因为避免了在 Java 和 Native 之间的来回复制数据,Java 堆直接通过一个引用快速解决操作。

4.2 说一下 Java 对象创建的过程

4.2.1 STEP1:类加载检查

虚拟机遇到一条 new 指令的时候,首先去常量池中检查该对象的符号引用,并检查该引用是否被加载过、初始化过、解析过。如果没有,就要去执行类加载过程。

4.2.2 STEP2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定。分配方式有两种:”指针碰撞”和“空闲列表”两种,选择那种分配方式由Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集是否带有压缩整理功能所决定。

指针碰撞:

适用场合:堆内存规整(没有内存碎片)的情况(复制算法,标记压缩算法)

原理:用过的内存全部整合到一边,其中用一个指针来分隔,来了一个新对象,指针往没有用过内存的地方移动。

GC 收集器:serial(标记压缩),parallel(serial 的多线程版本)

 

空闲列表

使用场合:堆内存不规整,有内存碎片(标记清楚算法)

原理:虚拟机会维护一个列表,该列表中会记录那些内存块是可用的,在分配的是偶,找一块足够大的内存块来创建对象实例,然后更新列表。

内存分配并发问题

在创建对象中,我们肯定不能允许另外的线程来干扰,就比如你女票被男的骚扰了,你爽吗?所以我们虚拟机在创建对象的时候要保证线程安全。通常也有两种方式来保证创建对象    是线程安全的:

CAS+失败重试:

CAS 是乐观锁的一种实现。乐观锁是指,它每次都假设没有其他线程来干扰的,如果有线程干扰,那就重新创建,直到创建成功。这样可以保证更新操作的原子性。

TLAB:

为每一个线程预先在 Eden 区域分配一块内存,首先 TLAB 分配,对象的需要的内存大于了 TLAB 提供的,再采用 CAS 进行内存分配。

4.2.3 STEP3:初始化零值

当内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这    一步操作保证了对象的实例字段在 Java 代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型所对应的零值。就跟有些成员变量你赋值了,有些没有赋值,那么那    些没有赋值的就是 Null 的道理是一样的。

4.2.4 STEP4:设置对象头

初始化完成后,我们需要一个东西去辨认我们这个新创建对象的一些信息。很多事物的基本信息都存在什么头,比如 http,它的大概属性都会存在信息头中,比如请求方式之类的。当然我们这个新创建对象也是一样的,我们就用对象头来存储对象是那个类的实例、类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。

4.2.5 STEP5:执行 init 方法

经过上面 4 步操作后,我们从虚拟机的角度来看,一个新的对象已经产生了,但从 Java 程序中,对象创建好了,我们都一般还有构造函数去初始化值,所以<init>方法就起作用    了,把对象按照程序员的意愿来进行初始化,这样 5 步才算把一个真正可用的对象完全产生出来。

4.3 对象的访问定位有哪两种方式?

我之前讲了创建对象,现在已经有对象了,那我们怎么去访问与使用呢?我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:

句柄:

如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

直接指针访问方式:

Reference 中直接存的就是 Java 堆中的对象实例数据,然后堆中空间里有指向对象类型数据的地址。

对比:

使用句柄的好处是 reference 中存储的是稳定的句柄地址,在对象被移动时指挥改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,节省了一次指针定位的时间开销。Hotspot 默认的时直接指针。

4.4 JVM 内存分配和回收(几个代之间的 GC 处理)

ava 相对于 C/C++主要区别之一是:Java 是有 JVM 自动管理对象的内存分配和回收。JVM 内存分配是在堆内存上面进行分配,堆内存主要分为以下几个部分:新生代和

老年代(元空间属于非堆);再细分:Eden、From Survivor、To Survivor 空间。进一步的划分目的是为了更好地回收内存,或者更快分配内存。

大部分的情况,对象都会现在 Eden 区域分配,然后进行了一次新生代垃圾回收,如果对象还活着,就进入进入幸存区 0 或 1,并且年龄还会+1,当它的年龄增到一定程度(默认15 岁),就会到老年代。对象晋升到老年代的年龄阈值:可以通过  -XX:MaxTenuringThreshold来设置。

其中还有一个动态对象年龄判断,如果幸存区中相同年龄的对象所有大小之和超过了幸存区的空间一半,那么大于等于该年龄的对象直接进入老年区,无需等到默认年龄。

回收的时候分为轻 GC 和重 GC 是指,当我的伊甸园区的内存了,会触发一次轻 GC, 将能够活下来的对象给幸存区,此时伊甸园区内存就清空了,继续运行,直到新生区(伊甸园+幸存区)的内存满了就进行重 GC,将新生区清空,进入养老区。如果新生和养老区都满了,就 OOM 了。

Minor GC 具体流程:

Eden 区可以存活的对象给 to 区,from 区也给 to 区;然后先对象集中在左图的 to 区,因为谁空谁是 to,所以以前的 from 区变为 to 区,下次一起又进行 Minor GC 的时候,就往右图的 to 放对象了。这样幸存区中是没有内存碎片的。

如果其中 From+Eden 的对象大于了 To 区那么会进行 Minor GC,有超过合格年龄的会进入老年区;Major GC 会在老年代满的时候进行清理;Full GC 会清理新生区和老年区: 1.System.gc;2.伊甸园区进入存活区放不下,去老年区也放不下;3.gc 线程与用户线程同时执行,那么用户线程依旧可能同时产生垃圾,如果垃圾较多无法放入预留空间;4.新生代的晋升平均大小是大于老年代的剩余空间。

4.5 堆内存中对象的分配的基本策略

 4.5.1 对象优先在eden 区分配

目前主流的垃圾收集器都是采用分代回收算法,因此需要将堆内存分为新生代和老年代,    这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

大多数情况下,对象在新生代中 Eden 区分配。当 Egen 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.

4.5.2 大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组) 如果对象比较大,那么复制算法效率就比较低下。

4.5.3 长期存活的对象将进入老年代

大部分的情况,对象都会现在 Eden 区域分配,然后进行了一次新生代垃圾回收,如果对象还活着,就进入进入幸存区 0 或 1,并且年龄还会+1,当它的年龄增到一定程度(默认15 岁),就会到老年代。对象晋升到老年代的年龄阈值:可以通过  -XX:MaxTenuringThreshold来设置。

其中还有一个动态对象年龄判断,如果幸存区中相同年龄的对象所有大小之和超过了幸存区的空间一半,那么大于等于该年龄的对象直接进入老年区,无需等到默认年龄。

4.6 如何判断对象是否死亡?(两种方法)

我们进行 GC 收集的时候,不能随便收集啊。所以要判断这个对象是符合”垃圾”对象,才会去回收。

4.6.1 引用计数法

给对象添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1。举个例子!(引用他人:https://www.cnblogs.com/igoodful/p/8727241.html)

STEP1:obj1 引用 GcObject 实例 1,实例 1 的引用次数+1,实例 1 的引用次数=1;

STEP2:obj2 引用 GcObject 实例 2,实例 2 的引用次数+1,实例 2 的引用次数=1;

STEP3:GcObject 实例 2 的引用次数+1,实例 2 的引用次数=2;

STEP4:GcObject 实例 1 的引用次数+1,实例 1 的引用次数=2; 然后执行 STEP5,STEP6,内存如下图:

STEP5:obj1 不再引用 GcObject 实例 1,实例 1 的引用次数-1,实例 1 的引用次数=1;

STEP6:obj2 不再引用 GcObject 实例 2,实例 2 的引用次数-1,实例 2 的引用次数=1;

此时 GC 实例 1 和 GC 实例 2 的次数都不为 0,所以这两个实例所占的内存得不到释放,这便产生了内存泄漏。

4.6.2 可达性算法

主流的虚拟机都是采用 GC Roots Trancing 算法,比如 Sun 的 Hotspot 虚拟机便是采用该算法。该算法的核心是从 GC Roots 对象作为起始点,利用数学中图论只是,途中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。其中涉及到两个重要的概念,GC Roots ,一是可达性。

GC Roots 的对象(三中其 1):

  1. 虚拟机的栈帧的局部变量表所引用的对象;
  2. 本地方法栈的 JNI 所引用的对象;
  3. 方法区的静态变量和常量所引用的对象;

从上图可以看出,1,2,4,6 都可以达到 GC roots,也就是存活对象,不能被 GC 回收。但是对于 3 和 5,他们就涉及不到 GC roots,这就是不可达对象,需要被回收。

总结来说,对于对象之间循环引用的情况,引用计数算法,则 GC 无法回收这两个对象, 但是可达性算法则可以正确回收。

4.7 简单介绍一下强引用,软引用,弱引用,虚引用

无论是通过引用计数法判断引用数量,还是通过可达性分析判断对象的引用是否可大,判    断对象的存活都与”引用”有关。

4.7.1 强引用

如果一个对象具有强引用,对于我们来说是不能缺少的对象垃圾回收器绝不会回收它。

当内存空间不足,JVM 宁可抛出异常也不会回收它。例子:list 集合里的数据不会释放,即使内存不足也不会。

4.7.2 软引用

软引用的对象是可有可无的对象。如果内存空间足够,垃圾回收器就不会回收它,如果内    存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用与一个引用队列联合使用,如果软引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入与之关联的引用队列中。当内存足够大时可以把数组存入软引用,取数据就可以从内存里取数据,提交效率。例子:浏览器的后退按钮

4.7.3 弱引用

弱引用的对象也是可有可无的对象。弱引用于软引用的区别在于:垃圾回收器在扫描他所    管辖的内存区域,一旦发现弱引用的对象,不管内存是否足够,都会回收它的内存。不过垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用与一个引用队列联合使用,如果软引用的对象被垃圾回收,JAVA 虚拟机就会把这个弱引用加入与之关联的引用队列中。

当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这个时候就是弱引用。

4.7.4 虚引用

虚引用并不会决定对象的生命周期。他跟没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动。 虚引用必须和引用队列联合使用。

4.7.5  总 结

 4.8  何判断一个常量是废弃常量?

运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢? 假如在常量池中存在字符串”abc”,如果当前没有任何 String 对象引用该字符串常量的话,

就说明常量”abc”就是废弃常量,如果此时发生内存回收的话,”abc”就会被系统清理出常量池。

4.9 如何判断一个类是无用的类?

方法区主要回收的是无用的类,那么如何判断一个类是无用的呢?

如果要判断是无用的类要同时满足以下三个条件:

1.该类的所有实例都被回收,也就是说 Java 堆中没有该类的任何对象2.加载该类的 ClassLoader 已经被回收

3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

4.10 垃圾收集有那些算法,各自的特点?

4.10.1 标记-清除算法(两个问题)

见文知意,这个算法肯定有“标记”与“清除”两个阶段。

第一步:扫描,标记出不需要清除的对象

第二步:对没有标记的对象,进行清除

  1. 效率问题(扫描了两次,浪费时间)
  2. 空间问题(产生了大量的内存碎片)

4.10.2 复制算法(解决效率问题)

复制算法为了解决效率问题。它将内存分为大小一样的两块,每次使用其中的一块,当一块使用完后,将存活的复制到另外一块去,再把使用的空间一次清掉。在 JVM 应用中是新生代,谁空谁 to。

4.10.3 记-压缩算法(解决内存碎片)

比标记-清除算法多了一步:压缩,将零散的对象都压缩在一起。

4.10.4 分代收集算法

当前的虚拟机采用的是分代收集算法。因为我们将 JVM 中堆内存分为了新生区和老年区。这两个区的对象性质不一样,所以适合的算法也不一样。就跟这句话一样只有适合自己的才是最好的。

新生代有大量的对象死去,所以我们选择复制算法,因为我们需要复制的成本就很低,只需要把少量的存活对象复制下来即可;老年代的对象存活率比较高,而且没有额外的空间对他进行分配担保,所以我们是选择标记-清除或者标记-压缩算法。

4.11 为什么要分新生区和老年区?

为了提升 GC 效率,因为不同区使用不同的 GC 算法。

4.12 常见的垃圾收集器有那些?

4.13 JVM 调优

JVM 调优的目标是:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。重要指标:

  • 内存占用:程序正常运用需要的内存大小
  • 延迟:由于垃圾收集而引起的程序停顿时间
  • 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值

当然这个跟分布式系统中的 CAP 原则一样,不能同时达到,所以我们应用的场景不同, 那么调优所考虑的方向也不同。

调优经验:

JVM 配置方面,可以先使用默认配合,堆初始是内存的 1/16,最大堆内存是内存的 1/4。然后在测试中根据系统的运行情况,结合 GC 日志、内存监控、使用的垃收集器等机型合理的调整,比如:当老年代内存过小时可能引起频繁的 Full GC,当内存过大时 Full GC 时间又会特别长。

那么 JVM 的配置比如新生代老年代应该配置多少才合适呢?答案肯定是不一致的。因为物理内存一定的条件下,新生代设置越大,老年代就越小,Full GC 频率越高,但是 Full GC 时间越短;相反新生代设置越小,老年代就越大,Full GC 频率就越低,但每次 Full GC 消耗的时间越大。所以要找到自己场景适合的合适点。

调优建议:

  1. -Xms 和-Xmx 的值设置一样,为了避免内存的动态调整,因为当空闲堆内存不同的时候, 会切换-Xms 和-Xmx 内存状态,如果设置一样的话,就不会进行动态内存调整,节约资源。
  2. 新生代尽量设置大一些,让对象在新生代多存活一段时间,每次 Minor GC 进可能多收集垃圾对象,防止进入老年代,进行 Full GC.
  3. 老年代如果使用 CMS 收集器,新生代可以不用太大,因为 CMS 的并发收集速度也很快, 收集过程可以与用户线程并发执行。

避免以下问题(避免不需要的对象进入老年代)

  1. 避免创建过大的对象或者数组:过大的对象和数组在新生代没有足够的空间会进入,老年代,会提前出发 FullGC
  2. 避免同时加载大量数据:从数据库或者 Excel 取大量数据,尽量分批读取
  3. 当程序中有对象引用,如果使用完后,尽量设置为 null,比如 obj1=null。避免这些对象进入老年代
  4. 避免长时间等待外部资源,缩小对象的生命周期,避免进入老年代。

JAVA基础高频面试题:

https://blog.csdn.net/qq_40262372/article/details/112556249

 

B站视频讲解JVM:

https://www.bilibili.com/video/BV1xf4y1r7J9/

https://www.bilibili.com/video/BV15z4y1U7pu/

B站视频讲解如何三个月学习JAVA拿到实习Offer:

https://www.bilibili.com/video/BV1dV411t71K

如果想要在学习的道路上和更多的小伙伴们交流讨论

请加Q群:725936761   

欢迎每一位小伙伴的加入

我们一起朝着目标加油!冲锋陷阵!

想要了解更多请关注微信公众号:万小猿  

回复“JVM”即可获取原文PDF文件

                 

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万小猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值