JVM的理解(从小白到大白)

一、jvm在java平台的位置

在这里插入图片描述

  1. 最下面是平台,JRE是java运行时的环境(就是一些类库)
  2. JDK是java开发工具包,包含JRE,并且还包含一些开发常用的工具,例如编译的一些命令,jvm调试的一些工具等等
  3. JVM就在平台的上一级,包含在JRE中,当然也包含在JDK中

二、简述java程序的执行过程

  1. 首先写出的是java源文件(.java)
  2. 然后是经过java编译器编译(javac命令),生成的与平台无关的字节码文件(.class),该字节码文件只需要JVM能理解即可
  3. 此时将字节码文件交给java虚拟机(JVM),通过java解释器翻译字节码文件,将每一条指令翻译成不同平台下的机器码(10111000)
  4. 机器码在特定的平台上运行

三、 关于JVM的几点说明

  1. 1995年诞生,实现 write once , run everywhere(一次编译,到处运行)
  2. 不同的平台下需要下载对应的JVM
  3. 从软件层面屏蔽了底层硬件和指令层面的细节(同一个功能在linux和windows是不同的命令指令:例如打开一个文件等等)
  4. 学习JVM的目的:查bug(内存溢出、内存泄漏等等)、面试、性能优化
  5. JVM的优势:在java中人们只需要关注业务,而不用过多的关注内存管理(分配空间、回收空间等等),这部分交给JVM来管理。

四、简述JVM的执行过程

  1. 加载.class文件
  2. 管理并分配内存
  3. 执行垃圾回收

五、JVM运行时数据区(GC也在这)(重点研究)

在这里插入图片描述

  1. 程序计数器:当前线程正在执行的字节码指令的地址(行号),是线程独享的,不会出现线程安全问题。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来实现。
    1)既然正在执行,为什么要存起来?因为很多线程都在抢夺cpu的时间片,当前正在执行的线程没有执行完会被挂起,当下一次该线程抢到了时间片的时候就会从这个程序计数器中读到执行地址
    如果线程执行的是一个java方法,记录的就是字节码指令的地址
    如果线程执行的是一个native方法,这个计数器则为空(Undefined)

  2. 虚拟机栈:存储当前线程运行方法时所需要的数据、指令、返回地址
    1)程序中写的方法是由线程来执行的
    2)当前很多虚拟机栈可以设置栈深度固定值(当前线程请求的栈深度大于虚拟机设定所允许的栈深度会报栈溢出),也可以动态扩展(无法申请足够的内存会报内存溢出)
    3)是栈–》是数据结构–》用来存数据的–》存什么数据呢?
    4)一个方法对应一个栈帧(Stack Frame),一个栈帧中不止这4部分还有其余东西,但是学习注意深度,挖深了有没有必要需要自己衡量。
    在这里插入图片描述
    局部变量表
    方法中定义的变量,其中的每行记录都是32位定长的,如果是long,double类型的64位需要占2个局部变量表的空间(slot),其余的数据类型只占1个。
    当进入一个方法的时候,方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行的时候不会改变局部变量表的大小。
    操作数栈
    存储中间变量,中间结果等等
    (1)找到字节码文件,进行分解一下
    javap是 Java class文件分解器,可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码。用于分解class文件,javap -c a.class>a.txt在这里插入图片描述
    (2)源码与字节码分解文件进行比较在这里插入图片描述
    拿这个int j=0来说,对应了2条操作,分别是0:iconst_0(意思是:将int类型的常量0压入操作数栈)和1:istore_2(意思是:将int类型的值存入局部变量表中下标为2的位置)
    问题:为什么不存放在0位置和1位置呢?
    我们看到方法中传进一个int i,那么可能会是这个吗,来通过int sum = i+j验证一下,因为这里用到了传入的int i
    在这里插入图片描述
    iload_1 从局部变量1中装载int类型值
    iload_2 从局部变量2中装载int类型值
    都是装载到操作数栈中,由此可知局部变量的1号位置存的是方法的传入参数i,但是0号位置是什么呢?如果该方法是成员方法,那么0号位置放的this,如果不是成员方法存储的是空,例如静态方法,完全不需要this这个当前对象,因为可以直接通过类调用。

