JVM内存分析

JVM内存分析

经历
在面试的时候,有时候面试官会问到JVM的结构,JVM是什么,内存结构是什么?可能对于一位初学者来说就彻底懵逼了.可能你认为这些底层原理对于一位菜鸟何必要求这么高?但其实不然,我也作为一名菜鸟,当你知道JVM的结构后,才知道后面的很多问题一下子就通透了。

  1. JVM是什么?
    JVM是Java Virtual Machine(Java虚拟机)的缩写,其实就是我们常说的虚拟机。
    java之所以能够跨平台,是因为java的运行机制是,源文件经过编译后生成.源文件名字.class字节码文件,这个字节码文件可以不加修改的到其他平台的JVM进行运行。源文件 –.class文件—JVM–机器码,就实现了一次编译,多处运行

  2. JVM的结构(结构图日后补上)

    1.程序计数器
    2.虚拟机栈
    3.本地方法栈
    4.堆(heap)
    5.方法区
    6.直接内存

结构图

这里写图片描述
1.程序计数器

简介

对于初学者而言,可能这个概念都么听说过,反正我是这样的。而我也是查阅资料才知道程序计数器,我大概把它认为是一种指引,就比如说我们在上课时中途请假了,请假回来后,需要补上之前漏缺的知识点。而我们需要知道我们从哪里开始补,从哪一页开始复习。而记住复习的那一页就像程序计数器,记住那个点,如果程序中途暂停,恢复后该从哪里重新开始执行,则需要依赖这个点。(可能说的比较勉强,第一次写)

程序计数器:是JAVA中一块很小的内存,可以看作是当前线程中所执行的字节码的行号指示器。分支,循环,跳转,异常处理,线程恢复等都需要依赖这个行号指示器。
JAVA的多线程是多个线程轮流切换然后分配处理器执行时间进行的。一个内核只会执行一条线程中的一条指令。为了保证线程切换后在执行时可以到正确的位置执行,所以每一条线程独立都会建立一个独有的程序计数器,他们不会互相干扰,都是独立存储,也属于线程私有
当然这个内存区域没有规定异常的(OutMenoryError);

2.虚拟机栈
就是我们常说的栈

线程私有,生命周期与线程一样,线程结束,栈就不存在。JAVA的栈就是方法的内存模型:每一个方法的执行都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作栈,返回地址,动态连接等。
对于执行引擎来说,活动线程中只有栈顶的栈帧有效。这个栈帧所关联的方法成为当前方法。执行引擎执行的字节码指令只针对当前栈帧有效。
局部变量表
是一组变量值存储空间,存储方法的局部变量和方法的参数,JAVA的参数值传递到参数列表传递过程就是靠它完成。在非static方法中,局部变量表的第0为索引的Solt默认是传递方法所属实例对象的引用,所以可以用this访问。
它不会被赋予初始值,只有实例才会被初始化。
操作栈
和局部变量表一样,是一组以字节为单位的数组。但是他不是通过索引来访问,他有完成的栈出栈过程。对于栈来说,只要又一个值压到栈中,必然会弹出一个值可供使用。
分析一波 例如:a = 10; b = 5; a +b = 15的内存过程

begin                    本地       栈  
a = 10,b = 5a   b   a+5   0   1   2
a = 10, b = 4;    10   5        10
a = 10, b = 4;    10   5        10   5
a = 10, b = 4;    10   5        15
a = 10, b = 4;    10   5   15

end/

动态连接
JVM运行时候,会存在大量的符号引用,这个符号引用相当于方法的间接引用。例如栈帧A要访问栈帧B的方法,JVM的运行指令是会创建一个栈帧B的符号引用为参数,因为符号引用并不指向方法的真是地址,所以在调用之前还会把它转换为直接引用,才能访问到方法的真实地址。
在JAVA程序中, 如果符号引用实在类加载的第一次就转换为直接引用,那么就是静态解析。如果实在运行时被转换为直接引用那么就是动态连接。
返回地址
对于JAVA的返回来说,分为两种,一是正常退出和异常退出。在退出前,方法都会定位到当前方法执行的位置。
正常退出:会将返回值传递给上层的调用者,则调用程序计数器的值作为返回地址。
异常退出:不会有返回值传递给上层调用者,返回值有异常处理决定。
对于JVM来说,一次方法的调用就会有一次栈出栈的过程:恢复上层局部变量表和操作栈,如果有返回值,则会把返回值压到操作栈中,会把程序计数器的值作为下一条方法调用入口的指令。

