深入理解java虚拟机之内存结构和内存分配

深入理解java虚拟机之内存结构和内存分配

摘要:本文主要讲解JVM的内存结构和内存分配,首先是逻辑上的内存模型,分为三大块:方法区、堆内存以及栈内存,然后是内存分配策略对象的创建/布局/访问,堆/栈的区别,JVM指令重排以及内存屏蔽的知识点,最后是对java程序内存溢出,内存泄露的讨论。hotspot虚拟机是java程序运行的平台,掌握JVM对于项目bug的解决大有裨益。

1)JVM的内存结构和内存分配?jmm

1、java内存模型:

逻辑上主要有三大块:方法区(常量池是方法区的一部分)、堆内存,栈内存

1)方法区

  1. 线程共享,存储已被虚拟机加载的 1、类信息:包括类加载器的加载;2、常量池:编译期生成的字面量【final常量】和符号引用 ;3、静态变量、编译器编译后的静态变量数据;
  2. 内存回收目标主要是常量池的回收和对类型的卸载

2)堆内存

  1. 是JVM中最大的一块由年轻代和老年代组成,所有线程共享
  2. 堆中存放被创建的实例对象、数组
  3. 堆是GC管理的主要区域
  4. 新生代(1/3堆空间) 老年代(2/3堆空间) 通过参数-XX:NewRatio来指定(JDK1.8时,只有新生代和老年代,没有持久代了)

3)栈内存先进后出

虚拟机栈:

  • 线程私有,生命周期与线程相同
  • 描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作数栈、动态连接(指向常量池)、方法返回地址等信息。
栈帧的数据 详情
1、局部变量表 局部变量:方法参数和方法内部定义的变量,局部变量表所需的内存空间在编译期间完成分配;编译期可知的8大基本数据类型 对象or数组引用(作为参数) 返回地址类型
2、操作数栈 先入后出,初始为空,在方法的执行过程中加入数据:算术运算、方法参数、方法返回值
3、动态链接 每个栈帧都包含一个指针:指向运行时常量池中–所属方法的符号引用,在运行期间将符号引用转化为直接引用称为动态链接
4、方法返回地址 正常完成出口:是执行引擎遇到一个return的字节码指令,恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,然后调用程序计数器执行后一条指令;异常完成出口:方法执行过程中遇到了异常,异常没有在方法体内得到处理,返回地址由异常处理器确定

本地方法栈:

  • 虚拟机使用到的native方法服务

4)程序计数器

  • 概念:
    1、线程私有,一块较小的内存空间
    2、当前线程所执行的字节码的行号指示器
    3、每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响

  • 作用:程序计数器指向线程下一步执行的位置

    为了线程切换后能恢复到正确的执行位置 (线程时程序运行最小的执行单位)


2、java内存分配:

  1. 基础数据类型直接在栈空间分配;

  2. 方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;

  3. 引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量

  4. 方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;

  5. 局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC 回收

  6. 方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;

  7. 字符串常量DATA 区域分配this堆空间分配;

  8. 数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小


3、如何查看JVM的内存使用情况?

1)虚拟机性能检测工具 与jvm调优相结合 个推 (结合JVM启动参数常见配置,jstat等命令)
定位项目问题时,知识,经验是关键基础,数据是依据,工具是运用知识处理数据的手段。数据包括:运行日志/GC日志/线程快照//堆转储快照等

  • jps:虚拟机进程状态工具

  • jstat:虚拟机统计信息监控工具

    jstat是用于监视虚拟机各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程的类加载、内存、垃圾回收、JIT编译等运行数据
    选项:-gcutil 最重要的参数是GC时间(YGC和FGC)次数和收集时间(YGCT和FGCT)

  • jinfo:java配置信息工具
    实时查看和调整虚拟机各项参数

  • jmap:java内存映像工具
    用于生成堆转储快照heapdump或dump,还可以查询finalize执行队列、java堆和永久代的详细信息

  • jhat:虚拟机堆转储快照分析工具,让用户可以在浏览器上查看分析结果
    使用VisualVM或专业的分析dump文件的eclipse memoryAnalyzer工具更强大

  • jstack:java堆栈跟踪工具
    用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待