这里顺便说一个大家常说的栈指向堆
堆中就是一个一个的实例对象,每次new的时候就会在堆中开辟空间,生成这个对象的一些内容。
在这里插入图片描述
就是在虚拟机栈的一个栈帧(一个方法)中的局部变量表中定义这个对象的引用,这个引用就是局部变量表中的一条记录,里面的内容存的时堆中该对象的地址,因为这里的例子是该方法中局部变量 Object acb = obj; 而这个obj是成员变量 Object obj = new Object();
这个指向还有2种方式:一种是直接指向,另一种是指向句柄池,这是因为jvm的版本不同,不同的jdk版本可能有不同的jvm

再说一个问题:在一个方法中调用另外一个方法是怎样的呢?还有种特殊的重复调用自己本身叫递归,那么递归呢?
首先会将调用的那个方法压入虚拟机栈形成一个栈帧,但是递归的情况是压入1个栈帧还是压入n个栈帧呢?我们知道虚拟机栈是有固定大小的,我们验证一下会不会爆掉
在这里插入图片描述
爆掉了,栈溢出了,由下图说明递归是放n个栈帧。当前很多虚拟机栈可以设置固定值(当前线程请求的栈深度大于虚拟机设定所允许的栈深度会报栈溢出),也可以动态扩展(无法申请足够的内存会报内存溢出)
在这里插入图片描述
思考问题:每个栈帧的大小是不是一样大小的呢???
不一样,因为局部变量表不同和操作数栈不同。应该在编译时就确定了大小。
当JVM执行一个方法时,它会检查class中的数据,以便确定一个方法执行时在局部变量表和操作数栈中所需存储的word size。然后,JVM会为当前方法创建一个size相对应的栈帧,然后把它push到栈顶。

动态链接
java的动态特性,运行时多态,面向接口编程的时候,一个接口可以有多个实现类,通过接口来实例化一个该接口的实现类的对象,此时通过这个引用去调用实现类中的方法的时候就会动态解析成对应实现类的对象调用的方法。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段(解析)或第一次使用的时候转化为直接引用(如final、static域等),称为静态链接,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态链接。
问题:动态链接为什么放这里???支持方法调用过程中的动态特性

出口
方法的出口,正常的是return,异常的有error和exception,exception需要有解决方式是try/catch还是throws

  1. 本地方法栈:本地方法是用c和c++语言写的,可以类比虚拟机栈,主要处理native修饰的方法,而虚拟机栈主要处理java方法。
    有的虚拟机(譬如Sun公司的HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。
    在这里插入图片描述
    在这里插入图片描述
  2. 方法区(MethodArea):存储的是类信息(也可以叫类元信息,可以简单理解为class文件)、常量(1.7+有变化)(final)、静态变量(static)、JIT(1.7以前)编译时的一些代码
    1)通过方法区拿到类信息,生成的一个一个的实例是放在堆中的,通过实例去调用方法(通过线程去调用方法),方法又加载到虚拟机栈中的栈帧中
    2)字符串常量在1.6及以下是放在方法区中,但是1.7及以上放到了堆中。
    3)对于hotspot虚拟机,jdk1.8及以上称为元空间(一块独立的内存空间)实现方法区,jdk1.8之前叫永久代实现方法区,永久代和元空间都是方法区的实现方式而已,有的虚拟机中的方法区不是用永久代实现的(如BEA JRockit、IBM J9 等,但常用热门的Sun hotspot是用永久代实现的,但现在改为元空间实现了)。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。
    4)java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。
    根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。
    5)运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量1和符合引用2,这部分内容将在类加载后存放到方法区的运行时常量池中。
    java虚拟机对class文件中的每一个部分(当然包括常量池)的格式都有严格的规定,这样才能被虚拟机认可装载和执行。但对于运行时常量池,java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需求来实现这个内存区域。不过,一般来说,除了保存class文件中描述的符合引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
    运行时常量池相对于class文件中的常量池的另外一个重要特征是具备动态性,java语言并不要求常量一定只能在编译期产生,也就是并非提前放到class文件中的常量池里的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的便是String类的intern()方法。
    既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

关于共享数据问题稍作总结:
下面是逻辑图因为线程只负责干活,不负责存数据。绿色的三部分是线程独有的,不会有线程问题,但是存数据的方法区和栈是共享的。
在这里插入图片描述

  1. Heap(堆)
    关于堆的几点说明
    Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
    (1)java堆是垃圾收集器管理的主要区域,现在的收集器基本都采用分代收集算法,所以分为新生代和老年代。
    (2)分代的好处是优化了GC的性能,如果不分代所有对象放一块,gc的时候需要全部区域扫描非常耗时效率低,但分代后我们先去朝生夕死特点的对象较多的eden区回收,就能腾出大量空间,就可以大大提高效率!

