栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JVM笔记<记录学习,持续更新中>

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JVM笔记<记录学习,持续更新中>

内存区域和内存溢出异常

线程独占:栈,本地方法栈,程序计数器
线程共享:堆,方法区
<1>.程序计数器 线程私有
JVM的多线程是通过线程之间轮流切换,分配处理器执行时间的方式来实现的。
为了保证线程切换后能回到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这个内存区域称为线程私有的内存,执行Native方法时,程序计数器为空。
<2>.JVM栈 线程私有
描述的是Java执行方法的线程内存模型:每个方法被执行的时候,JVM都会同步创建一个 栈桢 用于存储 局部变量表 操作数栈 动态连接 方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈桢在虚拟机栈中的从入栈到出栈的过程。
<3>.异常
如果线程请求的 栈深度 大于 虚拟机所允许的深度,将抛出StackOverFlowError异常。
如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够内存空间将会抛出OutOfMemoryError异常。
<4>.本地方法栈 线程私有
他与虚拟机栈所发挥的作用是极其相似的,区别在于虚拟机栈为虚拟机执行java方法也就是class字节码服务。而本地方法栈则是为虚拟机使用到 本地(Native)方法 服务。
<5>.堆 线程共享
存放对象的实例。是 垃圾收集器 管理的内存区域。可扩展(通过 -Xms和-Xmx参数设定),如果在java堆中没有内存完成实例分配,并且堆无法再扩展时,会抛出 OutOfMemoryError 异常。
<6>.方法区 线程共享
通过 永久代 实现 方法区,到了JDK8,完全废弃了 永久代 的概念,改用 元空间(meta-space)来代替。把JDK7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。这区域的内存回收目标主要是针对 常量池的回收 和对类型 的卸载。
如果方法区无法满足新的内存分配需求时,会抛出 OutOfMemoryError
<7>.运行时常量池
是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,
还有一项信息是 常量池表(Constant Pool Table),用于存放 编译器生成的各种字面量和符号引用,这部分内容将在 类加载后 存放到方法区的运行时常量池中。除了保存Class文件中描述的符号引用,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
<8>.直接内存
JDK1.4引入了 NIO(New Input/OutPut)类,引入一种基于 通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用 Native函数库 直接分配堆外内存,然后通过一个存储在java堆里面的 DirectByteBuffer对象
作为这块内存的引用进行操作。这样能在一些场景中显著提高性能
因为避免了在 java堆 和 Native堆 中来回 复制数据。
<9>.对象的创建
①当 java 虚拟机遇到一条字节码 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从java堆中划分出来。假设java堆中内存是绝对规整的,所有被使用过的内存都放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
这种分配方式称为指针碰撞(Bump The Pointer)。但如果 java堆中的内存 并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行 指针碰撞 了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到了一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由 java堆 是否规整决定,而 java堆 是否规整又由所采用的 垃圾收集器 是否带有 空间压缩整理(Compact)的能力决定。
因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用 CMS这种 基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。
②除如何划分可用空间外,还有一个需要考虑的问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
①对分配内存空间的动作进行同步处理—实际上虚拟机是采用 CAS(CAS,compare and swap,比较并交换,在JDK 5之前Java 是靠synchronized 保证同步的,synchronized是独占锁,独占锁是一种悲观锁,会导致其他线程挂起。乐观锁用到的机制就是 CAS)配上失败重试的方式保证更新操作的原子性。
在CAS中,有这样三个值:
V:要更新的变量(var)
E:预期值(expected)
N:新值(new)
⽐较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。所以这⾥的预期值E本质上指的是"旧值"。
②另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程java堆中预先分配一小块内存,称为
本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
<10>.对象的内存布局
对象在 堆内存 中的存储布局可以划分为三个部分:对象头(Header),实例数据(Instance Data)和对齐填充(Padding).
<11>.取消堆的自动扩展
将 堆 的 最小值 -Xms 参数与 最大值 -Xmx 参数设置为一样即可避免堆自动扩展
<12>.内存溢出 与 内存泄漏
溢出:没有足够的空间
泄漏:申请到内存空间后,无法释放已申请的空间,积累多了,会提高溢出发生的概率。
<13>.堆溢出
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
解决:先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)。
①如果是 内存泄漏,可通过查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收他们。
②如果是 内存溢出,就是内存中的对象确实是必须存活的,那就应当检查JVM的堆参数(-Xms和-Xmx)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
<14>.虚拟机栈和本地方法栈溢出
栈容量只能通过 -Xss 参数来设定。
① 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError。
② 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,抛出OutOfMemoryError。
结论:无论是由于栈桢太大还是虚拟机栈容量太小,当新的栈桢内存无法分配的时候,HotSport虚拟机 抛出的都是 StackOverFlowError。如果是在允许动态扩展栈容量大小的虚拟机上,相同代码会导致不一样的情况。
<15>.方法区和运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGenspace
String:intern()是一个本地方法,他的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回该对象在常量池中的引用。否则,将会此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。 在JDK6或者更早之前的HotSport中,常量池都是分配在 永久代 中。
可以通过 -XX:PermSize和 -XX:MaxPermSize 限制 永久代 的大小。
<16>.SOF(堆栈溢出StackOverflow):
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。