2)jdk的可视化工具(Jconsole和visualVM)

  • Jconsole:java监视与管理控制台,在jdk目录下可以看见
    内存监控:内存标签页相当于可视化的jstat命令,用于监控受收集器管理的虚拟机内存的变化趋势;
    线程监控:线程标签页相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析;

  • visualVM:多合一故障处理工具,可以做到

  1. 显示虚拟机进程以及进程的配置、环境信息(jps/jinfo)

  2. 监视应用程序的cpu、gc、堆、方法区以及线程的信息(jstat/jstack)

  3. dump以及分析堆转储快照(jmap/jhat)

  4. 程序运行性能分析,找出被调用最多、运行时间最长的方法

3)递归时,jvm栈里面有一个栈帧还是n个栈帧?
栈帧的作用:用于存储局部变量表、操作数栈、动态连接(指向常量池)、方法返回地址等信息,递归时会创建n个栈帧,当递归层数过多时,会导致虚拟机出现stackoverflowError的错误


4、为什么有两个幸存空间? 解决碎片问题**

  • 降低gc频率,如果活着的对象全部进入老年代,老年代很快被填满,Full GC的频率大大增加

  • 解决碎片问题

  • Eden空间快满时young GC,频率得以降低

  • 缺点:两个Survivor,10%的空间浪费、复制对象开销

    内存释放机制:

    1、如果对象在新生代gc之后任然存活,暂时进入幸存区;以后每过一次gc,对象年龄 +1,直到某个设定的值15或直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中
    2、Eden: From Survivor : To Survivor空间大小设成8:1:1,对象总是在Eden区出生,From Survivor保存当前的幸存对象,To Survivor为空。一次gc发生后:

    • 1、Eden区活着的对象+From Survivor存储的对象被复制到To Survivor;
    • 2、清空 Eden 和 From Survivor ;
    • 3、颠倒 From Survivor和To Survivor的逻辑关系:From变To,To变From

JAVA内存模型

规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行。在 java 的内存模型中每一个线程运行时都有一个线程栈线程栈保存了线程运行时候变量值信息当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。 ***


5、JVM指令重排、内存屏蔽概念

1)什么是重排序?

  • 在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的,两个语句的赋值操作的顺序被颠倒了。常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。可以增强程序执行的能力

2)as-if-serial语义

  • 所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。

3)内存访问重排序与内存可见性

  • 计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能可以使用volatile关键字来保证内存可见性

4)内存访问重排序与Java内存模型

  • Happens-before原则:前后两个操作不会被重排序且后者对前者的内存可见**(8大原则)
原则 特点
1、程序次序法则 单线程内书写在前面的操作happen-before后面的操作
2、解锁先于锁定 同一个锁的unlock操作happen-before此锁的lock操作
3、volatile变量法则 对volatile变量的写入操作先于每一个后续对同一个变量的读写操作
4、传递性原则 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作
5、线程start方法优先 同一个线程的start方法happen-before此线程的其它方法
6、线程中断 对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码
7、线程终结 线程中的所有操作都happen-before线程的终止检测。(通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行)
8、对象创建 先初始化,后finalize;一个对象的初始化完成先于他的finalize方法调用。

5)内存屏障 Memory Barrier

  • 是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题,Java编译器也会根据内存屏障的规则禁止重排序
内存屏障的类型 特点
1、LoadLoad屏障 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕
2、StoreStore屏障 对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
3、LoadStore屏障 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
4、StoreLoad屏障 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。(在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。)
  • 为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。
    x.finalField = v; StoreStore; sharedRef = x;
    

6、heap和stack有什么区别?

从以下几个方面阐述:
1)申请方式:

  • stack:由系统自动分配 在栈中
  • heap:需要程序员自己申请,并指明大小

2)申请后系统的响应

  • stack: 只要栈的剩余空间大于所申请空间,就会分配,否则栈溢出
  • heap: 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表。寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