在这里插入图片描述
(1)新生代和老年代的大小默认比例是1:2
(2)新生代又分为3个区(1个eden区和2个Survivor区):eden区(伊甸园:亚当和夏娃的出生地,所以也是对象初始的地方)、s0区、s1区,大小默认比例是8:1:1,s0和s1区都叫Survivor区,也有叫from Survivor区和to Survivor区。

如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread(线程) Local(本地) Allocation(分配) Buffer(缓冲区),TLAB)

无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

问题来了:为什么是8:1:1,而不是其余的比例呢?
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。(解释一句:就是朝生夕死的对象占到80%以上,80%以上的对象用完直接完全回收掉,所以只有不到20%的对象会利用复制回收算法到survivor区中,所以这个比例充分的提高了内存的利用效率。)

发生minor gc 会将form 和to 调换! 但是第一次from为空的时候不需要调换,只需要把幸存的对象从eden区复制到from区即可,从第二次minor gc开始,需要将eden区和form 区中的幸存对象送往to区,然后再调换from 和 to,使得to区总是为空,目的是为了避免内存碎片化。form区gc时其实有2个方向:一个方向是to区,一个方向是老年代(达到一定年龄(默认是15次),也就是该对象幸存次数,也可以说该对象经历minor gc 的次数,还可以说该对象来回复制的次数)。
Minor GC会一直重复这样的过程,直到“To”区装不下了,“To”区装不下之后,会将所有对象移动到年老代中。
在这里插入图片描述

(3)一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中

例子解释:一个对象的这一辈子
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我16岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

(4)有关年轻代的JVM参数
①-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
②-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
③-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
④-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

HotSpot VM,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。它名称中的HotSpot指的就是它的热点代码探测技术,(其实两个VM基本上是同时期的独立产品,HotSpot还稍早一些,HotSpot一开始就是准确式GC,
而Exact VM之中也有与HotSpot几乎一样的热点探测。
为了Exact VM和HotSpot VM哪个成为Sun主要支持的VM产品,在Sun公司内部还有过争论,HotSpot打败Exact并不能算技术上的胜利),
HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。
如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。
通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序,
即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
在2006年的JavaOne大会上,Sun公司宣布最终会把Java开源,并在随后的一年,陆续将JDK的各个部分(其中当然也包括了HotSpot VM)在GPL协议下公开了源码,
并在此基础上建立了OpenJDK。这样,HotSpot VM便成为了Sun JDK和OpenJDK两个实现极度接近的JDK项目的共同虚拟机。
在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。
Oracle公司宣布在不久的将来(大约应在发布JDK 8的时候)会完成这两款虚拟机的整合工作,使之优势互补。
整合的方式大致上是在HotSpot的基础上,移植JRockit的优秀特性,譬如使用JRockit的垃圾回收器与MissionControl服务,
使用HotSpot的JIT编译器与混合的运行时系统。

(4)在hotspot VM(现在用的较多的)中新生代和老年代就是堆,永久代就是方法区,下图用相同的颜色进行了分类。
在这里插入图片描述
(5)分析堆中运行过程:
通过在一个方法中,先new一个8M的空间,在new一个1M的空间,再new一个8M的空间来分析验证

public void m(){
//Object obj1 = new 8M();
//Object obj2 = new 1M();
//Object obj3 = new 8M();
}

问题:如何new一个1M的空间?
1MB(兆)=1024KB(千字节)
1KB=1024Byte(字节)
1Byte=8bit(位或比特)
通过int数组的方式:
一个int类型的是4byte=4*8=32bit,
1M=1024*1024*8bit=1024*1024*8/32 个int = 1024*256 个int

public static int[] generate1M(){
return new int[1024*256]
}

在这里插入图片描述
当new第一个8M对象的时候,先放到了eden区,然后再new1M的时候,询问eden区,发现装不下,此时会触发ygc(也叫minor gc),将第一个8M的对象往from区移动,发现from区只有1M的空间,放不下,此时触发担保机制,将这个8M的对象放到老年代中,此时那个1M的对象可以放到eden区了。
发生完了ygc后,from区和to区对调(s0区由from变为to,s1区由to变为from)!为什么对调???(其实就是为了每次开始复制之前一直使to区为空,也是为了解决内存碎片化问题)
此时第二个8M的对象来了,先问eden区,但是此时只有7M,放不下,此时又需要触发ygc/minor gc,将1M的对象放到s1区(此时是from),这时可以将第二个8M对象放到eden区了。

