在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的;如下图所示
程序计数器:字节码的行号指示器。
作用:
程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
私有,生命周期=线程;java执行的内存模型,每次方法调用的数据都是通过栈传递的
组成:由一个个栈帧组成,每个栈帧有:局部变量表、操作数栈、动态链接、方法出入口信息
局部变量表:存放各种数据类型、对象引用
本地方法栈
和虚拟机栈相似,区别:虚拟机栈为虚拟机执行方法服务,本地方法栈为虚拟机使用到的Native方法服务
堆
内存中最大的一块,虚拟机启动时创建
目的:存放对象实例,数组
垃圾回收的主要区域
方法区,存放
1.类的信息(名称、修饰符等)、
2.类中的静态常量、
3.类中定义为final类型的常量、
4.类中的Field信息、
5.类中的方法信息
分为常量池
字符串、final变量
类和接口名,字段名,描述符,方法名
静态变量
父类静态成员变量,子类静态成员变量,父类构造方法,子类构造方法
元空间:类信息;编译代码
主内存和工作内存
JVM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成
那么两个线程之间如何能够达到可见性,可以参考文章volatile的使用:https://blog.csdn.net/Goligory/article/details/89648177
内存溢出原因:
垃圾过多jvm没有及时回收
1.引用变量过多使用static
2.大量的递归和无限递归(递归中新建对象)
3.大量循环(循环中新建对象)/ 死循环产生过多实体
4.数据库查询一次性查所有记录超过10万条可能造成溢出
5.String 是否使用过多+的操作
启动参数内存值设定过小
栈溢出原因:
1.是否递归调用,递归调用方法
2.大量循环/死循环
3.全局变量是否过多
4.数组,list,map是否过大
堆溢出:递归new对象
内存泄漏
申请内存后,无法释放已申请的内存空间,内存泄漏堆积后果很严重
1.常发性内存泄漏:多次执行
2.偶发性内存泄漏:
3.一次性内存泄漏:只执行一次,如构造函数中分配内存没有释放
4.隐式内存泄漏:程序不停分配内存,结束后才释放内存,严格说没有泄漏,但是如果不及时释放也可能导致耗尽所有内存
泄漏原因
长生命周期对象持有短生命周期对象导致持有其引用不能回收,发生内存泄漏
1.静态集合类引起内存泄漏:如HashMap,vector
2.监听器,释放对象时没有删除
3.各种链接没有close
4.单例模式持有外部对象引用
============GC垃圾回收=================================================================
垃圾回收我们要熟悉,我们要知道它是如何工作的,为什么这么工作,好处是什么?
堆是GC的主要区域
jdk1.8之后永久代改成了元空间,元空间使用的是直接内存
JVM如何确定哪些对象应该进行回收?
两种方式
1.引用计数法:给对象加计数器,用时+1,不用-1,但是无法解决对象之间循环引用的问题,java没用
2.可达性分析算法:
通过一些被称为引用链GC Roots的对象作为起点,从这些节点开始向下搜索,走过的路径被成为Reference Chain,当一个对象到GC Roots没有任何引用链相连时(即从GC Roots到该节点不可达),证明该对象是不可用的
可作为GC Root的对象包括一下几种:
知道了需要回收的对象,那什么时候回收?怎么回收?
什么时候进行回收?
cpu空闲时自动回收
堆内存满了后
主动调用System.gc()
3大回收算法
1.标记/清除算法【最基础】
2.复制算法
3.标记/整理算法
jvm采用`分代收集算法`对不同区域采用不同的回收算法。
新生代:复制算法
Eden,From Survivor,To Survivor
java对象一般在新生代出生,新生代也是GC的频繁区域。深入理解JVM虚拟机上说98%的对象存活率很低,适用于复制算法
它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1
目标尽快收集掉生命周期短的对象,一般情况Eden区中新生成的对象首先放在新生代。垃圾回收时,先将eden区存活的对象复制到survivor0区,然后清空eden区,当survivor0区满了时,将eden区和survivor0区存活对象复制到survivor1区,然后清空eden和survivor0区,交换survivor0区和survivor1区角色,下次垃圾回收时会扫描eden区和survivor1区,如此往复。
如果当survivor1区也不足以放eden区和survivor0区的存活对象,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都回收。
新生代发生的GC也叫MinorGC,MinorGC发生频率比较高,不一定等eden区满了才触发
老年代:标记-清除;标记-整理
老年代存放的都是生命周期较长的对象,在新生代中经历了n次垃圾回收后仍然存活的对象就会被放到老年代中
永久代
主要存放静态文件,如java类,方法等。永久代对垃圾回收没有显著影响。但是反射,动态代理,CGLib等可能动态生成class文件,这时需要设置比较大的永久代空间存放
大对象、长期存活的对象进入老年代
注意:jdk1.8废弃了永久代,提供了相似的元空间技术
去永久代的原因有:
(1)字符串存在永久代中,容易出现性能问题和内存溢出。
(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
JVM参数
堆内存最小和最大指定
-Xms2G -Xmx5G
将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
显式新生代内存 两种指定方式
1.-XX:newSize=256m
-XX:MaxNewSize=1024m
2.-Xmn256m
显示指定元空间大小
jdk8要指定metabase大小,因为使用直接内存,虚拟机可能会耗尽系统内存
垃圾回收器
1.串行垃圾回收器
2.并行垃圾回收器
3.CMS垃圾回收器
4.G1垃圾回收器
堆参数
回收器参数
常用命令
GC调优原则
多数的 Java 应用不需要在服务器上进行 GC 优化; 多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题; 在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合); 减少创建对象的数量; 减少使用全局变量和大对象(占用连续内存空间); GC 优化是到最后不得已才采用的手段; 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多
调优策略
1.新对象预留在新生代,通过-Xmn调价新生代大小,最大限度降低新对象直接进入老年代的情况
2.大对象进入老年代
3.合理设置老年代对象的年龄-XX:MaxTenuringThreshold
4.设置稳定的堆大小
5.注意:满足下面说法则不需要GC优化
MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次
引用:https://www.cnblogs.com/shenjianjun/p/9512949.html
GC https://www.cnblogs.com/ITPower/p/7929010.html
这个引用蛮全的推荐:https://www.jianshu.com/p/76959115d486