本文主要是介绍GC基础知识 不看你后悔啊 哥们哐哐吃的jvm书啊。建议呢,jvm 有百分之5的哥们们,和妹子们去看,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
以下呢是一些前置的知识,有助于在最后我进行知识梳理的时候,你能跟上我的思路。
1.什么是垃圾
c语言申请内存 malloc 释放内存 free
c++: new delete
java: new
自动内存回收 优点:编程上简单,手动释放内存,容易出两种类型的问题:
1.忘记回收
2多次回收
jvm的调优呢,主要就是集中在垃圾回收机制的选择和参数设置
定义:没有任何引用执行那个的一个对象或者多个对象(循环引用)
2.如何定位垃圾
- 引用计数 但是无法循环引用的垃圾,就是一堆垃圾
- 根可达算法 Java虚拟机中就是采用这种方式
理解 GC roots包含的变量。通过根找不到的对象就是垃圾
3.常见的垃圾回收算法
-
Mark-Sweep(标记清除) 位置不连续,产生碎片
就是找到垃圾,把他标记成非垃圾区域
-
Copying(拷贝)
把内存分成两半,只用其中一般,当垃圾回收的时候,我们把存活的对象拷贝到另一半如上图所示。再把上边那块全部清掉,下边的清垃圾了,就考到上边,然后下边全清空,来回重复。
快,但是浪费空间
-
Mark-Compact(标记压缩)
再回收的时候,将后边的存活的对象,依次填充到前边未使用或者可回收的磁盘块中,做一个整理,如上图。
可是这个效率比copy低。没有碎片。任何一块挪动都要进行线程同步
4.jvm内存分代模型(用于分代垃圾回收算法)
1.在部分垃圾回收器,会把jvm分成各种代,也就是不同的区域。(部分)
比较新的垃圾回收器,他就不使用代,比如说G1
->2. 新生代+老年代+永久代(1.7)/元数据区(1.8)Metaspace
- 永久代 和 元数据 装class对象的
- 永久代必须指定大小限制(将来会出现限制),元数据区可以设,也可以不设置,无上限(受限于物理内存)
- 字符串常量 1.7 存在永久代 1.8 在堆里
- 元数据区,jvm都不去管他了,它受限于操作系统。永久代1.7 好像是在堆里
- MethodArea 是一个逻辑概念,在永久代 或 元数据区
3. 运行时
上边数字就是每个区的比。了解每个区是怎么使得呢,我们就需要知道一个对象产生得过程。
当我们new一个对象,默认去eden去找空间,如果盛不开,直接去老年代。两个survivor 便于垃圾回收(YGC回收之后,大多数的对象都会被清楚),在新生代这生曾的对象,很容易回收。比如for循环中的对象。用copy算法,将eden区的活着的对象考到第一个survivor区,然后eden区清空。再次YGC之后,第一个survivor区和eden区的活着的对象考到第二个survivor区。再次YGC
把第二个sur。。区和eden 整到第一个sur区,清空。然后反复。如果survivor区的有那麽几个一直不被回收,直接进入老年代,成不来了也进入老年代。老年代就是兜底的
这里插播为啥尼survivor区和Eden区不是一比一的关系他还能赋值呢?
因为我们研究发现Eden区90多%的对象都会死掉。
如果这次Eden都活这,我们会有一个空间分配担保的策略。检测,老年代能不能装上 eden垃圾收集前的策略,如果老年代有足够的空间,老年代随便回收,老年代也放不下,就会判断 有一个参数,允不允许冒险,允许则检查一下老年代历史上生的平均数,如果大于就冒险,如果不行,引发fullgc
fullgc 也存不下就崩了
老年代满了就会触发Full GC(FGC),老年代就是就是装顽固分子的。
我们GC调优就是 减少FGC
FULL GC 就是新生代和老年代一起 进行回收的。
两个概念
MinorGC = YGC 年轻代垃圾回收
MajorGC = FGC
5 常见的垃圾回收器
10种垃圾回收器
图的说明
在中间的是不在用新生代和老年代了。上边就是用在新生代,下半部分就是用在老年代的。
待解G1。。只了解了分带模型
Serial()是新生代的垃圾回收器,垃圾回收时 停止所有线程,开始垃圾回收 单线程垃圾回收。卡顿是你程序卡顿了,可不是你的cpu,cpu永远有活干
ps 并行回收
parnew适合cms配合使用的,为了配合cms 在ps设计了parnew
cms 简单来说是在回收时不用 停止应用程序 不用stop the word
总结梳理jvm 脉络
做简单的梳理
二话不说先上一章图。以下全靠自己的语言写的,有不对的请指正说的补全请参考周志明的jvm的书
从student stu = new student()开始
先解决java程序是怎么划分jvm内存的:
一个部分是线程独享的,一个是一部分线程共享的
线程的独享的呢又分为栈,分别是虚拟机栈和本地方法栈。每一个方法的调用就伴随着一个栈帧的创建。栈帧里边主要存的就是局部变量表(简单理解为方法里传的参数和一些方法里边的局部变量)本地方法栈是我们掉 本地方法所用的。再有一个就是pc pc不会出现内存溢出,因为他村的东西少 存放下一行指令的地放
线程共享的呢,就是这个方法区和这个堆
方法区加载的类信息就跟我们类加载那部分有关了
每个线程都按照方法区的类的模板在堆中创建对象,自然他俩就是线程共享的。
那么小的对象在堆里边是怎么划分的呢
主要放在堆里边:
如果堆里边内存是规整的。用过的在一边,没用用过的在另外一边,就是一个指针碰撞法去分配空间。另一个空闲列表法,你这个堆特别乱,我用一个表去记录摸个地方使用没使用过。
因为每个线程都在不段的去创建对象,每个对象都要去抢那个表,或者那个指针。线程的安全问题就会下降 。所以我们就先分配一个线程空间我们成为TLAB,
视野在小点聚焦到一个对象上
对象这个小玩意的内存是怎样的呢?
它由三部分组成:
- 对象头 markword 指向这个实例的对应的类型数据。。。。。 这点看书吧
- 示例数据
- 对齐填充
那么student a= new student()又是怎么指向这个对象的呢
句柄法 :由a栈里边的这个地址指向句柄,句柄呢一个指向类型数据,一个实例数据
好处,移动对象时,栈里边的数据不会变。
直接指针法: 是直接由栈里边的数据指向了堆里边对象的这个实例数据,实例数据中的一个地方,指向了我们的类型数据
好处就是少一次指针定位的时间,
分分完了,那就准备收呗
先判生死
上边前置知识我已经写了过了,哪那些该死呢
- 引用计数 但是无法循环引用的垃圾,就是一堆垃圾
- 根可达算法 Java虚拟机中就是采用这种方式 gc roots是什么 虚拟机栈 本地方法栈中的东西,锁所持有的东西,静态变量 所指向的 常量所置向的对象,本身jvm指向的对象,老年代有新生代指向的指针老年去区那一块的对象
看前面。
在这里可达性分析,也就是判生死的算法,因为他是看那些能从根找到,巴拉巴拉,自己看书很简单。他们之间是引用的所以还要说一下这个
四种引用
强引用 new object()
软引用 我垃圾快溢出了,我用删你这个这就是软引用,特别适合做缓存。 new softrefence()
弱引用 new weakreference 每一次垃级收集都会把只有弱引用引用的对象干掉
虚引用 啥也不是这玩意,我可能是垃圾,没理解他的用处
该死该活确定了怎么办 该回收了,介绍了三个假说,
弱分代假说: 大部分都死老快了,不需要分代
强分代:获得越久的就越不容易死
跨代引用:要么一会就死,要么一值活着,所有跨代引用的对象都是少量的。不如sur。。区,到了老年代
进而引入了三种垃圾清除算法 最一开始写了
说说具体实现
有一个oop map的帮助我们变量gc roots
有一个安全点帮助我们创建oopmap的
有一个安全区域来处理 阻塞的或者睡眠的线程等等
垃圾收集器 g1 cms
cms 老年代 标记清楚的 并发的收集器 就有碎片化的问题,因为是并发的他对cpu资源就要求高一点。
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”
初始标记仅仅只是标记一下GC
Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发行而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对的标记记录(详见3.4.6节中关于增量更新的讲解),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一
起工作,所以从总体上来说,CM S收集器的内存回收过程是与用户线程一起并发执行的。
优点并发收集,低停顿
他为解决对象消失使用的方法后边再说
G1
是全堆收集的,他不划分新生代 老年代了
他是基于region这样的小区域收集的。并且收集的时候会把region做排队,那个优先级高,那个先收集。没有明显的分带,但是region可以扮演分代。他怎样解决跨代引用 ----- 卡表
不同区域 的对象直接相互引用这样就叫跨代引用 出现这个就无法判断对象是不是该死了
这时候就会有一个并发的可达性分析问题对象消失 引出了三色标记
cms 增量更新 g1原始快照
参考
GC 深入(小白,对gc有一个进一步的了解)_肥春勿扰的博客-CSDN博客
垃圾收完了 上边讲的那个分代模型,就是最上边有eden的那一部分
其中补充 有动态年龄判断的问题,当前这个年龄的对象占了我们整个这个survivor区的一半,我们就把这个年龄的,和比他大的给他放到老年代。目的就是减少我们来回复制所带来的开销。然后就是空间担保的问题,新生代存活太多怎么办。
关于内存的这一块学完了,然后就是我们的类这一块
类结构
代更
类加载
双亲委派
核心的类由更上层的类加载器加载 ,越往下他的类加载器通用器就越低
用个通俗的话说类加载器层级越高,他越是底层的类加载
这个是我们自己写的类到这加载
我们是一个类加载的时候先把他一级一级的递到他的最顶层的类加载器,在一级一级的判断看看那个类加载器能加载。也是说你自己写的类加载时会一层一层的到Bootstrap那个类加载器,然后再往下判断知道判断到应用程序类加载器 ,发现这个类加载器能加载自己了。就不再向下走了。
越往下他加载的通用性就越低。
Java 自带的三种类加载器分别是:BootStrap 启动类加载器、扩展类加载器和应用加载器(也叫系统加载器)。图右边的桔黄色文字表示各类加载器对应的加载目录。启动类加载器加载 java home 中 lib 目录下的类,扩展加载器负责加载 ext 目录下的类,应用加载器加载 classpath 指定目录下的类。除此之外,可以自定义类加载器
这种机制呢就防止了我们这个核心文件被篡改的风险 。因为核心类一般都是由上层加载器加载的。因为我们比如我们自己编写一个核心类,而我们自己编写的这个呢,得用第三层加载器才能加载,可当自己写的核心类网上递交的时候,在第一层是不会加载的,因为他没有自己编写类的这类加载器,所以只能在第三层加载(个人认为是这样,不对可以改)。因为这个双亲委派机制避免了这个类的重复加载,所以你的这个核心api就不会别篡改
在加载的时候他也足够聪明会有一个缓存,如果加载过,就直接向缓存中取了。
破坏双亲委派
上一级的加载器想访问下一级加载器加载的类,访问不到。
第二次jdbc 很顶层了 但是在调用的时候他需要各大厂商提供的驱动,也就是在应用类加载器加载的类,那么根据双亲委派机制他是没法访问的,
为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器
(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
然后我们写了一个SPI
第三次被替换呢,就是追求热部署。osgi 支持模块热部署 改一点文件,接得重启,麻烦。
为啥热部署就得破环双亲委派呢,因为我们java中认为类是唯一的,要把类和类加载器同时判断你才是唯一的,所以你就得把加载器和他加载的类干掉 ,他才能替换到他。双亲委派机制可以判断类的唯一性。
这篇关于GC基础知识 不看你后悔啊 哥们哐哐吃的jvm书啊。建议呢,jvm 有百分之5的哥们们,和妹子们去看的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!