新生代用的是复制回收算法,老年代用的标记整理算法,这两种算法都不会产生内存碎片(因为都是移动,而不是部分删除,像标记删除算法就会产生内存碎片)

eden区触发minor gc是会被完全放空的,再将新的对象放进来

通过jconsole来启动java监视和管理控制台来查看这几个区的变化情况,步骤如下:
步骤1:先运行一个java进程
在这里插入图片描述
步骤2:gitShell中输入jconsole启动,选择本地的刚运行的那个进程
在这里插入图片描述
步骤3:查看eden区,关注底下绿色的中间那个就是eden区,空间不够用的时候,会放空,再放入新的
第一个绿色的柱子是老年代,鼠标放上面会显示的
在这里插入图片描述

担保机制:老年代会为新生代的from区和to区提供担保,如果from区和to区放不下,由老年代来放。如果老年代担保不起,空间不够,那么老年代也需要gc,但是这个gc分多种情况,比如说会挪一部分过去,而不是所有的都移动。

一个gc日志:
在这里插入图片描述
有的是gc是系统内部的,上面那些full gc,更多关注那些allocation failure
Allocation Failure:
表明本次引起GC的原因是因为在新生代中没有足够的空间能够存储新的数据了。
参考:https://blog.csdn.net/zc19921215/article/details/83029952

Minor GC ,Full GC 触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

jdk1.8及以上:
在这里插入图片描述
最大变化:永久代变成了元空间,元空间是自扩展的空间,相当于ArrayList,防止了永久代的溢出
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
在这里插入图片描述
关于直接内存的介绍:
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。

在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native(本地) 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

六、各种溢出例子演示

  1. java堆溢出
    下面的程中我们限制Java 堆的大小为20MB,不可扩展(将堆的最小值-Xms 参数与最大值-Xmx 参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump 出当前的内存堆转储快照以便事后进行分析。
    参数设置如下:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
package com.yhj.jvm.memory.heap;
import java.util.ArrayList;
import java.util.List;
/**
 * @Described:堆溢出测试
 * @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails
 * @author YHJ create at 2011-11-12 下午07:52:22
 * @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java
*/

public class HeapOutOfMemory {
 /**
 * @param args
 * @Author YHJ create at 2011-11-12 下午07:52:18
 */

    public static void main(String[] args) {
       List<TestCase> cases = new ArrayList<TestCase>();
       while(true){
           cases.add(new TestCase());
       }
    }
}

/**
 * @Described:测试用例
 * @author YHJ create at 2011-11-12 下午07:55:50
 * @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java
 */

class TestCase{
 

}

在这里插入图片描述
Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。
要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如EclipseMemory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

  1. java栈溢出
package com.yhj.jvm.memory.stack;
/**
 * @Described:栈层级不足探究
 * @VM args:-Xss128k
 * @author YHJ create at 2011-11-12 下午08:19:28
 * @FileNmae com.yhj.jvm.memory.stack.StackOverFlow.java
 */

public class StackOverFlow {
    private int i ;
    public void plus() {
       i++;
       plus();
    }
    /**
     * @param args
     * @Author YHJ create at 2011-11-12 下午08:19:21
     */

    public static void main(String[] args) {
       StackOverFlow stackOverFlow = new StackOverFlow();
       try {
           stackOverFlow.plus();
       } catch (Exception e) {
           System.out.println("Exception:stack length:"+stackOverFlow.i);
           e.printStackTrace();
       } catch (Error e) {
           System.out.println("Error:stack length:"+stackOverFlow.i);
           e.printStackTrace();
       }
    }
}
  1. 常量池溢出(常量池都有哪些信息,JVM类文件结构中有详细描述)


package com.yhj.jvm.memory.constant;
import java.util.ArrayList;
import java.util.List;
/**
 * @Described:常量池内存溢出探究
 * @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author YHJ create at 2011-10-30 下午04:28:30
 * @FileNmae com.yhj.jvm.memory.constant.ConstantOutOfMemory.java
 */

public class ConstantOutOfMemory {
    /**
     * @param args
     * @throws Exception
     * @Author YHJ create at 2011-10-30 下午04:28:25
     */

    public static void main(String[] args) throws Exception {
       try {
           List<String> strings = new ArrayList<String>();
           int i = 0;
           while(true){
              strings.add(String.valueOf(i++).intern());
           }
       } catch (Exception e) {
           e.printStackTrace();
           throw e;
       }

    }
 
}
  1. 方法区溢出


package com.yhj.jvm.memory.methodArea;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * @Described:方法区溢出测试
 * 使用技术 CBlib
 * @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author YHJ create at 2011-11-12 下午08:47:55
 * @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java
 */

public class MethodAreaOutOfMemory {
    /**
     * @param args
     * @Author YHJ create at 2011-11-12 下午08:47:51
     */

    public static void main(String[] args) {
       while(true){
           Enhancer enhancer = new Enhancer();
           enhancer.setSuperclass(TestCase.class);
           enhancer.setUseCache(false);
           enhancer.setCallback(new MethodInterceptor() {
              @Override
              public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
                  return arg3.invokeSuper(arg0, arg2);
              }
           });
           enhancer.create();
       }
    }
 
}

