JVM内存模型

JVM理解

1.Java为什么要依靠JVM

操作系统管理硬件,并向程序员提供了一层接口,叫系统呼应层,程序员可以面向这一层的借口编程,但是在不同的操作系统中这些接口是不一样的。所以同一个程序不能在不同的系统上运行,比如与平台有关的程序,C、C++。这对开发者来说就不是很友好。

而java能解决这个问题,通过JVM来向下关联所有的操作系统,向上提供同一的接口(JavaAPI),开发者只需要面向JVM(JavaAPI)编程,至于JVM是如何各种不同的操作系统打交道开发者完全不用管,这就是java的跨平台的性质。也是JVM很重要的原因。但同时也是JVM需要与底层进行解释,速度也会比编译型语言的速度慢。

2.JDK1.8中的JVM内存模型

与1.7移动了方法区到元空间中
1.8同1.7比,最大的差别就是:元数据区取代了永久代(方法区的一种实现)。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

因此总体分为堆、栈、方法区。堆、方法区线程共享,栈为线程私有。

每个里面存放的都是些什么?

JVM中内存最大的一块,是所有线程共享的一块内存区域。存放的是对象实例及数组。也是垃圾收集的主要区域。
成员变量和new的对象,字符串常量池(1.8之后)
在这里插入图片描述
主要分为新生代:Eden,两个Survivor区域,老年代(1/3,2/3)
大对象首先会在Eden区域分配,然后一次新生回收垃圾,对象还存活会进入s0或s1,对象的年龄会增加1,当年龄增加到一定程度后晋升到老年代中。
-X:MaxTenuringThreshold 设置阈值
堆中最容易出现OOM(java heap space,GC overhead limit Exceeded)

每个Java方法在执行的时候会创建一个栈帧用于存储局部变量表,操作数栈,动态链接和方法出口等信息。每个方法从调用直到结束,就对应一个栈帧从虚拟机栈中入栈和出栈的过程
本地方法栈就是对应的native方法的执行
他的生命周期随着线程的创建而创建,线程结束则死亡

局部变量表:基本类型(引用和值都存储在栈中)和对象引用(指向对象起始地址的引用指针,或者句柄以及其他与此对象相关的位置),return Address类型(指向了一条字节码指令的地址)
会出现:StackOverFlowError(栈请求深度超过当前虚拟机栈的最大深度)和OOM(虚拟机在动态扩展栈的时候无法申请足够的空间)

程序计数器

当前线程所执行的字节码的行号指示器,指向下一条JVM指令的执行地址。
如果当前线程执行的是native方法,则其值为null。
常见的分支,循环,跳转,线程恢复等功能需要依赖这个计数器。
所以每个线程是私有的,是唯一不会出现OOM的区域。
他的生命周期随着线程的创建而创建,线程结束则死亡

常问问题:堆和栈的区别?
1.栈:系统自动分配,堆需要自己申请new,因此比较慢
2.栈:只要剩余空间大于所申请的空间,就为程序提供内存,否则栈溢出
堆:通过一个记录空闲内存地址的链表,寻找第一个大于所申请空间的节点,将该节点从空闲链表删除,并将其空间分配给程序。
3.栈:连续的内存区域,系统事先规定好,2M(windows下),堆是向高地址扩展的数据结构,不连续的内存区域,由空闲链表来空闲内存地址。大小受到有效虚拟内存的限制,空间比栈大。

元空间

1.7的方法区移动到直接内存的元空间中:
1.使用直接内存,受本机可用内存的限制,溢出几率小
2.元空间中存放的是类的元数据,加载元数据由系统实际可用空间控制,这样加载的类会更多

原始方法区:加载的类的信息,常量和静态变量,及时编译后的代码
因此与方法区类似,这里面存放的主要是虚拟机类的信息。
而常量和静态变量在堆中

各种常量池
1.运行时常量池–内存的元空间中

在JVM运行时诞生,其实就是将编译后的类信息放入运行时的一个区域,是在类加载完成后,将每个class常量池中的符号引用值转存到运行时常量池中。也就是每个class都有一个运行时常量池,类解析后,将符号引用替换为直接引用。

2.字符串常量-堆中

里面的内容是在类加载完成,经过验证,准备阶段之后在堆中生成的字符串对象实例,然后实例引用值存到string pool中(注意是引用,)

3.类文件常量池–存在于class文件中-堆

主要内容:符号引用和字面量
符号引用:类和结构的完全限定名,字段的名称和描述符,方法的名称和描述符
字面量:文本字符串,基本数据类型的值,声明为final的常量值
为表结构,编译时生成,Java虚拟机在执行指令的时候会依赖这些信息,后面就放入运行时常量池中。

3.类如何加载

在这里插入图片描述

完整的生命周期:加载–连接-(验证,准备,解析)–初始化–使用–卸载
类是在运行期间第一次使用时动态加载的,不是一次性加载所有类,这样会占用很多内存