3)申请大小的限制

  • stack: 栈是向低地址扩展的数据结构,是一块连续的内存的区域 栈的大小固定
  • heap: 堆是向高地址扩展的数据结构,是不连续的内存区域。堆的大小受限于计算机系统中有效的虚拟内存

4)申请效率的比较:

  • stack:由系统自动分配,速度较快。但程序员是无法控制的。
  • heap: 由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

5)heap和stack中的存储内容

  • stack: 在函数调用时,第一个进栈的是主函数中的下一条指令的地址,然后是函数的各个参数
  • heap: 一般是在堆的头部用一个字节存放堆的大小

6)数据结构层面的区别

  • 这里的实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权
  • 实际上就是满足先进后出的性质的数学或数据结构

补充:

  1. 栈有一个很重要的特殊性,就是存在栈中的数据可以共享

  2. 字面值的引用与类对象的引用不同,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变

  3. String str = “abc” 的内部工作

    1、先定义一个名为str的对 String类的对象引用变量:String str
    2、在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址
    3、将str指向对象o的地址
    结论: 我们在使用诸如 String str = “abc”;的格式定义类,对象可能并没有被创建!唯一可以肯定的是,指向 String类的引用被创建了


7、对象的创建/布局/访问,以hotspot和java堆为例说明 周志明

1)对象的创建

  • 检查new指令的参数是否能在常量池中定位到一个类的符号引用,按如下步骤执行类加载:

    规划可用空间: 使用指针碰撞空闲列表方法,选择哪种分配方式由java堆是否规整决定在使用Serial、ParNew等带标记-整理过程的收集器时,系统采用指针碰撞法,在使用CMS这种基于标记-清除算法的收集器时采用空闲列表

    并发情况下线程安全的解决方法CAS配上失败重试保证更新操作的原子性,第二种:本地线程分配缓冲TLAB,内存分配完后,jvm将分配的内存空间初始化为零,以保证不赋初始值就可直接使用。

2)对象的内存布局(对象头/示例数据,对齐填充)

  1. 对象头信息(对对象进行必要的设置)
    1、Mark Word 6个: hash码、GC年龄、锁状态、持有的锁、偏向锁线程ID、偏向锁时间戳
    2、类型指针:判断对象属于哪个类的实例,指向所属类的指针

  2. 实例数据 存储真正有效数据(对象按程序员的意愿进行实例化)
    1、字段的分配策略:相同宽度的字段总是被分配到一起,便于之后取数据;
    2、父类定义的变量会出现在子类前面

  3. 对齐填充 (占位符的作用,非必须)
    经过上述步骤:从虚拟机的角度,一个新的对象已经产生

3)对象的访问定位:通过栈上的引用数据来操作堆中具体对象

  1. 使用句柄地址:二次定位
    java堆中将会划出内存作为句柄池,reference存储对象的句柄地址,通过句柄再查找对象地址
    优点:对象被移动时,只改变实例数据指针(垃圾回收时)
  2. 直接指针访问对象,reference中存储的就是对象地址
    优点:只进行了一次指针定位,节省了时间,而这也是HotSpot采用的实现方式;由于对象访问比较频繁,这个较好

2)Java程序是否会内存溢出,内存泄露情况发生?针对hotspot虚拟机。被个推面试过 技术稍微好点的都会被问到

1、什么是内存溢出?

  • 除程序计数器外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能
  • 研究内存溢出的意义:当实际工作中遇到内存溢出异常时,能根据异常的信息快速判断是哪个区域的内存溢出,什么样的代码可能导致这些区域内存溢出,以及如何处理

2、内存溢出分三种情况(堆/栈/方法区)

1)java堆空间

  • 对象创建太多超出了最大堆容量限制,且GCroots到对象之间路径可达,就会发生内存溢出异常(循环或递归中大量的new对象)

  • 一般的异常信息:java.lang.OutOfMemoryError:Java heap space

    Java堆溢出解决方案
    通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析 ,先分清是内存泄漏还是内存溢出内存溢出:实例过多、数组过大、大量的new对象;内存泄露:没被引用的对象(垃圾)过多
    若是内存泄漏: 通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象与GC Roots相关联路径。
    若是内存溢出:(对象确实还活着) 检查虚拟机的参数(-Xmx最大堆与-Xms最小堆)的设置是否适当,代码上检查是否某些对象生命周期过长。