/**
 * @Described:测试用例
 * @author YHJ create at 2011-11-12 下午08:53:09
 * @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java
 */

class TestCase{
  
}
  1. 直接内存溢出


package com.yhj.jvm.memory.directoryMemory;
import java.lang.reflect.Field;
import sun.misc.Unsafe;

/**
 * @Described:直接内存溢出测试
 * @VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author YHJ create at 2011-11-12 下午09:06:10
 * @FileNmae com.yhj.jvm.memory.directoryMemory.DirectoryMemoryOutOfmemory.java
 */

public class DirectoryMemoryOutOfmemory {
 
    private static final int ONE_MB = 1024*1024;
    private static int count = 1;

    /**
     * @param args
     * @Author YHJ create at 2011-11-12 下午09:05:54
     */

    public static void main(String[] args) {

       try {

           Field field = Unsafe.class.getDeclaredField("theUnsafe");

           field.setAccessible(true);

           Unsafe unsafe = (Unsafe) field.get(null);

           while (true) {

              unsafe.allocateMemory(ONE_MB);

              count++;

           }

       } catch (Exception e) {

           System.out.println("Exception:instance created "+count);

           e.printStackTrace();

       } catch (Error e) {

           System.out.println("Error:instance created "+count);

           e.printStackTrace();

       }

 

    }

 

}

七、几个问题思考

  1. java通过字节码文件和JVM来实现跨平台,那么c和c++如何实现跨平台的(编译型):不同的操作系统写不同的代码。
  2. 有人说i++不安全,原因是底层的指令不止一条(javap命令),一个语句对应多条操作。
  3. 垃圾回收算法(理论)、垃圾回收器(实践):
    (1)标记清除算法:通过可达性分析将可回收的对象进行标记,标记后再统一回收所有标记的对象。缺点:标记和清除效率不高;标记清除之后会产生大量不连续的内存碎片。
    (2)复制回收算法:为了解决效率问题,将内存划分为大小相同的两部分,每次只使用其中的一块。当这块内存用完了,就将还存活的对象复制到另外一块内存中,然后将第一块用完的内存进行完全清理,然后再来放接下来的数据,这样每次只对半个区域进行垃圾回收,内存分配时也不用考虑内存碎片的情况。但是这个代价是牺牲一半的内存空间,研究发现大部分的对象都是“朝生夕死”,不需要转存到另一块,所以这个1:1的比例被调成了8:1,这样只有这少部分内存会被浪费,但内存可以充分利用,提高效率。
    (3)标记整理算法:先标记需要回收的对象,然后把所有存活的对象移动到内存的一端,这样的好处是避免了内存碎片,老年代的full gc 就是用了这种算法。
  4. 什么样的对象能被回收?不能被回收?
    判断一个对象死亡,并不是根据是否还有对象对其引用,而是通过根可达进行分析。对象之间的引用可以抽象成树形结构,通过一系列树根(GC ROOTS)作为起点,从树根往下搜索,搜索走过的链称为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明这个对象是不可用的,该对象被判定为可回收对象。java还提供了软引用和弱引用,这两种引用是可以随时被虚拟机回收的对象
    可以作为GC ROOTS的对象:
    (1)虚拟机栈(栈帧中的局部变量表)中引用的对象
    (2)方法区中类静态属性引用的对象和常量引用的对象
    (3)本地方法栈中JNI(即一般说的native方法)引用的对象
  5. Spring Bean是如何被回收的?多少种bean(单例的原型的等等),周期不同,回收时间也不同
  6. jvm调优
    在这里插入图片描述
    mat分析泄露源
  7. full gc 5分钟一次怎么优化?
    明确目的:追求吞吐量、追求最小执行时间
  8. Dakvik虚拟机能否执行class文件?Dakvik并不是一个java虚拟机,它没有遵循java虚拟机的规范,不能直接执行java的class文件,使用的是寄存器架构不是JVM中常见的栈架构,但是它与java又有着千丝万缕的关系,它执行的的dex文件可以通过class文件转换而来。
  9. 内存溢出(OOM:out of Memory)和内存泄漏(Memory Leak):
    内存溢出:剩下的内存或分配的内存不足以放下我的数据,就报错
    内存泄漏: 用完了这块内存上的数据,但是不回收不释放,一直占用着,一次没关系,多次就会内存溢出了。内存泄漏的堆积,这会最终消耗尽系统所有的内存!
    内存溢出的原因及解决:
    1)检查数据库查询中,是否有一次获得大量或全部数据的查询,用分页解决。
    2)检查代码中是否有死循环或递归调用,或者循环重复产生新对象实体
    3)检查list、map等集合对象是否有使用完后未清除的问题,因集合中始终存在对象的引用,导致不会被GC
    4)启动参数内存值设定过小,可以修改JVM的启动参数,增加内存((-Xms,-Xmx参数一定不要忘记加。))
    5)查“outofMemory”错误日志,或内存工具动态查看
  10. 为什么只需要一个eden而需要两个survivor?
    (1)Survivor区的作用:如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。

