JVM内存结构阐述

内存结构

程序计数器

  • 作用,是记住下一条jvm指令的执行地址

  • 是线程私有的

  • 在线程上下文切换的过程中需要记录到下一条要执行的指令的地址,等到线程再次被调度到执行的时候,还是根据该线程的程序计数器,来找到下一条要执行的指令的地址

  • 每个线程都有自己独有的程序计数器

  • 唯一一个内存不会溢出的

  • 随着线程创建而创建,随着线程销毁而销毁

栈可以说是虚拟机栈中的局部变量表

局部变量表中存放了编译期可知的各种基本数据类型,对象引用不等于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

  • 线程运行需要的内存空间

  • 栈帧(参数,局部变量,返回地址):每个方法运行时需要的内存

  • 一个栈由多个栈帧组成

  • 栈先入后出

栈的演示

 

主方法调用method1,method1调用method2

method2栈帧在栈的顶部

method1在栈的中间

主方法在栈的底部

局部变量,参数在method2栈帧内占用内存

方法结束完后一步步从顶至下弹出,占用的内存也会被释放掉

问题辨析

  1. 垃圾回收是否涉及栈内存:不需要,栈内存是一次次方法调用产生的栈帧内存,而每一次方法调用后都会被弹出栈,自动被回收掉,不需要垃圾回收来涉及栈内存

  2. 栈内存分配越大越好吗:栈内存过大会导致线程数变少,物理内存大小是有限的,假设物理内存为500M,如果栈内存为250M,能运行的线程就只有俩个

  3. 方法内的局部变量是否线程安全:局部变量是线程私有的,不会受到其他线程干扰,是线程安全的。但是给局部变量加上static修饰,就会有线程安全问题了!如果方法内局部变量没有逃离方法的作用范围,它就是线程安全的。如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。

 

三个方法 m1线程安全,m2,m3线程安全需要考虑

m2中StringBuilder作为参数(逃离了方法的作用范围)可能被别的线程访问到,需要改成Stringbuffer才能保证线程安全

m3中把sb返回(逃离了方法的作用范围)可能导致被别的线程访问到

栈内存溢出

  1. 栈帧过多导致栈内存溢出(stackOverFlowError)(递归没有退出条件)

  2. 栈帧过大导致栈内存溢出

线程运行诊断

  • 用top(linux命令)定位哪个进程对cpu占用过高

  •  ps H -eo pid,%cpu | grep 进程id

    (用ps命令进一步定位是哪个线程引起的cpu占用过高)

  • jstack进程id (根据线程id找到有问题的线程)

本地方法栈

 

java中有时候不能与操作系统直接交互,需要本地方法接口(c,c++编写的)与操作系统更底层的api来实现交互

堆:通过new关键字,创建对象都会使用堆内存,是java虚拟机所管理的内存中最大的一块,此内存区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,堆是垃圾收集器管理的主要区域

  • 它是线程共享的,堆中对象都要考虑线程安全

  • 有垃圾回收机制

堆内存溢出(java.lang.OutofMemoryError:java heap space)

堆内存诊断(idea Terminal中输入命令)

  • jps:查看当前系统有哪些java进程

  • jmap -heap (进程id) 检查java堆内存占用

  • jconsole 图形化界面监视和管理控制台

  • jvisualvm 可视化虚拟机

方法区

随着虚拟机启动时创建

方法区与堆一样是各个java虚拟机线程共享的一块区域

它存储了跟类的结构相关的一些信息

类的成员变量,常量,静态变量,方法数据,以及成员方法,构造器方法,特殊方法的代码部分等数据,虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来

永久代:hotspot虚拟机1.8以前对方法区的称呼

方法区内存溢出

  • 1.8以前导致永久代溢出

  • 1.8以后会导致元空间溢出

 

场景:

  • spring

  • mybatis

  • 动态代理

    在运行时生成类导致内存溢出

运行时常量池

 

常量池:就是一张表,虚拟机指令可以根据这张表找到要执行的类名,方法名,参数类型,字面量等信息

运行时常量池:运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放,并把里面的符号变为真实地址

JDK1.8 StringTable(字符串常量池(运行时常量池的一部分))是存在堆中的

jdk1.6即以下版本 StringTable是在永久代中

最重要一点,StringTable中存储的并不是String类型的对象,储存的而是指向String对象的索引,真实对象还是储存在堆中。

 

s1+s2是变量,在运行中可能引用的值被修改,结果不能确定,所以必须在运行期间动态的用StringBuilder进行字符串拼接,而s5是常量在编译期就已经能确定好,不需要StringBuilder方式拼接

字符串是延迟称为对象的,即执行到哪一行才会在字符串常量池中放入那一行

了解字符串常量池StringTable案例

 

String.intern()是一个Native方法,它的作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个对象的String对象,否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用

这里一开始将x放入了字符串常量池,然后new了一个s放在了堆中

s.intern将s放入串池,但是串池中已经有了“ab” 则不会放入串池,只会返回串池中的对象 所以s2 == x(true) 因为s2是串池中返回的对象 与常量池中的ab相等,s==x为false 常量池已经有了“ab”所以没把s放入串池中,还是存在堆中 (jdk1.8).

 

s2==x(true)

s== x(true) 因为s.intern()方法将s放入了字符串常量池(串池中没有“ab”)

如果是jdk1.6是将s拷贝,结果又会不同了,这里就不进行详细阐述了

 

x1 == x2 为false 因为常量池已有cd x2.intern()没有将x2放入常量池成功,x2.intern()的返回对象才会与 x1 相同

StringTable调优

调整 -XX:StringTableSize=桶个数

考虑将字符串对象是否入池

如果字符串很多 可考虑入池

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存频繁地被使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里进行讲解

在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channle)与缓冲区(Buffer)的的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆中和Native堆中来回复制数据

显然,本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存以及处理器寻址空间的限制,服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx等参数信息,但经常忽略直接内存,使得各个内存区域综合大于物理内存限制,从而导致动态扩展时OutOfMemoryError异常。

  • 常见于NIO操作,用于数据缓冲区

  • 分配回收成本高,但读写性能高

  • 不受jvm内存回收管理

分配和回收原理:

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法

  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放内存

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>