当前位置:首页>编程知识库>后端开发知识>Java的Jvm gc
Java的Jvm gc
阅读 3
2022-12-13

1、JVM的内存结构


方法区和堆是所有线程共享的内存区域;
java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
堆内存 是jvm里最大的区域,是线程共享的,存储的是对象实例。
方法区 方法区也是线程共享的,主要存储的是jvm加载的类信息,常量、静态变量、即时编译器编译后的代码等数据
程序计数器 是一块较小的内存空间,它的作用是标记当前线程执行到什么位置。
栈内存 它也是每个线程独立一个区域,生命周期和线程相同,主要存储线程执行的方法的局部变量,方法出口等信息。

2、Java8内存模型区别

Java7及以前版本的Hotspot中方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。
Java8永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。

2.1 堆栈模型

数据分为基本数据类型(String, Number, Boolean, Null, UndefinedSymbol)和对象数据类型。
基本数据类型的特点:直接存储在栈(stack)中的数据
引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

2.2 内存中的栈(stack)、堆(heap)和方法区(method area)的用法

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;
通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为EdenSurvivor(又可分为From SurvivorTo Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;
程序中的字面量(literal)如直接书写的10、”siddim”和常量都是放在常量池中,常量池是方法区的一部分。
栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError
String str = new String("siddim.com");
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而”siddim.com”这个字面量是放在方法区的。
较新版本的Java(从Java 6的某个更新开始)中,由于JIT编译器的发展和”逃逸分析”技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。
运行时常量池相当于Class文件常量池具有动态性,Java语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String类的intern()方法就是这样的。 看看下面代码的执行结果是什么并且比较一下Java 7以前和以后的运行结果是否一致。
String s1 = new StringBuilder("sidd").append("im").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("progr").append("am").toString();
System.out.println(s2.intern() == s2);

2.4 对象分配规则

2.4.1 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
2.4.2 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值对象进入老年区。
2.4.3 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
2.4.4 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
S1、程序初始化,新生代的三个空间均为空
S2Eden被分配的新对象占满,触发第一次Minor GCEden中存活对象被复制到Survivor1中,剩余对象被回收(回收后,Eden为空,Survivor1无碎片地存放所有存活对象,Survivor2为空)
S3Eden再次被新对象占满,触发第二次Minor GC,此时EdenSurvivor1中的存活对象被复制到Survivor2中,剩余对象被回收(回收后,Eden为空,Survivor1为空,Survivor2无碎片地存放所有存活对象)
S4、如此交替,在执行一定次数的Minor GC后,会通过Full GCsurvivor中的存活对象移入老年代。

3、JVM调优终结

如果CPU使用率较高,GC频繁且GC时间长,可能就需要JVM调优了。 基本思路就是让每一次GC都回收尽可能多的对象,对于CMS来说,要合理设置年轻代和年老代的大小。
这是一个迭代的过程,可以先采用JVM的默认值,然后通过压测分析GC日志。
如果看年轻代的内存使用率处在高位,导致频繁的Minor GC,而频繁GC的效率又不高,说明对象没那么快能被回收,这时年轻代可以适当调大一点。 如果看年老代的内存使用率处在高位,导致频繁的Full GC,这样分两种情况:如果每次Full GC后年老代的内存占用率没有下来,可以怀疑是内存泄漏;如果Full GC后年老代的内存占用率下来了,说明不是内存泄漏,要考虑调大年老代。 对于G1收集器来说,可以适当调大Java堆,因为G1收集器采用了局部区域收集策略,单次垃圾收集的时间可控,可以管理较大的Java堆。

4、类的生命周期

包括这几个部分,加载、连接、初始化、使用和卸载
4.1 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
4.2 连接,连接又包含三块内容:验证、准备、解析。
4.2.1)验证,文件格式、元数据、字节码、符号引用验证;
4.2.2)准备,为类的静态变量分配内存,并将其初始化为默认值;
4.2.3)解析,把类中的符号引用转换为直接引用
4.3 初始化,为类的静态变量赋予正确的初始值
4.4 使用,new出对象程序中使用
4.5 卸载,执行垃圾回收

5、 判断一个对象应该被回收

重写finalize方法 如果被执行 代表已经被回收

6、垃圾收集算法

GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,
我们常用的垃圾回收器一般都采用分代收集算法(新生代老生代)。

7、查看jvm内存

7.1 jps 获取java进程的pid
7.2 jmap - heap PID 查看推内存情况 会生成一个堆内存快照
7.3 jstat -gcutil PID 监控各个区域内存占用比例
jstat -gcutil pid
统计gc信息统计。如下图


 jstat -gc pid
可以显示gc的信息,查看gc的次数,及时间。其中最后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。


 jstat -gccapacity pid

可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小,如:PGCMN显示的是最小perm的内存使用量,PGCMX显示的是perm的内存最大使用量,PGC是当前新生成的perm内存占用量,PC是但前perm内存占用量。
其他的可以根据这个类推, OC是old内纯的占用量。


jstat -gcnew pid
年轻代对象的信息。


jstat -gcnewcapacity pid
年轻代对象的信息及其占用量。


jstat -gcold pid
old代对象的信息。


jstat -gcoldcapacity pid
old代对象的信息及其占用量。


8、设置jvm大小

新生代和老年代 通常12
新生代分为EdenS0S1 比例是811
参数 意义
-Xms 初始堆大小
-Xmx 最大堆空间
-Xmn 设置新生代大小
-XX:SurvivorRatio 设置新生代eden空间和from/to空间的比例关系
-XX:PermSize 方法区初始大小
-XX:MaxPermSize 方法区最大大小
-XX:MetaspaceSize 元空间GC阈值(JDK1.8)
-XX:MaxMetaspaceSize 最大元空间大小(JDK1.8)
-Xss 栈大小
-XX:MaxDirectMemorySize 直接内存大小,默认为最大堆空间

9、JAVA 垃圾回收器的特点

9.1 Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
9.2 ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
9.3 Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
9.4 Parallel Old 收集器,Parallel OldParallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
9.5 CMS收集器,CMSConcurrent Mark Sweep)高并发执行,收集器是一种以获取最短回收停顿时间为目标的收集器。
缺点:CMS收集器对CPU资源非常敏感,CMS默认启动对回收线程数(CPU数量 3)/4, 当CPU数量在4个以上时,并发回收时垃圾收集线程不少于25%,并随着CPU数量的增加而下降,但当CPU数量不足4个时,对用户影响较大。 适用场景:重视服务器响应速度,要求系统停顿时间最短。可以使用参数- XX: UserConMarkSweepGC来选择CMS作为老年代回收器。 G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。 部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。 G1运作步骤: 1、初始标记;2、并发标记;3、最终标记;4、筛选回收

10、CMS和G1区别

CMS 并发标记清除。 主要步骤是 初始收集-并发标记-重新标记-并发清除-重置
G1 主要步骤: 初始标记-并发标记-重新标记-复制清除
CMS的缺点是对CPU的要求比较高。
G1的缺点是将内存化成了多块,所以对内存段的大小有很大的要求。
CMS是清除,所以会有很多的内存碎片。
G1是整理,所以碎片空间较小
G1CMS都是响应优先,他们的目的都是尽量控制 stop the world 的时间。
G1CMSFull GC都是单线程 mark sweep compact算法,直到JDK10才优化成并行的。

10、CMS GC是内存碎片处理方法

我们知道,CMSGC在老生代回收时产生的内存碎片会导致老生代的利用率变低;或者可能在老生代总内存大小足够的情况下,却不能容纳新生代的晋升行为(由于没有连续的内存空间可用),导致触发FullGC。针对这个问题,Sun官方给出了以下的四种解决方法:
10.1 增大Xmx或者减少Xmn
10.2 在应用访问量最低的时候,在程序中主动调用System.gc(),比如每天凌晨。
10.3 在应用启动并完成所有初始化工作后,主动调用System.gc(),它可以将初始化的数据压缩到一个单独的chunk中,以腾出更多的连续内存空间给新生代晋升使用。
10.4 降低-XX:CMSInitiatingOccupancyFraction参数以提早执行CMSGC动作,虽然CMSGC不会进行内存碎片的压缩整理,但它会合并老生代中相邻的free空间。这样就可以容纳更多的新生代晋升行为。

11、双亲委派模型、优势及如何破坏双亲委派模型

一个类是由加载它的类加载器和这个类本身来共同确定其在Java虚拟机中的唯一性。