没有Survivor的情况下,有没有什么解决办法,可以避免上述情况:

方案优点缺点
增加老年代空间更多存活对象才能填满老年代。降低Full GC频率一旦发生Full GC,执行所需要的时间更长
减少老年代空间Full GC所需时间减少老年代很快被存活对象填满,Full GC频率增加

显而易见,没有Survivor的话,上述两种解决方案都不能从根本上解决问题。

我们可以得到第一条结论:Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
(2)为什么设置2个survivor区:目的就是解决了内存碎片化。
为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
其中颜色块代表对象,白色框分别代表Eden区(大)和Survivor区(小)。Eden区理所当然大一些,否则新建对象很快就导致Eden区满,进而触发Minor GC,有悖于初衷。
在这里插入图片描述
碎片化带来的风险是极大的,严重影响JAVA程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存,就会放不下了,这就好比我们爬山的时候,背包里所有东西紧挨着放,最后就可能省出一块完整的空间放相机。如果每件行李之间隔一点空隙乱放,很可能最后就要一路把相机挂在脖子上了。

那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。下图中每部分的意义和上一张图一样。
在这里插入图片描述
上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

那么,Survivor为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满,因此,我认为两块Survivor区是经过权衡之后的最佳方案。

  1. 执行minor gc 的时候不是只操作eden 区,除了第一次比较特殊外,以后都是操作eden区还有form区还有to区等等,每个区域都会minor gc进行根可达筛选,筛选出幸存的对象。
  2. GC 主要在堆中,但是不是别的地方没有,例如方法区中的常量池等等都需要被回收的,但说到方法区我们一般不去回收,因为反射的存在还会用到。
  3. 类加载机制:
    类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

其中加载、验证、准备、初始化、和卸载这5个阶段的顺序是确定的。而解析阶段不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的运行时绑定。

关于初始化:JVM规范明确规定,有且只有5中情况必须执行对类的初始化(加载、验证、准备自然再此之前要发生):
1.遇到new、getstatic、putstatic、invokestatic,如果类没有初始化,则必须初始化,这几条指令分别是指:new新对象、读取静态变量、设置静态变量,调用静态函数。
2.使用java.lang.reflect包的方法对类进行反射调用时,如果类没初始化,则需要初始化
3.当初始化一个类时,如果发现父类没有初始化,则需要先触发父类初始化。
4.当虚拟机启动时,用户需要制定一个执行的主类(包含main函数的类),虚拟机会先初始化这个类。
5.但是用JDK1.7启的动态语言支持时,如果一个MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、Ref_invokeStatic的方法句柄时,并且这个方法句柄所对应的类没有进行初始化,则要先触发其初始化。

