JVM是运行在操作系统之上的,它与硬件没有直接的交互
属于线程私有的(灰色暗色):本地方法栈,java栈,程序计数器 所有线程共享(亮色,存在垃圾回收) :方法区 , 堆
负责加载class文件,class文件在文件开头有特定的文件标示(cafe babe开头的文件),将class文件字节码内容加载到内存中,并将这些内容转换成方法区(放类的描述的地方)中的运行时数据结构,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
虚拟机自带的加载器: 1、启动类加载器 Bootstrap Class Loader (C++) 2、扩展类加载器Extension Class Loader (Java) 3、应用程序类加载器AppClassLoader 用户自定义加载器 4、java.lang.ClassLoader的子类,用户可以定制类的加载方式
jdk自带的类(jre/lib/rt.jar下面),由BootstrapClassLoader加载, 自己创建的由AppClassLoader加载
Object o= new Object(); Test test = new Test(); //输出 null System.out.println(o.getClass().getClassLoader()); //输出sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(test.getClass().getClassLoader());当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需要加载的Class),子类加载器才会尝试自己去加载。( 从最顶层的BootstrapClassLoader的开始加载 )
采用双亲委派的一个好处就是 沙箱安全机制 比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托自己顶层的启动类加载器来进行加载,这样就保证了使用不同的类加载器最终得到的都是同样的一个Object对象。
如下,自定义的类和jdk自带的类名重复,从最顶层找String类,找到了jdk自带的类,但是它没有main方法,所以报错。
native,关键字,标记的方法放到本地方法栈里,有声明,无实现。 Natice Method Stack :本地方法栈为虚拟机使用到的Native方法服务。 本地方法栈特性: 线程私有 后进先出(LIFO)栈 作用是支撑Native方法的调用、执行和退出 可能出现OutOfMemoryError异常和StackOverflowError异常
使用举例: Thread的start0()方法。本地方法栈用于调用c/c++写的方法去调用硬件,如java程序驱动打印机等。 类中定义本地方法,当使用的时候由执行引擎加载native libraies。
俗话解释: 是一个指针,指向下一次执行的位置。
供各个线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。 上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace) JAVA7及以前是永久代,Java7以后叫元空间。
tip: 实例变量(每个对象特有的)存在堆内存中,和方法区无关。
栈管运行,堆管存储。 队列: 先进先出(FIFO) 栈:先进后出(FILO)
栈也叫栈内存,主管java程序的运行,是在线程创建是创建,它的生命周期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程已结束该栈就Over,生命周期和线程一致,是线程私有的。 栈内存分配的有: 8种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配。
栈帧中主要保存3类数据 本地变量(Local Variables) 输入参数和输出参数以及方法内的变量。 栈操作 (Operand Stack) 记录出栈,入栈的操作 栈帧数据 (Frame Data) 包括类文件,方法等等
栈运行原理: 栈中的数据都是以栈帧(Stack Frame) 的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生一个栈帧F1,并被压入到栈中, A方法又调用了B方法,于是产生栈帧F2也被压入栈, B方法又调用了C方法,于是产生栈帧F3也被压入栈, …… 执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧…… 遵循 ”先进后出"/ “后进先出"原则。
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈道出栈的过程。栈的大小和具体的JVM的实现有关,通常在256k~756K之间,约等于1Mb左右。
栈里面存储结构如下:
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
java7之前: 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。 S0区 = from 区 S1区 = to 区 from区和to区每次GC之后会交换,谁空谁是to. 逻辑上分为 新生 + 养老 + 永久(元空间) 物理上分为 新生 + 养老
1、eden 、SurvivorFrom 复制到SurvivoreTo ,年龄+1 首先,当Eden区满的时候会触发一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1
2、清空eden、SurvivorFrom 然后,清空Eden 和SurvivorFrom中的对象,也即复制之后的交换,谁空谁为To
3、SurvivorTo和SurvivorFrom互换 最后,SurvivorTo和SurvivorFrom交换,原SuvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From区和To区复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息 + 普通常量 + 静态常量 + 编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一部分 ,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开 对于HotSpot虚拟机,很多开发者习惯将方法区称为"永久代(Parmanent Gen)",但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口Interface)的一个实现,jdk1.7的版本中,已经将原来放在永久代的字符串常量池移走。
永久区储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域占用的内存。
在java8中,永久代已经被移除,被一个称为元空间的区域所取代,元空间的本质和永久代类似
元空间与永久代之间最大的区别在于: 永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用的本机物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
OOM的八种类型
日志分析:
jvm设置10m: -Xms10m -Xmx10m -XX:+PrintGCDetails
[GC (Allocation Failure) [PSYoungGen: 1213K->496K(2560K)] 1384K->755K(9728K), 0.0007905 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 496K->512K(2560K)] 755K->778K(9728K), 0.0006015 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 266K->539K(7168K)] 778K->539K(9728K), [Metaspace: 2990K->2990K(1056768K)], 0.0201245 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 539K->539K(9728K), 0.0054780 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 539K->524K(7168K)] 539K->524K(9728K), [Metaspace: 2990K->2990K(1056768K)], 0.0077834 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] Heap PSYoungGen total 2560K, used 102K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000) eden space 2048K, 4% used [0x00000007bfd00000,0x00000007bfd19868,0x00000007bff00000) from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000) ParOldGen total 7168K, used 524K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000) object space 7168K, 7% used [0x00000007bf600000,0x00000007bf683228,0x00000007bfd00000) Metaspace used 3021K, capacity 4556K, committed 4864K, reserved 1056768K class space used 322K, capacity 392K, committed 512K, reserved 1048576K分代收集算法:
1、次数上频繁收集Young区2、次数上较少收集Old区3、基本不动Perm区JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。 因此GC按照回收的区域又分为两种类型,一种是普通GC( minor GC,发生在young区),一种是全局GC(major GC or Full GC,发生在old区)
Minor GC会把Eden中所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden就变成空的了。 当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然存活的对象复制到另外一块Survivor区域(即to区域)中,然后清理所使用过的Eden以及Survivor区域(即from区域),并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,通过-XX:MaxTenuringThreshold来设定参数),这些对象就会成为老年代。 -XX:MaxTenuringThreshold 设置对象在新生代中存活的次数
//jvm设置 -Xmx10m -Xms10m -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15年轻代中的GC,主要是复制算法。 大小比例:Eden区 : From区 : To区 = 8 : 1 :1 HotSpot JVm把年轻代分为三部分,1个Eden区和2个Survivor区(分别叫from和to),默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区,对象在Survivor区中每熬过一次MinorGC,年龄就会增加1,当它的年龄增加到一定程度时,就会被移动到老年代中,因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面,复制算法不会产生内存碎片
从根集合(GC Root)开始,通过Tracing(遍历)从From中找到存活对象,拷贝到To中; From 、To 交换身份,下次内存分配从To开始; GC回收图解
1、它浪费了一般的内存 2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍,复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
老年代一般是由 标记清除 或者 标记清除与标记整理混合使用
算法分成 标记 和 清除 两个阶段,先要标记出需要清除的对象,然后统一回收这些对象
1、两次扫描,耗时(一次扫描标记,一次扫描清除) 2、会产生内存碎片
当发生FullGC的时候,垃圾收集器为了回收更多的垃圾对象,它会从JVM的所有内存中回收垃圾(Young、Old、Perm、Metaspace)。FullGC有很多个步骤,有些步骤需要暂停所有的应用线程,,在这个过程中JVM是无法处理用户的请求的,用户体验差。JVM会使用所有的CPU来做垃圾回收,因此CPU的利用率会非常高。所以一般是非常不希望应用中出现FullGC,更别说是连续的FullGC了。
又叫标记整理算法 -》或 标记清除压缩算法 标记清除压缩算法->标记清除和标记压缩结合,多次GC后再压缩,减少移动的成本。
不仅要标记存活对象,还要整理所有存活对象的引用地址。
新生代:复制算法 old 区 :标记清除 & 标记压缩 混合使用