jvm gc(垃圾回收机制)
Java JVM 垃圾回收(GC 在什么时候,对什么东西,做了什么事情)
- 前言:(先大概了解一下整个过程)
- 作者:知乎用户
链接:https://www.zhihu.com/question/27339390/answer/36511809
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
java堆(JavaHeap)
<img src="https://pic1.zhimg.com/50/f7541e5d33d1d8b412dd0556c7e4b10d_hd.jpg" data-rawwidth="481" data-rawheight="713" class="origin_image zh-lightbox-thumb" width="481" data-original="https://pic1.zhimg.com/f7541e5d33d1d8b412dd0556c7e4b10d_r.jpg">图来了我就不用多说了。每个栈帧其实可以理解为一个方法,我是这么理解的,之间的关系就是调用。
1.用来存放对象的,几乎所有对象都放在这里,被线程共享的,或者说是被栈共享的
2.堆又可以分为新生代和老年代,实际还有一个区域叫永久代,但是jdk1.7已经去永久代了,所以可以当作没有,永久代是当jvm启动时就存放的JDK自身的类和接口数据,关闭则释放。
新生代可以分为Eden区和两个幸存区,这么设计是为了更好地利用内存 之前的设计是只分为两部分一样一半 后来发现这样只利用到了一半的内存 才改为按比例分成三个区的,使用的是复制回收算法,两个幸存区是较小的区域。逻辑是每次使用Eden区和其中一个幸存区,回收时将其还存活着的对象一次性的复制到另一个幸存区中,最后清理到刚才使用的Eden和其中一个幸存区。
美团的面试官也问了这个问题,他也说了他的理解,我感觉可能是不准确的。
新建对象就在Eden区,Eden就是伊甸,顾名思义。但是并不是对象最活跃的区域,对象最活跃的区域是老年代,因为经过各种垃圾回收之后对象都跑到这里来了。
3.内存溢出
内存溢出其实没什么好讲的,满了就会溢出。怎么才能满呢,不断创建对象,那问题又来了,创建多了被回收怎么办,好办,将新建的对象存到list里去,就不会回收了,为什么呢,因为jvm判定一个对象的死活就是根据对象是不是被引用。
此外堆跟随jvm的,有jvm就有堆。堆也是垃圾回收的主要区域,又叫GC堆,垃圾堆,玩笑。
jvm栈
1.要说栈是用来存什么的,其实我感觉不严谨,栈是运行时创建的,是跟随线程的,它不是用来存什么的,那它用来干什么的,它是用来存栈帧的,没有图不太好说呢,等下我去截个图。
2.栈的好处就是不需要垃圾回收,随着线程结束内存就释放。
3.但是并不是说就不会内存溢出,那么栈的内存溢出是怎么产生的呢,肯定也是满了,这个满了怎么理解呢,一是要申请的不够了,二是jvm内存太小,这是个有趣的问题。但是产生的错误却是不一样的,如果创建一个void方法调用自身,错误是stackoverflowError,如果不断创建线程则会outOfMemoryError。这里就有一个比较高级的问题了,对于第二种多线程内存溢出该怎么解决呢,深入理解jvm一书中给出的解决方案是这样的,通过减小最大堆和栈容量来换取更多的线程。
方法区和运行时常量池
1.方法区是堆的一个逻辑区域,但是又叫非堆。运行时常量池又是方法区的一部分,真正的一部分。方法区并不是存方法的,存方法的应该是栈或者栈帧。方法区存的是类信息、常量、静态变量等,也是被线程共享的区域。运行时常量池存放的是编译期生产的各种字面量和符号引用。
2.这块内存区域的回收没啥好说的,因为我也不太清楚,我只知道HotSpot的设计团队选择把GC分代扩展至方法区了,或者是使用永久代实现方法区。
3.内存是肯定会溢出的,不断创建类会导致方法区内存溢出,而不断将常量放入常量池(String.intern()),常量池也会内存溢出。 -
- 这里主要讲分代回收机制
- 内存结构
- 年轻代:一个 Eden 区和两个 Survivor 区
- 年老代:一个 old 区
- 持久代:一个 Permanent 区
-
新建立的对象先放到 Eden 区中,如果 Eden 区满了之后,就会执行标记-清除算法回收 Eden 区垃圾,并把生存的对象放到 Survivor 的其中一个区中,两个 Survivor 区有一个必须是空的,当其中一个 Survivor 满了之后,采用标记-复制方法,把生存的对象放到另外一个 Survivor 区。
-
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。
-
持久代用于存放静态文件、Java类、方法、静态对象等。
- 触发 gc 的条件
-
minor GC: 当新对象生成,并且在Eden申请空间失败时,就会触发minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区
-
Full GC: 对整个堆进行整理,包括Young、old 和Perm。Full GC因为需要对整个对进行回收,以下原因可能导致 Full GC
- 年老代(old )被写满
- 持久代(Perm)被写满
- System.gc()被显示调用
- 上一次GC之后Heap的各域分配策略动态变化
jvm参数说明~~
1.jvm的结构:
-
方法区: 也是 jvm gc 中的持久代,它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。
-
堆: 也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组,包括 jvm gc 中的年轻代和年老代。
-
虚拟机栈: 每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。而局部变量表中继承 Object 的对象都是引用堆和方法区的内存,而基本数据类型的对象(boolean、byte、char、short、int、float、long、double)则是直接保存存在栈中。
-
本地方法栈: 与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
-
程序计数器: 类似于PC寄存器,是一块较小的内存区域,通过程序计数器中的值寻找要执行的指令的字节码,由于多线程间切换时要恢复每一个线程的当前执行位置,所以每个线程都有自己的程序计算器。
- 各参数说明
-
-Xmx: 堆内存大小的上限
-
-Xms: 堆内存大小的初始值
-
-Xmn: 年轻代内存大小,年轻代包括两个区,Eden 和 Survivor 区,Suvrvior 区还被平均分成了两块 from space 和 to space
-
-Xss: 每条线程内存大小
-
-XX:PermSize(java8 之后变成了 -XX:MetaspaceSize): 持久代初始内存大小
-
-XX:MaxPermSize(java8 之后变成了 -XX:MaxMetaspaceSize): 最大持久代内存大小
-
-XX:NewSize: 新生代初始堆内存占用的默认值
-
-XX:MaxNewSize: 新生代占整个堆内存的最大值
-
-XX:NewRatio: 老年代对比新生代的空间大小, 比如2代表老年代空间是新生代的两倍大小
-
-XX:SurvivorRatio: Eden/Survivor的值,比如8表示Survivor:Eden=1:8, 因为survivor区有2个, 所以Eden的占比为8/10
-
-XX:CompressedClassSpaceSize: 类指针压缩空间大小
- 指针压缩介绍
- 64位平台上默认打开
- 使用-XX:+UseCompressedOops压缩对象指针
"oops"指的是普通对象指针("ordinary" object pointers)
Java堆中对象指针会被压缩成32位
使用堆基地址(如果堆在低26G内存中的话,基地址为0) - 使用-XX:+UseCompressedClassPointers选项来压缩类指针
对象中指向类元数据的指针会被压缩成32位
类指针压缩空间会有一个基地址
- 元空间和类指针压缩空间的区别
-
类指针压缩空间只包含类的元数据,比如InstanceKlass, ArrayKlass
仅当打开了UseCompressedClassPointers选项才生效
为了提高性能,Java中的虚方法表也存放到这里 -
元空间包含类的其它比较大的元数据,比如方法,字节码,常量池等
Java JVM 垃圾回收(GC 在什么时候,对什么东西,做了什么事情)
在什么时候
首先需要知道,GC又分为minor GC 和 Full Gc(也称为Major GC)。Java 堆内存分为新生代和老年代(持久代在方法区上),新生代中又分为1个Eden区域 和两个 Survivor区域。
那么对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC;
对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次 Full GC。
Ps:上面所说的只是一般情况下,实际上,需要考虑一个空间分配担保的问题:
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。
(空间分配担保总结:
minor GC :1. 老年代最大连续可用的空间大于新生代所有对象的总空间; 2. 老年代最大连续可用空间小于新生代所有对象的总空间,并且HandlePromotionFailure设置允许担保失败,且老年代最大可用连续空间大于历次晋升老年代对象的平均大小。
full GC :1. 老年代最大连续可用的空间小于新生代所有对象的总空间,并且HandlePromotionFailure设置不允许担保失败;
2。 如果上面的2尝试minor GC失败,则出发full GC。
但是,具体到什么时刻执行,这个是由系统来进行决定,是无法预测的。
对什么东西(总结:从GC root开始搜索,搜索不到的对象,并且经过第一个标记,清理之后,仍然没有复活的对象)
主要根据可达性分析算法,如果一个对象不可达,那么就是可以回收的;如果一个对象可达,那么这个对象就不可以回收。对于可达性分析算法,它是通过一系列称为“GC Roots” 的对象作为起始点,当一个对象到 GC Roots 没有任何引用链相接的时候,那么这个对象就是不可达,就可以被回收。如下图:
这个GC Root 对象可以是一些静态的对象,Java方法的local变量或参数, native 方法引用的对象,活着的线程。
做了什么事情
主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。例如新生代采用了标记复制算法,老年代采用了标记整理法。在新生代中,分为一个Eden 区域和两个Survivor区域,真正使用的是一个Eden区域和一个Survivor区域,GC的时候,会把存活的对象放入到另外一个Survivor区域中,然后再把这个Eden区域和Survivor区域清除。那么对于老年代,采用的是标记整理法,首先标记出存活的对象,然后再移动到一端。这样也有利于减少内存碎片。
额外补充:
- SafePoint是什么
比如GC的时候必须要等到Java线程都进入到safepoint的时候VMThread才能开始执行GC,
- 循环的末尾 (防止大循环的时候一直不进入safepoint,而其他线程在等待它进入safepoint)
- 方法返回前
- 调用方法的call之后
- 抛出异常的位置
- GC收集器有哪些?CMS收集器与G1收集器的特点。
串行收集器:串行收集器使用一个单独的线程进行收集,GC时服务有停顿时间
并行收集器:次要回收中使用多线程来执行
CMS收集器是基于“标记—清除”算法实现的,经过多次标记才会被清除
G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的
[GC收集器]: http://www.jianshu.com/p/50d5c88b272d
- 类加载的几个过程:
加载、验证、准备、解析、初始化。然后是使用和卸载了
通过全限定名来加载生成class对象到内存中,然后进行验证这个class文件,包括文件格式校验、元数据验证,字节码校验等。准备是对这个对象分配内存。解析是将符号引用转化为直接引用(指针引用),初始化就是开始执行构造器的代码
- 双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
Bootstrap ClassLoader:启动类加载器,负责将$ Java_Home/lib下面的类库加载到内存中(比如rt.jar
Extension ClassLoader:标准扩展(Extension)类加载器,它负责将$Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。
ApplicationClassLoader:它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器
双亲委派模型是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。-----例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱
- 分派
静态分派(重载)与动态分派(重写)。
- 你知道哪些JVM性能调优
设定堆最小内存大小-Xms
- -Xmx:堆内存最大限制。
- 设定新生代大小。
新生代不宜太小,否则会有大量对象涌入老年代-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
- 设定垃圾回收器
年轻代用 -XX:+UseParNewGC (串行) 年老代用-XX:+UseConcMarkSweepGC (CMS)
- 设定锁的使用
多线程下关闭偏向锁,比较浪费资源
- g1 和 cms 区别,吞吐量优先和响应优先的垃圾收集器选择
1CMS是一种以最短停顿时间为目标的收集器
响应优先选择CMS,吞吐量高选择G1
- 当出现了内存溢出,你怎么排错
用jmap看内存情况,然后用 jstack主要用来查看某个Java进程内的线程堆栈信息
参考: http://icyfenix.iteye.com/blog/715301
https://github.com/konginyan/Learning-Notes/blob/master/java/jvm%20%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E.md