jvm内存模型
u 程序计数器
u Java栈(虚拟机栈)
u 本地方法栈
u Java堆
u 方法区及其运行时常量池
垃圾回收机制
u 新生代和老年代
u 参数设置
u 垃圾回收(Minor GC 和 Full GC)和回收算法
u finalize()、减少GC开销、触发主GC的条件
u 判断无用对象、四种引用方式、为什么进行垃圾回收
u String、StringBuffer与StringBuilder
u 类加载机制
类加载机制
一 : jvm内存模型
第一、程序计数器(PC)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来取下一条需要执行的字节码指令
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。
注:程序计数器是线程私有的,每条线程都会有一个独立的程序计数器
第二、Java栈(虚拟机栈)
Java栈就是Java中的方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(关于栈帧后面介绍),这个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
注:Java栈也是线程私有的。
异常可能性:对于栈有两种异常情况:如果线程请求的栈深度大于栈所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈可以动态拓展,在拓展的时无法申请到足够的内存,将会抛出OutOfMemoryError异常
栈帧
1) 局部变量表
2) 操作数栈
3) 整数加法
4) 法返回地址
第三、本地方法栈
本地方法栈与Java栈所发挥的作用是非常相似的,它们之间的区别不过是Java栈执行Java方法,本地方法栈执行的是本地方法。
注:本地方法栈也是线程私有的
异常可能性:和Java栈一样,可能抛出StackOverflowError和OutOfMemeryError异常
第四、Java堆
对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,当然我们后面说到的垃圾回收器的内容的时候,其实Java堆就是垃圾回收器管理的主要区域。
注:堆是线程共享的
异常可能性:如果堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemeryError异常
第五、方法区(如果一个系统不断地产生新的类,而没有回收,那最终非常有可能导致永久区溢出。)
方法区它用于存储已被虚拟机加载的类信息(类型信息,字段信息,方法信息,其他信息)静态量、即时编译器编译后的代码等数据。方法区是线程安全的
注:方法区和堆一样是线程共享的
异常可能性:当方法区无法满足内存分配需求时,将抛出OutOfMemeryError异常
运行时常量池
用于存放编译器生成的各种字面量和符号引用当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。
问:在方法里面创建的本地对象,它会创建在内存结构的哪个地方?如何访问该对象?
在Java栈的栈帧里面创建了对象的引用,在堆上创建了对象,栈帧里的引用指向堆中的对象。
参数配置
- -Xmx3550m:设置JVM最大堆内存为3550M。
- -Xms3550m:设置JVM初始堆内存为3550M。
- -XX:NewSize=1024m:设置新生代初始值为1024M。
- -XX:MaxNewSize=1024m:设置新生代最大值为1024M。
- -XX:PermSize=256m:设置老年代初始值为256M。
- -XX:MaxPermSize=256m:设置老年代最大值为256M。
- -XX:NewRatio=4:设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示新生代比老年代为1:4。
- -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。
-Xss是设置栈的
二 :Jvm垃圾回收
Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。
堆的内存分配
新生代(Young Generation)(默认的Eden:Survivor = 8:1)
1.所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。一般把老年带发生的gc叫full GC
在Minor GC时会将新生代中还存活着的对象复制进一个Survivor中,然后对Eden和另一个Survivor进行清理。所以,平常可用的新生代大小为Eden的大小+一个Survivor的大小。所有的Minor GC都会触发全世界的暂停(stop-the-world除了垃圾收集收集器线程之外的线程都被挂起),停止应用程序的线程,不过这个过程非常短暂。Eden 和Survivor区不存在内存碎片。
当对象在 Eden出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定),这些对象就会成为老年代。
对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)
年老代(Old Generation)
1 . 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2 . 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。
什么情况下,新生代的对象会进入老年代呢?
当Minor GC时,新生代存活的对象大于Survivor的大小时,这时一个Survivor装不下它们,那么它们就会进入老年代。
在新生代的每一次Minor GC 都会给在新生代中的对象+1岁,默认到15岁时就会从新生代进入老年代-XX:MaxTenuringThreshold来设置这个临界点。
如果设置了-XX:PretenureSizeThreshold3M 那么大于3M的对象就会直接就进入老年代。
3.finalize()方法什么时候被调用?
l 垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法 但是在Java中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说filalize()可能永远不被执行,显然指望它做收尾工作是靠不住的。 那么finalize()究竟是做什么的呢?
l 它最主要的用途是回收特殊渠道申请的内存。由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候java允许在类中定义一个由 finalize()方法
问:.减少GC开销的措施
(1)不要显式调用System.gc()
(2)尽量减少临时对象的使用
(3)对象不用时最好显式置为Null
(4)尽量使用StringBuffer,而不用String来累加字符串
问:.什么时候会进行GC(Garbage Collector)
(1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
(2)主动调用system.gc()
(2)eden区满时、老年代满时
问:降低GC的调优
可调试NewSize、permSize、NewRatio、SurvivorRatio、进入老年带的临界岁数
Java对象在内存中的状态:
可达的/可触及的:
Java对象被创建后,如果被一个或多个变量引用,那就是可达的。即从根节点可以触及到这个对象。
其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的。
可恢复的:
Java对象不再被任何变量引用就进入了可恢复状态。
在回收该对象之前,该对象的finalize()方法进行资源清理。如果在finalize()方法中重新让变量引用该对象,则该对象再次变为可达状态,否则该对象进入不可达状态
不可达的:
Java对象不被任何变量引用,且系统在调用对象的finalize()方法后依然没有使该对象变成可达状态(该对象依然没有被变量引用),那么该对象将变成不可达状态。
当Java对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
两种最基本的垃圾收集器
1、Serial收集器:(串行收集器)
这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop-The-World:将用户正常工作的线程全部暂停掉),直到它收集结束。收集器的运行过程如下图所示:
2、ParNew收集器:Serial收集器的多线程版本(使用多条线程进行GC)
ParNew收集器是Serial收集器的多线程版本。
一.如何确定某个对象是“垃圾”(无用对象)?
引用计数法
给对象中添加一个引用计数器,任何时刻计数器为0的对象就是不可能再被使用的。主流的java虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因是:它很难解决对象之间相互循环引用的问题。
可达性分析法(常用)
该方法的基本思想是通过一系列的根对象的集合作为起点进行搜索,搜索所经过的路径称为“引用链”,从这些根对象开始,任何可以被触及的对象都是被认为是“活动”的对象。无法被触及的对象被认为是垃圾,因为它们不在影响程序的未来执行。在后面介绍标记-清理算法/标记整理算法时,也会一直强调从根节点开始,对所有可达对象做一次标记。
可作为GC Roots的对象包括下面几种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI[Java Native Interface]引用的对象
二.典型的垃圾收集算法
1.Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2.Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。问题是占用内存较多
3.Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
4.分代收集算法
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。方法区永久代,回收方法同老年代。
老年代采用的是标记-清除或者标记-整理算法,这两个算法主要看虚拟机采用的哪个收集器,两种算法的区别是:标记-清除可能会产生大量连续的内存碎片。
三.String、StringBuffer与StringBuilder之间区别
String 字符串常亮,底层是数组实现的
StringBuilder:线程非安全的
StringBuffer:线程安全的
对于三者使用的总结:
1.如果要操作少量的数据用 = String
2.单线程操作字符串缓冲区下操作大量数据 = StringBuilder
3.多线程操作字符串缓冲区下操作大量数据 = StringBuffer
四.为什么要进行垃圾回收
在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。 JVM的一个系统级线程会自动释放该内存块,减轻编程的负担。事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片。
五.java虚拟机类加载机制
包括加载、链接(含验证、准备、解析)、初始化
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
l 加载
加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性
类加载器:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。(Java中的Object类,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。)
l 验证
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
l 准备
准备阶段是正式为类变量分配内存并设置类变量初始值(零)的阶段,这些内存都将在方法区中分配。
l 解析
解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。
l 初始化
初始化是类加载过程的最后一步,此阶段才开始真正执行类中定义的Java程序代码(静态语句块和构造器)
类的初始化过程(重要)
Student s = new Student();在内存中做了哪些事情?
- 加载Student.class文件进内存
- 在栈内存为s开辟空间
- 在堆内存为学生对象开辟空间
- 对学生对象的成员变量进行默认初始化
- 对学生对象的成员变量进行显示初始化
- 通过构造方法对学生对象的成员变量赋值
- 学生对象初始化完毕,把对象地址赋值给s变量
强引用、软引用、弱引用、虚引用
l 强引用:当我们使用new 这个关键字创建对象时被创建的对象就是强引用,垃圾回收器就不会去回收有强引用的对象。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
l 软引用:如果一个对象具备软引用,如果内存空间足够,那么垃圾回收器就不会回收它,如果内存空间不足了,就会回收该对象。当然没有被回收之前,该对象依然可以被程序调用。java.lang.ref.SoftReference
l 弱引用:如果一个对象只具有弱引用,只要垃圾回收器在自己的内存空间中线程检测到了,就会立即被回收,对应内存也会被释放掉。java.lang.ref.WeakReference
l 虚引用:如果一个对象只具有虚引用,那么它就和没有任何引用一样,随时会被jvm当作垃圾进行回收。虚引用目的:当对象被收集器回收时收到系统通知。java.lang.ref.PhantomReference