另外要注意的是:通过子类来引用父类的静态字段,不会导致子类初始化:

public class SuperClass{
    public static int value=123;
    static{
        System.out.printLn("SuperClass init!");
    }
}

public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }


}

public class Test{

    public static void main(String[] args){
        System.out.println(SubClass.value);
    }
}

最后只会打印:SuperClass init!
对应静态变量,只有直接定义这个字段的类才会被初始化,因此通过子类类引用父类中定义的静态变量只会触发父类初始化而不会触发子类初始化。

通过数组定义来引用类,不会触发此类的初始化:

public class Test{

    public static void main(String[] args){
        SuperClass[] sca=new SuperClass[10];
    }
}

常量会在编译阶段存入调用者的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类初始化,示例代码如下:

public class ConstClass{
    public static final String HELLO_WORLD="hello world";
    static {
        System.out.println("ConstClass init!");
    }

}

public class Test{
    public static void main(String[] args){

        System.out.print(ConstClass.HELLO_WORLD);
    }


}

上面代码不会出现ConstClass init!

加载
加载过程主要做以下3件事
1.通过一个类的全限定名称来获取此类的二进制流
2.强这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

验证
这个阶段主要是为了确保Class文件字节流中包含信息符合当前虚拟机的要求,并且不会出现危害虚拟机自身的安全。

准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中分配。首先,这个时候分配内存仅仅包括类变量(被static修饰的变量),而不包括实例变量。实例变量会在对象实例化时随着对象一起分配在java堆中。其次这里所说的初始值“通常情况下”是数据类型的零值,假设一个类变量定义为

public static int value=123;

那变量value在准备阶段后的初始值是0,而不是123,因为还没有执行任何Java方法,而把value赋值为123是在程序编译后,存放在类构造函数()方法中。

解析
解析阶段是把虚拟机中常量池的符号引用替换为直接引用的过程。

初始化
类初始化时类加载的最后一步,前面类加载过程中,除了加载阶段用户可以通过自定义类加载器参与以外,其余动作都是虚拟机主导和控制。到了初始化阶段,才是真正执行类中定义Java程序代码。

准备阶段中,变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划初始化类变量。初始化过程其实是执行类构造器()方法的过程。

()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。收集的顺序是按照语句在源文件中出现的顺序。静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量可以赋值,但不能访问。如下所示:

public class Test{
    static{
        i=0;//給变量赋值,可以通过编译
        System.out.print(i);//这句编译器会提示:“非法向前引用”
    }
    static int i=1;

}

()方法与类构造函数(或者说实例构造器())不同,他不需要显式地调用父类构造器,虚拟机会保证子类的()方法执行之前,父类的()已经执行完毕。

类加载器
关于自定义类加载器,和双亲委派模型

  1. 简述java中访问对象过程:
    对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码:
    Object obj = new Object();
    假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。

如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的
具体地址信息,如下图所示

在这里插入图片描述

如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型
数据的相关信息,reference 中直接存储的就是对象地址,如下图所示
在这里插入图片描述
这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身不需要被修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。现在较为常用的虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

八、图灵公开课收获

  1. jvisualvm是java自带的工具,可以查看所有的java进程对资源的使用情况,需要再按照一个插件,visual GC即可方便查看某个java程序运行的GC情况。
    插件按照参考(工具-》插件-》更新一下就能显示所有插件,找到插件安装即可)

在这里插入图片描述
常用参数的描述:

  • -Xms:堆的最小大小
  • -Xmx:堆的最大大小
  • -Xmn:年轻代大小,相减也直接得出老年代的大小了
  • -Xss:每个线程占用的大小(一个线程对应一个栈)
  1. 对象动态年龄判断:在这里插入图片描述
    会导致一些问题,比如会导致一些对象更快的进入老年代,所以如果程序正好每次都导致一个对象进入老年代,老年代就会更快的full GC,所以有时候JVM调优,就可能想让survivor区变大,那就在堆不变的情况下,增加新生代(年轻代)大小,结果也就会导致老年代减少,打破了老年代:新生代=2:1的比例。

  2. Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。这也是为什么要减少full GC的一个原因


  1. 字面量就是比如说int a = 1; 这个1就是字面量。又比如String a = “abc”,这个abc就是字面量。 ↩︎

  2. 在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类要引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类 的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。 ↩︎

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值