从上图可以看出,JVM的内存区域主要分为两个大块,一是内存共享区,一是内存线程私有区。
结合java代码的执行编译过程,可以理解成:当我们的代码呗java执行引擎加载后,解释器进行处理成.class文件,通过JIT(即时编译器)编译,中间涉及到类加载的过程(先略过),此时编译后的代码和一些常量、静态变量都会保存在方法区中。随着代码的执行,会创建对象,这些对象都会存放在堆内存空间。代码的执行,都是由main线程执行,随着程序的调用,线程中的程序计数器,会记录每个程序执行到了哪一步,同时伴随着方法调用,通过虚拟机栈压栈和弹栈的动作往下执行,当程序执行完毕后就涉及到对象的回收和销毁过程。
聊完内存分配,涉及到的重点就是类加载过程
常规意义上,我们都把类加载器分为四层,逐层继承。最顶层是BootStrap加载器,通过源码,我们可以看到,他的实现和定义都是空的,所以我们可以理解成它并不存在。主要的类加载都是在extention和application中完成,其中extention偏向加载jre目录下的对象,application偏向记载classpath下jar中的对象。
**补充重点:双亲委派模型**
当一个类加载器区加载一个类的时候,会优先委托父类去尝试加载,以此传递到顶层(Extention),如果父类加载不了,才会由子类加载,这样确保了:
1.一个类只会被加载一次
2.每个类都会尽可能被夹在
3.避免恶意加载(直接通过自定义加载器加载成JVM无法处理的)
**补充重点:happend-before原则**
即先后原则,大致如下:
1.加载:由类加载器,对class文件进行读写到JVM中,流程大致为先获取class文件,以二进制流读入内存,再将二进制流静态存储结构转化为运行时数据结构,最后在内存(堆)中生成对象;
2.链接:链接也分为三个过程,验证、准备、解析。验证的目的是为了确保加载进来的二进制数据流,符合JVM规范,准备阶段是为静态变量和常量在方法区分配内存,设置默认值,解析是虚拟机讲常量池的符号引用替换为直接引用的过程
3.初始化:根据赋值语句为变量赋值和内存分配的过程
4.使用:程序内部的调用
5.卸载:对象的销毁,引用变为null
1.每个类都有一个初始化锁LOCK,初始化的第一步就是去获取LOCK
2.如果这个类正在被其他线程初始化,此时当前线程获取不到LOCK,处于等待状态
3.如果这个类已经被初始化,则不去尝试获取锁,直接使用该对象
4.如果其他线程初始化失败,抛出异常,该线程会释放锁,当前线程获取到LOCK后会去做初始化的动作(类变量初始化和静态代码块执行,对象内存分配)
5.当前线程初始化完成后,会释放LOCK,并通知其他线程放弃初始化
类加载说完之后,就涉及到对象的回收机制,这里我们主要针对的就是堆内存的回收。常见的堆内存分代为新生代和老年代,新生代又分为Eden区和Survivor区,Survivor区又分为From区和To区。常规情况下,我们的对象都是创建在新生代的Eden区,当一个对象被标记为可清除后,会从Eden区转移到From区,进行标记处理,满足回收条件后,会在To区进行回收。当一个对象多次回收失败,会被标记转移到老年代,重新进行回收动作。当然,如果一个对象是大对象,它的创建和销毁都是在老年代发生。
新生代的回收,称之为Young GC,老年代的回收称之为Full GC
**GC中的补充概念**:
**并行:** 多个回收线程同时执行,用户线程等待
**并发:** 回收线程和用户线程同时执行
**吞吐量:** 用户线程运行时长 / (用户线程运行时长 + GC时长)
说完内存分代,就会引出不同分代下的垃圾收集器和相关的垃圾回收算法,常见的垃圾收集器如下。
新生代收集器,都是使用复制算法,内存消耗会比较大,占用资源较多。并且他们都只有并行阶段,没有并发阶段,只要发生GC,就会STW
单线程回收,简单,单一线程使用率高
多线程回收,为了解决Serial的效率问题,出的多线程版本,默认开启线程数和CPU核数相同(变相佐证了IO密集型操作,线程数选择cpu核数)
多线程,高吞吐,GC自适应调节
标记清除算法,多线程回收
**回收过程:**
初始标记:标记GC Root能直接访问的对象,会出现STW
并发标记:在用户线程执行过程中,进行标记
重新标记:为了修正因用户线程执行,导致遗漏的对象,重新打标,会出现STW
并发清除:在用户线程执行的过程中,进行回收
标记整理算法,单线程回收
标记整理算法,多线程回收,高吞吐
区别于其他分代收集器,G1收集器的对象是整个堆,他的优点就是能充分利用CPU资源,完美适配多核条件下的回收场景,大幅减少STW时间。
G1针对新生代(标记整理)和老年代(复制算法),有不同的回收策略
G1回收后,会重新整理内存空间,不会产生内存碎片
他的回收过程和CMS类似,只是最后一步是筛选回收,并不是全部标记对象并发清除
G1的特点:
1、设计原则是尽可能多的回收垃圾
2、内存分区的思路(避免像CMS那种,整个堆内存回收,STW时间过长),分为不同的内存块region,它的内存回收就是把回收对象发知道另一个region中,实现局部压缩
3、只有分代概念,实际的内存空间会在运行时不停切换分区,从而使用不同方式回收,内存使用更合理
4、停顿时间模型,回收时间可控,不会长时间STW
GC回收器通常的选择:
CMS+ParNew
G1
旨在减少STW时间
**标记清除:** 标记为可回收的对象内存回收,会产生内存碎片,空间使用率低,但是高效
**标记整理:** 标记可回收的对象内存回收后,会进行内存移动,空间使用率高,相对回收效率降低
**复制算法:** 重新开辟一个内存空间,进行对象移动,在新的空间中删除边际回收的对象,同事整理合并
对于这样的语句Load1; LoadLoad; Load2, 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
对于这样的语句Store1; StoreStore; Store2, 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
对于这样的语句Load1; LoadStore; Store2, 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
对于这样的语句Store1; StoreLoad; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放
的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出
有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC.
代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、
或者通过动态代码加载等方法,导致常量池的膨胀
会回收掉直接内存这部分的内存,所以可能原因是直接或间接使用了ByteBuffer中的
allocateDirect方法的时候,而没有做clear
线程分配内存区域
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。