类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载

12、双亲委派模型的优势

12.1 避免类的重复加载

每次进行类加载时,都尽可能由顶层的加载器进行加载,保证父类加载器已经加载过的类,不会被子类再加载一次,同一个类都由同一个类加载器进行加载,避免了类的重复加载。

12.2 防止系统类被恶意修改

通过双亲委派模型机制,能保证系统类由系统类加载器进行加载(Bootstrap ClassLoader)后,用户即使定义了与系统类相同的类,也不会进行加载,保证了安全性。

13、如何破坏双亲委派模型

某些情况下,需要由子类加载器去加载class文件,这时就需要破坏双亲委派模型。要破坏双亲委派模型,可以通过重写ClassLoader类的loadClass()方法实现。
注:破坏双亲委派模型过程中的坑也不少!典型的打破双亲委派模型的例子有TomcatOSGI.

13.1 JDBC是如何破坏双亲委派模型

为什么JDBC需要破坏双亲委派模式,原因是原生的JDBCDriver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQLmysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。
破坏方式:
可以使用线程上下文类加载器实现
ClassLoader是启动类加载器,但是这个启动类加载并不能识别rt.jar之外的类,这个时候就把callerCL赋值为Thread.currentThread().getContextClassLoader();也就是应用程序启动类。

14 类的实例化顺序

1. 父类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
2. 子类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
3. 父类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
4. 父类构造方法
5. 子类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
6. 子类构造方法

15、四种引用

1)强引用
如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。比如String str = "hello"这时候str就是一个强引用。
2)软引用
内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
比如全局缓存 为了访问内存泄漏,可以使用软引用
SoftReference<M2Object> s = new SoftReference<ReferencesObjs.M2Object>(o);

3)弱引用
如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。
WeakReference<M2Object> m = new WeakReference<ReferencesObjs.M2Object>(o);
4)虚引用
如果一个对象具有虚引用,就相当于没有引用,在任何时候都有可能被回收。使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。

GCReferenceReferenceQueue的交互


AGC无法删除存在强引用的对象的内存。


BGC发现一个只有软引用的对象内存,那么:
SoftReference对象的referent 域被设置为null,从而使该对象不再引用heap对象。
SoftReference引用过的heap对象被声明为finalizable
③ 当 heap 对象的 finalize() 方法被运行而且该对象占用的内存被释放,SoftReference 对象就被添加到它的 ReferenceQueue(如果后者存在的话)。


CGC发现一个只有弱引用的对象内存,那么:
WeakReference对象的referent域被设置为null,从而使该对象不再引用heap对象。
WeakReference引用过的heap对象被声明为finalizable
③ 当heap对象的finalize()方法被运行而且该对象占用的内存被释放时,WeakReference对象就被添加到它的ReferenceQueue(如果后者存在的话)。


DGC发现一个只有虚引用的对象内存,那么:
PhantomReference引用过的heap对象被声明为finalizable
PhantomReference在堆对象被释放之前就被添加到它的ReferenceQueue

16、软弱引用使用场景

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用
软引用解决图片缓存的问题
弱应用解决HashMap中的oom问题

17.对象互相引用

17.1、引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1
当引用失效时,计数器值就减1
任何时刻计数器为0的对象就是不可能再被使用的。
但是主流的java虚拟机没有采用引用计数算法,其中最主要的原因就是它很难解决对象之间互相循环引用的问题。

17.2 可达性分析算法(主流算法)

主流的实现中,都是通过可达性分析来判定对象是否存活。
算法的基本思想:通过一系列的被称为“gc roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到“gc roots”没有任何引用链相连时,则证明此对象是不可用的。
可以作为“gc roots”的对象
1)虚拟机栈(栈针中的局部变量表)中引用的对象
2)方法区中类静态属性引用的对象。
3)方法区中常量引用的对象
4)本地方法栈中JNI引用的对象。

18 反射创建对象几种写法

//方式一
Class class1 = Class.forName("User");
User user1= (User) class1.newInstance();
System.out.println(user1);

//方式二
Constructor constructor = class1.getConstructor();
User user2 = (User) constructor.newInstance();
System.out.println(user2);
上一篇: 设计模式
下一篇:数据库
评论 (0)