关于异常
栈的角度上:如果程序所访问的栈深度大于JVM的栈深度,则会抛出StackMenoryError错误。
内存上:如果JVM可以动态扩展,那么动态扩展时,所需i要的内存申请不到足够的内存,则会抛出OutofMenoryError错误。

本地方法栈

这一点理解不多,就暂时归纳为Native服务。

直接内存

在JDK1.4后,提出基于管道和缓冲的NIO的I/O流,通过一个存储在堆里面的DirectByteBuffer 管理内存,节约效能,避免在堆和栈中来回访问数据。

堆(Heap)

其实对于堆的最大作用,就是存放所有的实例对象。是GC重点光顾的对象,也成为GC堆。所有的实例对象都需要在这里分配内存,如果出现实例无法在堆中分配内存,则会报出OutMenoryError错误。
关于堆的一系列设置,可以参考JVM调优。
因为是GC的光顾对象,可以了解到GC的收集是采用分代收集算法。在堆中分为新生代和老年代。
新生代:新创建的对象从新生代中分配内存。
老年代:在经过GC新生代后依然存活的对象会被分配到老年代中,有一些新生代会直接转入老年代,详情请参考其他资料。
一般来说进入老年代大多是大对象,无法引用外部对象的大数组。老年代可以通过启动参数进行设置大小。

如果在堆中,一个实例分配不到异常,就会报OutofMenoryError

方法区

存储类型信息,类型信息是从类加载器中提取出来。类的静态变量也存在方法区中。
方法区就是存储类型的元数据。一个.类一担要被使用,就要经过装载,连接(验证,准备,解析),初始化。而装载就是把class文件转换为方法区中的特定的数据结构。这个数据结构包括
字段信息,类型信息,方法信息,一个常量池,一个ClassLoader的指针,一直class对象的指针,静态变量。

存放实际的常量(string,iteger,floatING point 常量)的符号引用,池中的数据项象数组项一样,是通过索引访问的。
* 构建一个对象时,会在堆中为他分配空间,这个空间用来存储子类以及父类的实例属性(这些属性信息来自方法区),不仅仅是为当前的类分配空间,还会为父类进行初始化,并进行分配空间。即实例化子类时,还会为父类进行实例化为父类进行分配空间,构建子类,同时也会构建父类。从而印证:当调用当前类的构造器进行初始化时,会优先调用父类的构造器面对父类进行初始化,构建一个实例。所以,子类被实例化父类会被优先实例化。(笔试题真实遇到的)
每个类在被创建和访问的时候,都会访问元数据的信息,构建一个对象。JVM会在堆中分配内存,会在栈中存放他的引用。

类变量是被类的所有实例共享,即使没有实例化,也能访问。这些变量只会与类有关,在方法区中。JVM使用一个类时,必须为每一个non-final类分配空间。

方法区是线程安全的,假如多个线程要同时创建同一个类,而这个类还未把它装载,那么则只会允许一个线程去创建他。
方法去是大小不固定的,可以自由调整或者动态扩展。
可以被GC回收,当有些类不再被使用,不可触及便会被收回。

这种把类的元数据放在方法区,也有永久代的说法。

堆和栈的区别:

对于堆来说,非常灵活,能够存放大部分的实例对象。但是却不够安全,因为对于程序而言,我们需要动态的创建的对象,不能说先创建的对象不能被销毁,后创建的对象就可以被被销毁,假如堆中的对象被销毁了,那么我们访问实例对象会报出一个NullPointException,这就是一种错误的引用逻辑在运行才会被发现。
对于栈来说,不如堆灵活,但是属于线程私有,比较安全。栈是按照先进后出,一般来说上面的引用只要还在,那么下面的引用必然存在。对于大部分程序而言,都是先定义再引用,就像一个方法开始的时候,区块内部变量, 引用在区块时压栈,区块结束后再出栈。在整个方法结束。这种作用域很多都是用大部分的编程语言的作用域的概念了。这就是栈的优点,错误的引用逻辑在编译时会被发现。

溢出和泄漏

  • 溢出(out of memory):JVM在申请内存使用时,申请不到足够的内存供使用。
  • 泄漏(menmory leak): 程序在申请内存后,无法释放内存。发生一次泄漏没有什么影响,但是发生多次后,都不管的话,那么内存终将被消耗光。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值