加载:
主要完成:
1.通过类的完全限定名称获取定义该类的二进制字节流
2.将二进制字节流表示的静态存储结构转化为方法区的运行时存储结构(对应上面的运行时常量池吧)
3.在内存中生成一个代表该类的class对象,作为方法区访问数据的入口

有哪些类加载器?(java.lang.classloader)
JVM内置了三个重要的ClassLoader:
BootstrapClassLoader(启动类加载器):负责加载javahome/lib下 的jar包,-Xbootclasspath参数指定的路径中的所有类
下面均继承自java.lang.ClassLoader:
ExtensionClassLoader(扩展类加载器):负责加载jrehome/lib/ext目录下的jar包和类,
AppClassLoader(应用程序类加载器):面向用户的加载器,负责加载当前应用classpath下的jar包和类

package JVM;

public class ClassLoader {
    public static void main(String[] args) {
        System.out.println("ClassLoader's classloader is" + ClassLoader.class.getClassLoader());
        System.out.println("The parent of ClassLoader's classloader is" + ClassLoader.class.getClassLoader().getParent());
        System.out.println("The Grandparent of ClassLoader's classloader is" + ClassLoader.class.getClassLoader().getParent().getParent());
    }
}

ClassLoader's classloader isjdk.internal.loader.ClassLoaders$AppClassLoader@2f0e140b
The parent of ClassLoader's classloader isjdk.internal.loader.ClassLoaders$PlatformClassLoader@15aeb7ab
The Grandparent of ClassLoader's classloader isnull

