JVM内存管理深度剖析
JVM-java虚拟机
JVM --- Java Virtual Machine --- java虚拟机。
所谓的java虚拟机,其实就是翻译软件,将.class.jar等文件,翻译成各个操作系统所能识别的机器语言,JVM是跨平台的,甚至是跨语言的,所以这就是Java的魅力。
JVM、JRE、JDK
JDK:Java工具包
JRE:(Java Runtime Environment)Java 的运行时环境,也就是JVM+一堆基础类库,JRE提供的很多类库
JVM:翻译,把Class翻译成机器语言
所以 JDK>JRE>JVM
JVM 的结构
JVM运行过程
运行时数据区是重点!!!
运行时数据区又分成两部分:线程之间共享的(方法区、堆),线程私有的(虚拟机栈、程序计数器、本地方法栈)
程序计数器
用来记录当前线程的每一行代码所执行的字节码地址、操作等。是字节码的行号指示器。(本行对应的操作,内存物理地址,线程恢复等)
原因:cpu的时间片轮转,因为时间片轮转,执行到一半停了,切其他线程了,所以得有一个记录当前执行到哪行,啥操作的记录器。
程序计数器很小,所以不会OOM
虚拟机栈
常说的堆栈,其中的栈指的就是JVM的虚拟机栈。
栈是先进后出的结构(LIFO),类比于弹夹。
一个栈(虚拟机栈)类似于弹夹,都是先进后出的,每个栈帧就是子弹(1个方法=1个栈帧),栈帧是一个一个压入栈的(子弹压入弹夹),然后一个一个射出。
栈帧的结构
大小限制 -Xss
栈的大小缺省为1M,可用参数 –Xss调整大小,例如-Xss256k,如果超过限制就会OOM。栈放的栈帧是有大小的(弹夹有大小,放的子弹有限制)
局部变量表
存储局部变量,存储八大基本类型和对象的引用,Object对象会存放堆里,引用会存这。一个32位,64位的类型会一个占两个位置。
操作数栈
存放我们方法执行的操作数的。刚开始栈是空的,有操作的时候会频繁入栈出栈进行计算。
动态连接
用来记录多态具体执行哪个方法的,如 People wang = new Man(); wang.wc(); 在运行时动态连接到 wang.男厕所
返回地址
返回返回值的地址(程序计数器记录的物理地址),异常的时候不走这个,走异常处理器表
执行下面操作的流程:
- 局部变量表(第一个一般都是this,除了静态这样的)会声明两变量,x和y用1,2存储
- x=1压入操作数栈
- y=2压入操作数栈(因为是栈 先进后出,所以x=1本来在栈顶1号位,会被压到下面位置(类似子弹))
- 将xy都移出栈,去cpu执行 1+2操作,返回3放入操作数栈
- 将操作数栈里的3返回值取出来,赋值给局部变量表中的y=2
本地方法栈
功能类似于虚拟机栈,是native 方法,交给C来执行。
方法区
(永久代,在HotSpot 虚拟机中使用永久代来实现方法区,但是其他虚拟机并不是。所以方法区≠永久代)
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。 (String会存到堆里面,引用会存在常量池里)
JVM 在加载类时,会先加载.class文件。
.class文件包含类的版本、字段、方法和接口等描述+常量池
常量池用于存放编译期间生成的各种字面量和符号引用。
字面量=String+final常量。符号引用=类、方法、字段的全名和描述符。
而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。 例如,类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的;在 JVM 加载完类之后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。
方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。 假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地
配置元空间大小参数:
dk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8以后大小就只受本机总内存的限制(如果不设置参数的话)
Java8 为什么使用元空间替代永久代,这样做有什么好处呢?
1.两公司合并,为了融合
2.永久代内存经常不够用或发生内存溢出
堆
堆是 JVM 上最大的内存区域,这里存储了 几乎所有对象+数组。垃圾回收,操作的对象就是堆。
程序启动就会申请堆空间,随着堆增大,会进行GC(垃圾回收)。
对象new出来就是在堆上创建出来的。基本数据类型:方法体内声明的(局部变量)存在栈里面,不然就是存在堆上。
配置堆的大小:
-Xmx:堆的最大值;
-Xms:堆的最小值(初始值);
堆分为新生代(容易被GC回收)和老年代(不容易被GC回收),物理地址是连续的。一个对象创建出来的时候在新生代,如果经过好几次GC都还活着,那就会移到老年代。
直接内存
不属于JVM运行时数据区的。但是使用NIO就可以在内存里申请一块区域供JVM使用。也会发生OOM
配置的大小:
-XX:MaxDirectMemorySize
HSDB工具
可以使用HSDB工具来查看JVM运行时的情况,用线程id去那个工具搜索。
内存溢出
JVM各个部分除了程序计数器以外所有部分都可能发生内存溢出。
- 栈溢出
1.每个栈都有最小固定大小的,1M。无限建线程,就会无限建栈,然后机器内存不足就会OOM。
2.栈的栈帧是有大小上限的(弹夹子弹有上限)。栈帧=方法,所以无限执行方法,会java.lang.StackOverflowError。
-
堆溢出
堆上对象太多太大,导致内存溢出,Android大部分都是堆溢出。
1.代码正常可以通过 调大堆大小。 -Xms,-Xmx参数。
2.大部分都是内存泄漏导致的,就应该检查代码了(异常持有引用等)。
3.检查调整对象,是否不合理的设计,持有时间太长,对象太大手动清理一部分,对象生命周期太长。 -
方法区溢出
(1) 运行时常量池溢出
(2)方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。 -
本机直接内存溢出
申请更大空间,或者检查代码。
虚拟机优化技术
1. 方法内联:将简单的方法直接不调用,放调用的地方复制一份执行(调用方法会在栈里多一个栈帧,然后频繁的入栈出栈)
2. 栈帧之间数据的共享(虚拟机已经优化好的)
栈帧之间会共用一些数据,这时不会创建多分,会共用一份。如a(10); 10这个变量传递在两方法(栈帧)上,只有一份。
对象与垃圾回收机制
对象的创建
对象创建过程图,非常重要
别的步骤看图,分配内存的时候会根据内存是否工整采用不同的方式分配内存,分别是指针碰撞和空闲列表。
但是因为堆是多线程共享的,所以会引发线程安全问题,所以有两种解决方案CAS机制和本地线程分配缓冲TLAB。
指针碰撞
空闲列表
CAS机制
CAS机制compare and swap比较并且交换,先找到空闲的区域,然后利用cpu的CAS指令,如果这块内存无人占据,那我就写入,如果在执行CPU CAS指令时,已经被别人占据了(比较compare以后跟我想要的不同)那就继续循环找下一块区域
详情见线程
juejin.cn/post/695650…
本地线程分配缓冲TLAB
因为线程不多,而且所需的内存较小一般,所以 去 堆里的新生代Eden区直接给每个线程划一块内存,供其使用。
这块区域很小,一般占用Eden区的1%,2%
对象的构成
对象分为:对象头、实例数据、对齐填充
对象的访问定位
访问对象主流的有两种:句柄(堆里划出一个句柄池,用句柄池再来管理)和直接指针
判断对象的存活
判断对象是否存活,是否需要被GC回收,现在一般有两种方式:引用计数算法和可达性分析(根可达)
可达性分析
GC Roots的对象(系统定义好的,堆以外的指针):
● 虚拟机栈(栈帧中的本地变量表)中引用的对象。
● 方法区中类静态属性引用的对象。
● 方法区中常量引用的对象。
● 本地方法栈中JNI(即一般说的Native方法)引用的对象。
● VM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。
● 所有被同步锁(synchronized关键)持有的对象。
● JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
● JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代时)
各种引用
强>软>弱>虚
对象的分配策略-对象创建的完整流程
- 有些对象是在栈上分配的,只要满足逃逸分析的小对象,就能在栈上分配(也就是方法里的局部变量,别的方法、线程没引用到)
好处:不必在堆上分配,速度快。栈是方法执行完了,里面的内存就释放了 不用GC
-
本地线程分配缓冲(TLAB)
-
是否是大对象,大对象直接分配到老年代(大的String,数组)。原因是老年代不用频繁GC、移动。而且老年代空间大。新生代:老年代=1/3 : 2/3
4.一般对象new出来都是在新生代的Eden(伊甸园)区。然后被频繁GC活下来以后晋级到老年代。
1.对象出生在Eden区,对象头上存储的GC年龄age为空(0岁)
2.第一次GC以后,会有90%的对象都被GC回收掉,剩下的10%会晋级到from区,这时age=1
3.第二次GC如果还是活下来会晋级到To区,age=2,第三次第四次...会反复在from区和to区跳然后年龄age++,直到age到达一个临界值15(或者自己设置或者根据不同算法得出)
4.年龄age到达临界值15,或者因为空间分配担保会晋级到老年代Tenured区
对象的回收
对象的生讲完了,现在讲对象的死。也就是GC。
分代收集理论
GC垃圾回收器,在新生代和老年代的回收算法是不一样的。新生代里用的是复制算法。老年代用的是标记清除算法和标记整理算法。
空间大小:新生代:老年代 = 1:2 。新生代里 Eden:from:to = 8:1:1
复制算法(Copying)
标记-清除算法(Mark-Sweep)
优点:不要整理,快,对象不需要移动
缺点:内存碎片
标记-整理算法(Mark-Compact)
JVM中常见的垃圾收集器
一代目:单线程。Serial--Serial Old---复制算法、标记整理算法
二代目:多线程并行。Parallel Scavenge--Parallel Old--复制算法、标记整理算法。就从单线程改成多线程没啥区别
三代目:多线程并发。ParNew--CMS---复制算法、标记清除算法
单线程与多线程并行
一代目、二代目
CMS垃圾回收器
Android采用的垃圾回收器
CMS将GC分成好几个阶段:
1.先单独执行初始标记(标记可达性分析的第一层)---执行速度快
2.并发标记 可达性分析根以外的叶子节点 --- 时间长,所以跟用户线程并发执行
3.重新标记 在执行期间新new出来的对象
4.并发清理并重置线程
垃圾回收器暂停用户线程,然后再去执行GC的现象叫Stop The World
G1垃圾回收器
总结与面试
常量池与String
JVM内存结构说一下!
什么情况下内存栈溢出?
java.lang.StackOverflowError 如果出现了可能会是无限递归。
OutOfMemoryError:不断建立线程,JVM申请栈内存,机器没有足够的内存。
描述new一个对象的流程!
Java对象会不会分配在栈中?
可以,如果这个对象不满足逃逸分析,那么虚拟机在特定的情况下会走栈上分配。
如果判断一个对象是否被回收,有哪些算法,实际虚拟机使用得最多的是什么?
引用计数法和根可达性分析两种,用得最多是根可达性分析。
GC收集算法有哪些?他们的特点是什么?
复制、标记清除、标记整理。复制速度快,但是要浪费空间,不会内存碎片。标记清除空间利用率高,但是有内存碎片。标记整理算法没有内存碎片,但是要移动对象,性能较低。三种算法各有所长,各有所短。
JVM中一次完整的GC流程是怎样的?对象如何晋级到老年代?
对象优先在新生代区中分配,若没有足够空间,Minor GC;
大对象(需要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。
如果对象在新生代出生并经过第一次MGC后仍然存活,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。
Java中的几种引用关系,他们的区别是什么?
final、finally、finalize的区别
在java中,final可以用来修饰类,方法和变量(成员变量或局部变量)
当用final修饰类的时,表明该类不能被其他类所继承。当我们需要让一个类永远不被继承,此时就可以用final修饰,但要注意:
final类中所有的成员方法都会隐式的定义为final方法。
使用final方法的原因主要有两个:
(1) 把方法锁定,以防止继承类对其进行更改。
(2) 效率,在早期的java版本中,会将final方法转为内嵌调用。但若方法过于庞大,可能在性能上不会有多大提升。因此在最近版本中,不需要final方法进行这些优化了。
final成员变量表示常量,只能被赋值一次,赋值后其值不再改变。
finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下
Object中的Finalize方法
即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。
所以建议大家尽量不要使用finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议大家忘了finalize方法!因为在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好