JAVA后端知识点碎片化整理 基础篇(十二) 认识JVM
目录
(7)为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
(13)JAVA对象的内存布局!!一个空Object对象的占多大空间?
(16)如何理解内存泄漏问题?有哪些情况会导致内存泄露?如何解决?
(一)什么事JAVA虚拟机为什么要
java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现。java虚拟机有自己想象中的硬件如处理器、堆栈、寄存器还有响应的指令系统。屏蔽了及具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。
java虚拟机进程启动就绪,然后由虚拟机中的类加载器加载必要的class文件,包括jdk中的基础类(如string和Object等)然后由虚拟机进程解释class字节码指令,把这些字节码指令翻译成本机CPU能识别的指令,才能在cpu上运行。
(二)JVM的体系架构与生命周期
JVM的体系架构:JVM包括类装载子系统、运行数据区、执行引擎。类装载器子系统根据给定的权限的名来装入类型。执行引擎负责执行哪些包含在被装载类的方法中的指令。运行时数据区包含方法区、堆、Java栈、PC寄存器、本地方法栈。
类装载子系统:在JVM中负责查找并装载类型的那本分被称为类装载子系统,JVM有两种类装载器,启动类装载器和用户自定义的类装载器。步骤如下(1)装载:查找并装载类型的二进制数据 (2)连接:执行验证、准备以及解析 验证——确保被导入的类型的正确性、准备——为类变量分配内存,并将其初始化为默认值。解析——吧类型中的符号转换为直接引用。(3)初始化:把类型变量初始化为正确的初始值。
方法区:关于装载类型的信息被存储在 一个逻辑上被称为方法区的内存中,类的类变量同样存储在方法区中。所有线程共享方法区,因此,他们对方法去的数据访问必须被设置成线程安全的。JVM方法区中储存了1、类型的全限定名;2、这个类型的超类的全限定名;3、这个类型是接口还是类类型等 除此之外,JVM还为每个被装载的类型存储以下信息,1、该类的常量池,就是该类型所用常量的一个有序集合,包括直接常量和对其他类型、字段、方法的符号引用。2、字段信息、3、除了常量外的所有类的静态变量。(hotspot用永久带实现方法区)
堆:一个Java虚拟机实例中只存在一个堆空间,因此所有的线程都共享这个堆。又由于一个Java程序独占一个JVM实例,因此每个JAVA程序都有自己的堆空间,他们不会彼此干扰,但同一个java程序的多个线程共享一个堆空间。所有的实例对象都在这上面进行分配,也是GC管理的重要区域。(堆还可以细分新生代和老年代,新生代又可以分为eden 、form Survivor和to Survivor空间等)
程序计数器:每个线程都有自己的PC寄存器,他的线程启动时创建,大小是一个字长,当执行本地方法时,PC寄存器内容是是吓一跳被执行指令的地址。(当前线程执行的字节码的行号显示)
JAVA虚拟机栈:每当启动一个新的线程时,JVM都会为它分配一个Java栈。Java栈是保存线程的状态,JVM只会对其进行入栈和出栈,每当调用一个JAVA方法时,虚拟机都会在该线程的Java栈压入一个新帧,来存储参数、局部变量、中间运算结果。Java栈上所有的数据都是这个线程所独有的。
本地方法栈:本地方法栈实际上依赖于JVM实现的,设计者可以根据自己需求使用Java程序调用本地方法。
执行引擎:在Java虚拟机规范中,执行引擎的行为使用指令集来定义。
JVM的生命周期:java虚拟机就是用来执行java程序,一个java程序一个java虚拟机。程序开始执行的时候才运行,程序结束的时候他就停止。你再同一台机器上运行3个java程序,那么就有3个java虚拟机运行。Main()是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。
Java线程分为两种。守护线程与普通线程。守护线程是jvm自己使用线程,他负责垃圾回收GC线程就是一个守护线程,java虚拟机上只要还有普通贤臣挂在运行,java虚拟机就不会停止,如果有足够权限,可以使用exit终止程序。
(三)说一说java的内存区域
与上面可能稍有重复 ,但是详细一点,
java虚拟机执行java程序时,会把他所有的内存划分为若干个不同的数据区域。运行时数据区域,线程共享区(方法区、堆)、线程隔离区(虚拟机栈、本地方法栈、程序计数器)
程序计数器:JAVA虚拟机栈:本地方法栈:JAVA堆:方法区:如上
(4)JVM选择基于栈的架构的原因
JVM执行字节码指令是基于栈的架构的,所有的操作数必须先入栈,然后根据指令的操作码选择从栈顶弹出若干个元素进行计算后再将结果入栈。JVM操作数可以存放在每一个栈帧中的一个本地变量中,即每个方法调用时就会给这个方法分配一个本地变量集,这个本地变量集在编译时就已经确定,所以操作数入栈可以直接是常量或者从本地变量集中娶一个变量压入栈中。 JVM基于栈的设计理由是
(1)JVM要设计成与平台无关的,而平台无关性就要保证在没有或者由很少的寄存器的机器上也能同样正确执行java代码,因为寄存器很难做到通用。
(2)基于栈的理由是为JVM更好地优化代码而设计的
(3)为了指令的紧凑性,因为java代码可能在网络上传输,所以class文件的大小也是设计JVM字节码指令的一个重要因素。
(5)Java虚拟机中,数据类型可以分为哪几类?
java虚拟机中数据类型可以分为:基本类型和引用类型,基本类型的变量保存原始值,即他代表的值就是数值本身;而引用类型的变量保存引用值,并不是对象本身。
基本数据类型:byte、short、int、long、char、float、double、Boolean
引用类型包括:类类型和接口类型和数组。
(6)怎么理解栈、堆?堆中存什么?栈中存什么?
栈内存,是一片内存区域,存储都是局部变量,变量有自己的作用域,一旦离开作用域,变量就会被释放,栈内存更新速度很快,因为局部变量的生命周期很短。(基本数据类型、对象的引用)
堆内存:存储的数组和对象,凡是new建立的都在堆中,堆中存放的都是实体对象,用于封装数据,这个对象并不会被随时释放,会依据GC的规则不定时的收取。(对象)
堆中都是对象,栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,在栈中为了节省空间,用一个引用来表示其在堆上地址。
(7)为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
(1)从软件设计的角度,JVM栈代表了处理逻辑,而堆代表了数据,这样分离,使得处理逻辑更为清晰,这种隔离、模块化的思想在软件设计的方方面面都有体现。
(2)堆与JVM栈的分离,使得堆中的内容可以被多个 JVM栈共享,一方面提供了一种有效的数据交互的方式,如内存共享。另一方面JVM堆中共享常量和缓存可以被所有的JVM栈访问,节省了空间。
(3)堆和栈的完美结合就是一个面向对象的实例,面向对象的引入是的对待问题思考的方式发生了改变,更接近于自然的思考方式,把对象拆分开可以发现对象的属性就是数据,存放在堆中。对象的方法村里逻辑其实还是在栈中。
(4)JVM栈智能向上增长,JVM中对象可以根据动态增长。因此,堆和JVM栈分离使得动态增长成为可能,相应的JVM栈只需要记录一个地址即可。
正是JVM栈都是程序运行的根本,堆是为JVM栈提供数据服务的。简单来说,堆就是共享的内存即节省了空间也提供了一种数据交互的可能,因为堆与JVM栈分离的思想也使得JVM的垃圾回收成为可能。(面向对象就是堆栈的完美结合)
(8)java中什么是栈的起点,同时也是程序的终点
栈的特性,先入后出。程序要运行总有一个起点的,同c语言一样,java中Main就是那个起点。无论什么java程序,找到main到找到了程序执行的入口。 堆中存的对象,栈中存的是基本数据和堆中对象的引用。
(9)为什么不把基本类型放在堆中
首先一个对象的大小是不可估计的,或者说是动态变化的,所以栈中存放对象的引用。
那么为什么不把基本类型放在堆中,因为一般其占据的空间时1-8个字节,需要空间比较少,而且因为是基本类型所以不会出现动态增长。
(10)Java参数传递时是传值还是传引用
程序运行时只传递基本类型与对象引用的问题,不会直接传对象本身。但传引用的错觉是如何造成的呢?在运行栈中,基本类型和引用的处理是一样的,都是传值,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理和基本类型完全一致,进入调用方法,被传递引用值被程序解释到堆中对象,这个时候对应真正的对象,而不是引用本身。
JAVA的参数传递就是传值,传对象也只是传入对象引用这个地址的值。方法内对这个对象的修改就是修改对象本身,而不是改变地址。
(11)Java中有没有指针的概念?
c/c++中指针是指向地址中内存,改地址就是存储变量的地址。但实际上java里面的指针无处不在,在s1与s2同时指向一个对象,我们知道里面创建一个对象存放在堆中,当我们new一个对象的时候其实是堆中开辟一个存储该对象的空间,返回是存储该对象的地址,所以java中我们所有的引用就是所谓的指针。
举例A a1=new A(); A a2 = a1;这时候并没有开辟所谓新的空间,只是将java里面创建的一个对象存放在堆上,当我们new一个对象的时候才是开辟一个存储该对象的空间,第二段只是将a2指向相a1的地址。
(12)常用JVM参数设置
-Xss256K 设置每个线程运行时栈的大小为256K
-Xmx 设置JVM最大内存,比如-Xmx512M:设置最大内存为512M 堆的大小也有限制,系统的数据模型限制(32bit或者64bit)、系统可用的虚拟内存限制、系统可用的物理内存限制。
-Xms 设置JVM最小内存,比如-Xms512M :设置最小内存为512M
-Xmn 设置JVM年轻代内存,比如-Xmn1G,设置年轻代内存为1G;
例如启动虚拟机时候编译一个jar包 java -Xss512K -jar xxx.jar
(13)JAVA对象的内存布局!!一个空Object对象的占多大空间?
首先JAVA对象的内存布局:对象头(Header)(32位8byte 64位16byte)、实例数据(Instance Data)和对其填充(Padding)。
一、对象头:对象头存储对象自身运行时的数据,
1、Mask Word(在32bit和64bit虚拟机上长度分别为32bit与64bit)包含如下信息 有 。对象的hashcode 。对象的GC分代年龄 。对象的锁状态(轻量级锁、重量级锁)。线程持有的锁(轻量级锁与重量级锁)、偏向锁相关(固定大小)
2、类型指针:对象指向元数据的指针(32bit和64bit分别为32bit与64bit) JVM通过这个指针来缺点这个对象是哪个类的实例。(确定对象的类型)
二、实例数据:对象真正存储的有效信息、reference引用类型在32位系统上每个占用4bytes,在64位系统占用8bytes
三、对其填充,JVM要求对象大小必须是8的整数倍数,若不是对其补充
那么Object obj = new Object();到底大小是多少多少空间呢。首先obj是对象的引用,这个长度决定了java的寻址鞥能力,32JDK是4个字节,64bit是JDK的8个字节。这个是引用,在new100个Object对象,用虚拟机可视化软件查看jvm堆上空间存储的大小,发现增加了1600byte,也就是说明每个Object对象都会增加16个byte,因为Object本身也含有一些方法。
(14)!!Java对象的四种引用类型(强 软 弱 虚)
这一段写过了,再敲一遍纯当复习。这些引用的的类型的存在都是为了帮助垃圾回收器管理JAVA内存
(1)强引用:时最普遍的引用,如果一个对象具有强引用,那么垃圾回收器绝不会回收塔,当空间内存不足,JAVA虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随机回收具有强引用的对象来解决内存不足的问题。(我们A a = new A();就是强引用)。(坚决不回收系列,拒绝凉凉)
(2)软引用(构建缓存):当一个对象只具有软引用的时候,内存空间足够垃圾回收器就不会回收它,如果空间内存不足,就会回收这些对象的内存,只要垃圾回收期没有回收它,该对象就可以被程序所使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列联合使用。例如通过一个hashmap维护一个软应用表,如果内存足够那么可以通过get方法直接获取java对象的强引用,另外一旦垃圾线程回收java对象后,get方法就返回null,容易操控,适合实现缓存。(内存不够用了GC,就会凉凉)
(3)弱引用:弱引用对象拥有更短暂的生命周期,在垃圾回收器扫描它所管辖的内存去榆中,一旦发现只具有弱引用对象,不管当前内存是否足够,都回收它的内存。不过由于垃圾回收器是一个优先级很低线程,因此不一定很快发现只有弱引用的对象。(创建后第二次GC只要被发现就凉了系列)
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null 弱引用的get不一定是null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
(4)虚引用:顾名思义,形同虚设,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么他就和没有任何引用一样,任何时候有可能被垃圾回收器回收,通过虚引用的get方法获得到的语句用于都是null,检测对象是否被删除。(表示自己一直都是凉凉)
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null 虚引用的get一定是null
pf.isEnQueued();//返回是否从内存中已经删除
(15)如何进行JVM调优,有哪些办法?
工具:Jconsole,jProfile,VisualVM
Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
JProfiler:商业软件,需要付费。功能强大。
VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。
如何调优:
堆信息查看:1查看堆空间大小分配(年轻代、年老代、持久代分配)2、提供即时的垃圾回收功能 3、垃圾监控(长时间监控回收情况)4、打开堆Dump,查看堆内类、对象信息等
解决:年老代年轻代大小划分是否合理,内存泄露、垃圾回收算法是否合理
线程监控:系统线程数量、查看各个线程状态,可以检查死锁等、Dump线程查看线程内部运行情况、
快照DUMP:快照是系统运行的某一个时刻的一个定个,在我们进行调优的时候,不可能用眼睛来追踪所有的系统变化,依赖快照功能,我们可以进行系统两个不同时刻,对象的不同,以便快速找到问题。(比如垃圾回收前后分别快照一次,对比两次快照的对象情况)
(16)如何理解内存泄漏问题?有哪些情况会导致内存泄露?如何解决?
内存泄露一般可以理解为系统资源(各方面的资源、堆、栈、线程等)在错误使用的情况,导致使用完毕的情况无法回收,导致新资源的分配请求无法完成,引起系统错误。(内存泄露用完的资源没有回收引起错误)
1、年老代堆空间被占满;java、lang。OutofMemory Java heap space ,随着时间的推移,系统的堆空间不断占满,最终会占满真个堆空间。因此可以初步认为系统内部可能是内存泄露。
解决:这个方式通过垃圾回收前后对比,同时根据对象引用情况分析,基本都可以找到泄漏点。
2、从持久带被占满:java.lang.OutOfMemeoryError PermGen space Perm被占满无法为新的class分配存储空间引发的异常,随着反射的大量使用这个异常也很常见,不同的classLoader即便使用相同的类,但都会对其进行加载,相单于同一个东西,如果有N个classLoader那么他将被加载N次。
3、堆栈溢出 java.lang.StackOverflowError 递归没返回或者循环调用。
4、线程堆栈满 Fatal Stack size too small java中一个线程的空间是由大小限制的,这个线程相关的数据将会保存在其中,但是线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小,这个配置不是根本解决,要根据代码部分是否造成内存泄露。
5、系统内存被占满 java.lang.OutOfMemoryError 这个异常是由于操作系统没有足够的资源来产生这个线程造成的,系统创建线程时,除了要在java堆中分配内存中,操作系统本身也需要分配资源来创建线程。(操作系统内存不足)
解决:重新设计系统减少线程数量,线程数量不能减少的情况下,减小单个线程的大小。