最重要的机制:双亲委派模型:
1.每一个类都有一个对应它的类加载器。在类加载的时候,首先会判断这个类是否已经被加载过,已经被加载过的类会直接返回,否则尝试加载。加载的时候,首先把请求委派给父类加载器处理,因此所有的请求会传送到顶层的启动类加载器。当父类加载器无法处理时,才有自己处理。
2.为什么要使用这个双亲委派模型?
(1)可以保证程序运行的稳定,避免类的重复加载(JVM中相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证的java核心API不被篡改。
(2):不想使用:自定义加载器继承ClassLoader,然后重写findClass和loadClass()方法

连接:
1.验证:保证class文件的字节流中包含的信息符合当前虚拟机的要求,(文件格式验证),(元数据验证),(字节码验证),(符号引用验证)
2.准备:为类变量(static修饰)分配内存并设置其初始值,这里使用的是方法区的内存(1.8后再堆中)。实例变量随着对象一块分配在java堆中。十二指初始值的通常情况下是默认0值。当被static final关键字修饰就是自己赋的值。
3.解析:将常量池中的符号引用替换为直接引用的过程,使得到类或字段、方法在内存中的指针或偏移量。

初始化:
执行初始化方法的过程(clinit()编译后自动生成,自带锁保证线程安全),是类加载的最后一步。JVM才开始真正执行类中定义的java程序代码(构造器)。
初始化时机:主动引用:触发,被动引用:不触发
例如:new,读取静态字段,调用类的静态方法,进行反射,main方法的类

卸载:(即类的class对象被GC)
1.类的所有实例对象被GC
2.该类没有其他任何地方被引用
3.该类的类加载器被GC

4.对象是如何创建的,放在哪里

对象的创建

在这里插入图片描述

1.类加载检查:在遇到new指令时,首先去检查这个指令的参数是否能在常量池中定位到这个类的符号引用。检查符号引用代表的类是否已经类加载,解析,初始化过。没有,则执行类加载过程
2.分配内存:对象所需的内存大小在类加载完成后便可以确定,因此等同于把一块大小确定的内存从java堆中划分出来。分配方式有:“指针碰撞”【内存规整的情况】和“空闲链表”【堆内存不规整】
内存分配的并发问题:(1)CAS+失败重试,(2)TLAB:为每个线程预先在Eden区分配一块内存,当JVM给线程中的对象分配内存预先在TLAB分配,当不足的时候在使用CAS分配。
3.初始化零值:将分配到的内存空间初始化0值(不包括对象头),保证在java代码不附初始值的时候接直接可以使用
4.设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置,比如对象是那个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分带年龄等。还有是否启用偏向锁。
5.执行init方法,把对象按照程序员的意图进行初始化,至此一个可用的对象就完全生产出来了。

对象的布局:对象头:存储对象的自身的运行时数据(哈希码,GC分带年龄,锁状态标志),另一部分是类型指针,即对象指向它类元数据的指针,通过这个指针来确定对象是哪个类的实例。
实例部分:对象真正存储有效信息的地方
对齐填充部分:占位的作用。对象的大小要是8字节的整数倍。

对象的访问定位:java程序通过栈上的rederence数据来操作堆上的具体对象。主流的访问方式有:(1)使用句柄;(2)直接指针;

在这里插入图片描述
在这里插入图片描述

5.垃圾回收,gc

那么对象如何回收呢?(哪些需要回收,什么时候回收,如何回收)
1.堆是回收的主要区域

java自动管理内存主要是针对对象内存的回收和对象内存的分配,主要是在堆中。

堆内存分配的策略:
对象优先在eden区域,大对象直接进入老年代,长期存活的对象之将进入老年代。
1.优先在eden区分配内存,当没有足够的空间时,会发起一次minor GC
2.大对象直接进入老年代:就是大量连续的内存空间(字符串,数组),避免大对象分配内存时,由于分配担保机制带来的复制而降低效率。
长期存活的对象进入老年代:通过每个对象的对象年龄计数器,来判断什么对象进入老年代

2.gc大致种类:

1.部分收集器(Partial GC):
新生代收集器(Minor GC):只对新生代进行收集
老年代收集器(Major GC):老年代收集,当然在有的语境中也指代整堆收集。
混合收集(Mixed GC):对整个新生代和部分老年代进行收集
2.整堆收集器(Fu’ll GC):收集整个java堆和方法区。

分配担保机制
为了确保在Minor GC之前老年代本身还有容纳新生代所有对象的剩余空间。
条件成立,则进行一次Minor GC,确保是安全的,不成立通过查看参数设置值是否允许担保失败,允许则检查老年代的最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,是则尝试Minor GC(尽管有风险),小则不允许冒险,从而进行fullGC。

3.如何判断对象死亡?

1.引用计数法:为对象添加引用计数器,当有一个地方引用它,计数器+1.计数器为0的对象就是不能在被使用。(不能解决循环引用的问题)
常用方式:侵入式与非侵入性,引用计数算法的垃圾收集一般有侵入式与非侵入式两种,侵入式的实现就是将引用计数器直接根植在对象内部,用C++的思想进行解释就是,在对象的构造或者拷贝构造中进行加一操作,在对象的析构中进行减一操作,非侵入式恩想就是有一块单独的内存区域,用作引用计数器

2.可达性分析:通过GC Roots作为起点向下搜索,节点走过的路径成为引用链,当一个对象到GC roots没有引用链的时候,证明对象不可用。
GC ROOTS:
1.虚拟机栈中的引用对象
2.本地方法栈的引用对象
3.方法区中的常量引用对象,静态属性引用对象
4.被同步锁持有的对象

所谓引用:
现在细分为强弱软虚:
强引用:最普遍的引用,对象具有强引用的话就必不可少,宁愿出现OOM,是程序异常终止,也不会随意回收
软引用:对象可有可无,当内存空间不足的时候会回收,用来实现内存敏感的高速缓存。可以加速JVM对垃圾回收的速度,防止OOM
弱引用:比软引用生命周期更短,垃圾收集器一旦发现,不管内存空间是否足够都会回收
虚引用:任何时候都可能被回收,主要用来跟踪对象被垃圾回收的活动,必须和引用队列联合使用。

在这里插入图片描述

方法区主要回收无用的类:
1.类的所有实例已经被回收
2.加载该类的classloader被回收
3.该类的java.lang.class对象没有在任何地方被引用,即无法通过反射访问该类

故障的诊断

JVM调优命令:
1.jps:查看所有的java进程
2.jstat:监视虚拟机各种运行状态信息,它可以显示虚拟机进程中的类装载,内存,垃圾回收等运行时数据:
jstat -class vmid:classloader相关信息
jstat -gc vmid:显示gc堆信息
3.jmap:用于生成heap dump文件(堆转出快照),或者使用-XX:+HeapDumpOutOfMemoryError参数来让虚拟机出现OOM时自动生成dump文件
还可以查询java堆,永久代的详细信息,例如空间使用率,当前收集器是什么
4.jhat:与jmap搭配使用,用于分析dump文件,内置一个http服务器,生成dump文件分析后的结果,在浏览器中查看
5.jinfo:实时查看和调整虚拟机的各项参数
6.jstack:生成虚拟机当前时刻的线程快照,即虚拟机内每一条线程正在执行的方法堆栈的集合。目的:定位线程长时间出现停顿的原因,可以知道没有响应的线程在后台做什么事情,等待什么资源。

如何排查OOM

1.认识

除了程序计数器,都会发生OOM
栈:栈溢出
常量池:在堆中,溢出会java heap space
堆溢出:同上
方法区OOm:遇到大量动态生成的类
直接内存OOM:涉及到对内存的申请

排查方法:

1.–XX:+HeapDumpOutOfMemoryError和-XX:HeapDumpPath=/tmp/heapdump.hprof,当发生OOM时自动dump堆信息到指定目录
2.jstat查看监控JVM的内存和GC情况,先观察问题出现在什么区域
3.使用MAT工具载入dump文件,分析对象的占用情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值