-
JVM简析:
-
JVM组成结构谈谈
-
Java Virtual Machine
-
操作系统(如Windows、Linux等)
-
硬件体系(如Intel体系、SPAC等)
JVM是运行在操作系统之上的,它与硬件没有直接交互
-
-
Class Loader类加载器:
负责加载class文件,class文件在开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
-
Native Interface
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,于是从内存中专门开辟了一块区域处理标记native方法,在Execution Engine执行时加载native libraies.
目前方法使用很少,除非和硬件有关应用,或者Java系统管理生产设备,在企业级应用中已经比较少见。
因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可使用Web Service等等。
-
Method Area 方法区 (淘宝周志明)
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也 在此定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
静态变量+常量+类信息+运行时常量池存在方法区中+实例变量存在堆内存中
-
PC Register 程序计数器
每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
-
Native Method Stack 本地方法栈
它的具体做法是 Native Method Stack 中登记native方法,在Execution Engine执行时加载native libraies.
栈管运行,堆管存储**!!!
-
Stack 栈是什么:栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就OVER,生命周期和线程一致,是线程私有的。基本类型的变量和对象的引用变量都是在函数的栈内存中分配!!!。
-
栈存储什么?栈帧主要保存3 类数据:
-
本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
-
栈操作(Operand Stack):记录出栈、入栈的操作;
-
栈帧数据(Frame Data):包括类文件、方法等。
-
-
栈运行原理:先进后出,后进先出。like 弹夹。
-
-
判断JVM优化是哪里? 主要优化堆
由线程共享的数据区:只有方法区和堆
三种JVM:
-
Sun公司的 HotSpot Sun被Oracle收购
-
BEA公司的 JRockit 被Oracle收购
-
IBM公司的J9 VM
7.1 新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两个部分:
伊甸区:Eden Space;
幸存区:Survivor 1 Space;
所有的类都是在伊甸区被new出来的。
幸存区有两个:
-
0区(Survivor 0 Space)
-
1区(Survivor 1 Space)
当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这时将产生minor GC(Full GC),进行养老区的内存清理。若养老区执行了Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常"Out Of Memory Error".
如果出现java.lang.OutOfMemoryError:Java heap space异常,说明java虚拟机的堆内存不够。原因有两个:
-
1.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
-
2.代码中创建了大量的对象,并且长时间不能被垃圾收集器收集(存在被引用)
7.2 养老区(Tenure Generation Space)
养老区用于保存从新生区筛选出来的 JAVA对象,一般池对象都在这个区域活跃。
7.3 永久区(Permanent Space)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收的,关闭JVM才会释放此区域所占的内存。
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个TomCat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
JDK 1.6及之前:有永久代,常量池1.6在方法区
JDK 1.7:有永久代,但已经逐步 “去永久代”,常量池1.7在堆内存
JDK1.8及之后:无永久代,常量池1.8在元空间
程序内存划分小总结
实际而言,方法区(Method Area) 和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等待,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久带来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
常量池(Constant Pool) 是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。
JVM垃圾收集(Java Garbage Collection)
上集,本次均以JDK1.7+HotSpot为例
1.JDK1.8之后将最初的永久代取消了,由元空间取代。
2.目的:将HotSpot与JRockit两个虚拟机标准
GC(堆内存调优简介)
-Xms:设置初始值分配大小,默认为物理内存的1/64
-Xmx:最大分配内存,默认为物理内存的1/4
-XX:+PrintGCDetails:输出详细的GC处理日志
package com.mvj; public class StackDemo2 { public static void main(String[] args) { long maxMemory = Runtime.getRuntime().maxMemory();// 返回java // 虚拟机试图使用的最大内存量 long totalMemory = Runtime.getRuntime().totalMemory();// 返回java System.out.println("maxMemory=" + maxMemory + "(字节)" + (maxMemory / (double) 1024 / 1024) + "MB"); System.out.println("TotalMemory" + totalMemory + "(字节)" + (totalMemory / (double) 1024 / 1024) + "MB"); } }
结果:
maxMemory=2731016192(字节)2604.5MB TotalMemory185073664(字节)176.5MB
//VM参数: Xmx1024m -Xms1024m -XX:+PrintGCDetails
改参数测试:
package com.mvj; import java.util.Random; public class StackDemo2 { public static void main(String[] args) { // long maxMemory = Runtime.getRuntime().maxMemory();// 返回java // // 虚拟机试图使用的最大内存量 // long totalMemory = Runtime.getRuntime().totalMemory();// 返回java // // System.out.println("maxMemory=" + maxMemory + "(字节)" + (maxMemory / // (double) 1024 / 1024) + "MB"); // System.out.println("TotalMemory" + totalMemory + "(字节)" + // (totalMemory / (double) 1024 / 1024) + "MB"); String str = "hello puzzle"; while (true) { str += str + new Random().nextInt(888888888) + new Random().nextInt(999999999); } } } // VM参数: Xmx1024m -Xms1024m -XX:+PrintGCDetails
不改参数结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOfRange(Arrays.java:3664) at java.lang.String.<init>(String.java:207) at java.lang.StringBuilder.toString(StringBuilder.java:407) at com.mvj.StackDemo2.main(StackDemo2.java:18)
改参数运行结果: 自动触发垃圾回收
[GC (Allocation Failure) [PSYoungGen: 1477K->502K(2048K)] 1477K->922K(7680K), 0.0022215 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1662K->505K(2048K)] 2082K->1617K(7680K), 0.0005040 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1962K->331K(2048K)] 5929K->4297K(7680K), 0.0004068 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 331K->283K(2048K)] 4297K->4249K(7680K), 0.0003533 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 283K->0K(2048K)] [ParOldGen: 3966K->2644K(5632K)] 4249K->2644K(7680K), [Metaspace: 2702K->2702K(1056768K)], 0.0066355 secs] [Times: user=0.09 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [PSYoungGen: 30K->32K(2048K)] 4578K->4579K(7680K), 0.0005217 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 32K->32K(2048K)] 4579K->4579K(7680K), 0.0004910 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 32K->0K(2048K)] [ParOldGen: 4547K->3596K(5632K)] 4579K->3596K(7680K), [Metaspace: 2702K->2702K(1056768K)], 0.0026266 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 0K->32K(2048K)] 5499K->5531K(7680K), 0.0003021 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 32K->0K(2048K)] [ParOldGen: 5499K->2644K(5632K)] 5531K->2644K(7680K), [Metaspace: 2702K->2702K(1056768K)], 0.0061275 secs] [Times: user=0.13 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [PSYoungGen: 30K->0K(2048K)] 4577K->4547K(7680K), 0.0005018 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4547K->4547K(7680K), 0.0002321 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4547K->4547K(5632K)] 4547K->4547K(7680K), [Metaspace: 2702K->2702K(1056768K)], 0.0017545 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4547K->4547K(7680K), 0.0007669 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4547K->4533K(5632K)] 4547K->4533K(7680K), [Metaspace: 2702K->2702K(1056768K)], 0.0074809 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332) at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448) at java.lang.StringBuilder.append(StringBuilder.java:136) at com.mvj.StackDemo2.main(StackDemo2.java:18) Heap PSYoungGen total 2048K, used 61K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000) eden space 1536K, 4% used [0x00000000ffd80000,0x00000000ffd8f618,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 5632K, used 4533K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000) object space 5632K, 80% used [0x00000000ff800000,0x00000000ffc6d5f0,0x00000000ffd80000) Metaspace used 2734K, capacity 4486K, committed 4864K, reserved 1056768K class space used 294K, capacity 386K, committed 512K, reserved 1048576K
GC面试题:
1.StackOverflowError和OutOfMemoryError,谈谈你的理解
如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OutOfMemoryError
这里把异常分为两种情况,但是存在一些相互重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已经使用的栈空间太大,本质上是对同一个问题的两种描述而已。
java.lang.OutOfMemoryError这个错误我相信大部分开发人员都有遇到过,产生该错误的原因大都出于以下原因:JVM内存过小、程序不严密,产生了过多的垃圾。
导致OutOfMemoryError异常的常见原因有以下几种:
-
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
-
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
-
代码中存在死循环或循环产生过多重复的对象实体;
-
使用的第三方软件中的BUG;
-
启动参数内存值设定的过小;
此错误常见的错误提示:
-
tomcat:java.lang.OutOfMemoryError: PermGen space
-
tomcat:java.lang.OutOfMemoryError: Java heap space
-
weblogic:Root cause of ServletException java.lang.OutOfMemoryError
-
resin:java.lang.OutOfMemoryError
-
java:java.lang.OutOfMemoryError
2.一般什么时候会发生GC?如何处理?
答:Java中的GC会有两种回收:年轻代的MinorGC,和年老带的FullGC;如果此时老年代的内存空间不足就会触发FullGC,如果空间都不足抛出OutOfMemoryError.
3.GC回收策略,谈谈你的理解
答:年轻代(伊甸区和两个幸存区),GC回收策略为复制;
年老带的保存空间一般较大,GC回收策略为整理-压缩;
GC是什么?
-
频繁收集Young区
-
较少收集Old区
-
基本不动Perm区
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
因此GC按照的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(Minor Gc OR full GC)
-
普通DC:(minor GC) 只针对新生代区域的GC.
-
全局GC:(Minor GC OR Full GC) 针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC
GC算法总体概述:
GC算法:
一、复制算法:Minor GC(普通GC) 较小收集 8:1:1
-
年轻代(新生代)中使用的是MinorGC,这种算法采用的是复制算法.(copying)
-
what?
-
原理:HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to). 默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区每熬过一次Minor GC,年龄就增加一岁,当它的年龄达到一定程度时,就会被移动到老年代中。因为年轻代的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是讲内存分为两块,每次只用其中一块,当这一块内存用完,就将活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为From的Survivor区,Survivor区To是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到To,而在From区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到To区域。经过这次GC后,Eden区和From就是上次GC前的To.不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到To区被填满,To区被填满之后,会将所有对象移动到年老代中。
因为Eden区域对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外的80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放,依次类推。
-
Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。 当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次MinorGC后,如果对象还存活,并且能够被另一块Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然活着的对象复制到另外一块Survivor区域(即to区域)中,然后清理所使用过的Eden以及Survivor区域(即from区域),并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,当对象的年龄达到某个值是(默认是15岁通过
-XX:MaxTenuringThreshold来设定参数),这些对象就会成为老年代。
-XX:MaxTenuringThreshold -设置对象在新生代中存活的次数
好处:复制算法弥补了标记/清除算法中,内存布局混乱的缺点。
坏处:如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍,复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变得不可忽视,所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
标记清除
标记清楚/标记整理算法: Full GC 又叫MajorGC(全局GC)
老年达一般是由标记清除或者是标记清除与标记整合的混合实现
-
==============================================
-
-
标记清除(Mark-Sweep)
-
标记整理(Mark-Compact)
1.标记:(Mark)
从根集合开始扫描,对存活的对象进行标记。
2.清除:(Sweep)
扫描整个内存空间,回收未被标记的对象,使用free-list记录可以区域。
不需要额外空间
两次扫描,耗时严重
会产生内存碎片
劣势:
1.首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差。
2.其次,主要的缺点则是这种方式清理出来的空闲内存式不连续的,这点不难理解,我们的死亡对象都是随机的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然就会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
标记整理 (Mark-Compact)
Before GC After GC
标记/整理算法的唯一缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
内存效率: 复制算法>标记清楚算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况并不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说比较平滑一些,但效率上依然不尽如人,它比复制算法多了一个标记阶段,又比标记/清除多了一个整理内存的过程。
难道没有一种最优算法么?。。。。。。
回答: 无,没有最好的算法,只有最合适的算法。----->分代收集算法。
面试题:
1.JVM内存模型以及分区,详细到每个分区放什么
2.堆里面的分区:Eden,Survival from to ,老年代,各自的特点
3.GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别说说
4.Minor GC 与Full GC 分别在什么时候发生
-