自JDK7起,原本存放在 永久代 的字符串常量池被移至 java堆 之中。

<17>.JDK6和JDK7的String:intern()区别

public static void main(String[] args) {
   	String str1 = new StringBuilder("计算机").append("软件").toString();
 	System.out.println(str1.intern() == str1);
 	String str2 = new StringBuilder("ja").append("va").toString();
	System.out.println(str2.intern() == str2);
	}

这段代码在JDK6中运行,结果是两个false,在JDK7中,会得到一个true和一个false。
因为在JDK6中,intern()方法会把首次遇到的字符串实例 复制到永久代的字符串常量池中存储,返回的也是永久代中的这个字符串实例的引用,而由StringBuilder创建的字符串对象实例实在java堆上。所以不是同一个引用。
在JDK7中的 intern() 方法实现不需要拷贝字符串的实例到永久代中了,既然字符串常量池已经移到了java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。而对 str2中的 java 这个字符串在执行StrigBuilder.toString()之前就出现出现过了。字符串常量池中已经有它的引用,不符合intern()要求首次遇到的原则,“计算机软件”这个字符串是首次出现的,所以返回true。

JDK8以后,永久代便完全退出,元空间替代。

<17>.设置元空间的参数:
-XX:MaxmetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
-XX:metaspaceSize:指定元空间的初始空间大小 字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整,如果释放了大量空间就适当降低该值,如果释放了少量空间,那么在不超过-XX:metaspaceSize(如果设置了话)的情况下,适当提高该值。
-XX:MinmetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有
-XX:MaxmetaspaceFreeRatio:用于控制最大的元空间剩余容量的百分比。
<18>.引用计数算法 <不是java使用的判断对象是否已经死亡的算法>
在对象中添加一个引用计数器,每当有一个地方引用他就加1,当引用失效时,就减1,任何时刻计数器为0的时候就说明对象不再是被使用的。但是当两个对象互相引用着彼此,导致计数器都不为0,所以无法被引用计数算法回收。
<18>.java使用的是 可达性分析算法
思路是:通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots之间没有任何引用链相连,或者是GC Roots到这个对象不可达的时候则证明此对象不可能再被引用了。
固定可作为GC Roots的对象包括:
1)在虚拟机栈(栈桢中的本地变量表)中引用的对象,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量等
2)在方法区中类静态属性引用的对象,如java类的引用类型静态变量
3)在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
4)在本地方法栈中JNI(即通常所说的Native方法)引用的对象
5)JVM内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器。
6)所有被同步锁(synchronized)持有的对象
7)反映JVM内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存等。
<19>.引用
引用分为 强引用、软引用、弱引用和虚引用。
1)强引用:只要强引用关系还在,垃圾收集器就永远不会回收被引用的对象。
使用场景:String str = new String(“str”);
2)软引用:描述一些还用有,但非必须的对象。只被软引用关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出的异常。提供了SoftReference类实现了软引用。
源码:假设垃圾收集器在某个时间点确定一个对象是软可达的。 那时,它可以选择以原子方式清除对该对象的所有软引用以及对任何其他软可访问对象的所有软引用,通过强引用链可以从这些对象中访问该对象。同时或稍后,它会将那些注册到引用队列(ReferenceQueue)的新清除的软引用加入队列。
使用场景:创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
3)弱引用:最常用于实现 规范化映射。被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类实现了软引用。
源码:通过强引用和软引用链可以从中访问该对象。 同时或稍后它会将那些注册到引用队列的新清除的弱引用加入队列。
使用场景: Java源码中的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
4)虚引用:也称为“幽灵引用”或者“幻影引用”。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是 为了能在这个对象被收集回收的时候收到一个系统通知。PhantomReference实现了虚引用。它被回收之前,会被放入ReferenceQueue中。注意,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue。
使用场景:对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效。
<20>.生存还是死亡
经过可达性分析算法中判定不可达的对象,也不是立刻死亡的,要真正宣告一个对象的死亡,最多会经历两次标记过程:
①如果对象在进行可达性分析算法后发现没有与GC Roots相连接的引用链,那他将会被第一次标记。随后进行一次筛选,筛选条件是此对象是否有必要执行 finalize(),假如对象没有覆盖finalize(),或者finalize()已经被虚拟机调用过,那么虚拟机将这两种情况都视为 没有必要执行。
如果这个对象被视为有必要执行finalize(),那么该对象会被放在一个叫做F-Queue队列中,并在之后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()。这里所说的“执行”是指虚拟机会触发这个方法不代表会等待这个方法的结束。原因是如果某个对象的finalize()执行缓慢或者更极端的发生了死循环,将有可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统崩溃。finalize()是对象逃脱死亡命运的最后机会,`稍后收集器会对F-Queue中的对象进行第二次标记,如果对象在finalize()中拯救了自己[只要重新与引用链上的任何一个对象建立关联即可]则可以逃脱。
<21>.java堆区域
新生代(Young Generation)和老年代(Old Generation),在新生代中,每次垃圾收集都会发现大量对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
部分收集(Partial GC):指目标不是完整收集整个java堆得垃圾收集,其中又分为:
1)新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
2)老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集
目前只有CMS收集器会单独收集老年代的行为。
【注意】Major GC这个词会有混淆,需要区分上下文区分到底是 老年代的收集还是整堆收集。
3)混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
<22>.标记-清除算法(Mark-Sweep)
先标记需要回收的对象,再进行回收。或者先标记存活的对象,再回收未被标记的对象。
有两个缺点:
1.执行效率不稳定,如果java堆中包含大量对象,其中大部分都会被回收,那么进行全局标记和清除的效率就会随着对象的数量增多而降低。
2.内存碎片化的问题,标记,清除之后会产生大量的内存碎片,导致以后在程序运行的时候需要分配较大对象时无法找到足够大的连续的内存空间而不得不提前触发另一次垃圾收集的动作。
<23>.标记-复制算法(Mark-Copying)
“半区复制”:将可用内存一分为二,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。缺点:如果内存中是大量存活的对象,那么这种算法会产生大量的内存间复制的开销,并且可用内存缩小了一半,空间资源浪费严重。
“Appel式回收”:把新生代分为一块较大的Eden(伊甸园)空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor空间上。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已使用过的Survivor空间。HotSport虚拟机默认Eden和Survivor的大小比例为8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%+一块Survivor的10%),只有一个Survivor空间即10%的新生代是不会被浪费的。除此之外,还有一个“逃生门”的安全设计,当一个Survivor空间不足以放下一次Minor GC之后存活的对象,就需要依赖其他区域(大多就是老年代)进行分配担保。也就是如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代空间。
<24>.标记-整理算法(Mark-Compact)
标记-复制算法在对象存活率较高的时候就要进行较多的复制操作,效率就会降低。更关键的是,如果不想浪费50%的空间,就要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选择这种算法。
标记过程和标记-清除一样,但是后续的步骤不是直接清除可回收对象,而是让所有存活下来的对象都向内存空间一段移动,然后直接清理掉边界以外的内存。和标记-清除算法区别的本质就在于 是否为移动式。
如果移动存活对象,尤其是老年代这种每次回收都有大量对象存活区域,更新所有引用这些对象就成为一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才可以进行。[通常标记-清除算法也是需要停顿用户线程来标记、清理可回收的对象,只是停顿时间相对而言要短]
HotSport虚拟机里关注吞吐量的Parallel Old收集器是基于标记-整理算法的,而关注延迟的CMS则是基于标记-清除算法的,并且在内存空间碎片过多的情况下,CMS收集器则会进行一次标记-整理算法收集一次。
<25>.Serial收集器 串行 单线程
不仅是他只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在他进行垃圾收集时,必须暂停其他所有工作线程,直到他收集结束。也就是STW。
有着优于其他收集器的地方那就是 简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的,对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

<26>.ParNew收集器
ParNew是Serial收集器的多线程并行版本。
-XX:SurvivorRatio:设置新生代中eden和S0/S1空间的比例
默认-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1和Serial收集器完全一致。
除了Serial收集器外,只有ParNew收集器可以和CMS收集器配合使用工作。
JDK5 出现了CMS收集器,首次实现了让垃圾收集线程和用户线程(基本上)同时工作
但是,作为老年代收集器的CMS无法和在JDK1.4.0中的新生代收集器Parallel Scavenge配合工作,所以在JDK5中使用CMS收集老年代的时候,新生代只能选择ParNew或者Serial之一。ParNew收集器是激活CMS后的默认新生代收集器
也可以使用-XX:+/-UseParNewGC参数选项来强制指定或者禁用它。
JDK9开始,取消了-XX:+/-UseParNewGC参数选项,这意味着ParNew合并入CMS,成为专门处理新生代的组成部分。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/671654.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号