2)虚拟机栈和本地方法栈溢出原因:(hotspot不区分两者,用-xss表示)

  • 递归太深、死循环导致栈帧创建过多(递归或无限递归) stackoverflow异常

  • 线程请求的栈深度大于虚拟机允许的最大深度,抛出StackOverflowError异常

  • 在扩展栈无法申请到足够的内存(函数体内的数组过大) 则抛出OutOfMemoryError异常

    Java堆溢出解决方案(减少内存的手段):多线程下,栈的大小越大,可分配的线程数就越少,通过减少最大堆和最大栈容量换取更多的线程

3)方法区和运行时常量池溢出

  • 方法区作用:存储已被JVM加载的类信息、常量池、静态变量等。编译器编译后的代码,线程共享

    溢出原因:CGlib字节码增强和动态语言填满了方法区(静态变量大、类加载过多)
    异常信息:java.lang.OutOfMemoryError:PermGen space (jdk1.6)
    解决方法:-XX:PermSize和-XX:MaxPermSize 设置方法区的大小 (jdk1.6)

4)本机直接内存溢出(-XX来指定)
java.lang.OutOfMemoryError抛异常时没有向操作系统申请分配资源,直接内存导致的内存溢出在dump文件中看不到明显异常
-XX:MaxDirectMemorySize指定,默认和Java堆最大值相同


3、在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些? 个推

项目中引起内存溢出的原因有很多种,常见的有以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

    举例:在后台管理商品时,要把商品的信息导入到EasyUI控件中,刚开始没有设置mysql的分页,数据量太大,从而导致内存溢出 ***

  2. 集合类中有对对象的强引用,使用完后未清空,使得JVM不能回收;

  3. 代码中存在死循环循环产生过多重复的对象实体

  4. 使用的第三方软件中的BUG;****

  5. 启动参数内存值设定的过小

    解决方案:
    第一步,修改JVM启动参数(-Xms,-Xmx),直接增加内存
    第二步,检查错误日志,查看“OOM”错误前是否有其它异常或错误。
    第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
    检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出,因此对于数据库查询尽量采用分页的方式查询。检查代码中是否有死循环或递归调用,检查是否有大循环重复产生新对象实体,检查List、MAP等集合对象是否有使用完后,未清除的问题
    第四步,使用内存查看工具 (如Memory Analyzer)动态查看内存使用情况。


4、内存泄漏的问题:

1)概念一个对象已经不需要再使用,本该被回收,另外一个正在使用的对象持有它的引用从而导致它不能被回收。停留在堆内存中,这就产生了内存泄漏
2)例如对象连接资源未关闭造成的内存泄漏,集合容器中的内存泄露,出栈时,栈中对象不会被当作垃圾回收,通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
3)如何避免:写代码时:保持对对象生命周期的敏感,特别注意单例静态对象全局性集合等的生命周期
例子1静态集合类 使用set,vector,hashmap等集合类时,当这些类被定义为静态时,由于他们的生命周期和应用程序一样长,这时候就可能发生内存泄漏。

class static test{
	private static vector v = new vector10);
	public void init(){
		for   object obj = new object();
			   v.add(obj);
			   obj = null;
	}
}

例子2:关于匿名内部类,非静态内部类会造成内存泄露的隐患。
Thread 是一个匿名内部类。

public void run() {
	while (true) {
		SystemClock.sleep(1000);
	}

例子3
监听器listener 物理连接 如数据库连接和网络连接 除非显式地关闭了连接,否则不会自动被GC回收。
单例模式对象初始化后在整个jvm生命周期中存在,持有的外部对象的引用,那么这个外部对象就不能被回收,导致内存泄漏。

有些人你永远不用爱,有哪个谈爱发了财;有些手机你永远不必等,您拨的用户比你更郁闷

发布了26 篇原创文章 · 获赞 18 · 访问量 9748

猜你喜欢

转载自blog.csdn.net/qq_28959087/article/details/86634567