赞
踩
最常见的引用,类似Object obj = new Object()、String str =“hello”。
如果一个对象具有强引用,垃圾回收器绝对不会回收它。即使抛出“OutOfMemoryError”错误,程序终止,也不会随意回收具有强引用的对象来解决内存不足的问题。只有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收。
用来描述有用但是不是必需的对象。
在Java中用Java.lang.ref.SoftReference类表示,如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足,就会回收这些对象的内存。通常,软引用用于网页缓存、图片缓存等。
- User user = new User();
- SoftReference softReference = new SoftReference(user);
同样用来描述非必需的对象,但是它的强度比软引用更弱一些。
Java中用java.lang.ref.WeakRefence类表示。当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象。(可以用于:单例持有一个activity引用时,会造成内存泄漏,把activity声明为弱引用,在activity被销毁后,垃圾收集器扫描到activity对象后,会回收对象的引用)
注:ThreadLocal中ThreadLocalMap对象中的key使用的是弱引用。
与其他引用并不同,虚引用不影响对象的生命周期,也无法通过虚引用来取得一个对象实例。
在Java中,用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。
- User user = new User();
- ReferenceQueue referenceQueue = new ReferenceQueue();
- PhantomReference phantomReference = new PhantomReference(user,queue);
Java运行时数据区:<程序计数器,虚拟机栈,本地方法栈,堆,方法区>;
其中,程序计数器、虚拟机栈、本地方法栈,为线程独有;堆和方法区为所有线程共有。
【程序计数器】用来记录当前线程执行到哪个指令(线程私有,每个线程都有),可以看作是当前线程所执行的字节码的行号指示器。
【虚拟机栈】执行引擎每调用一个方法,就为这个方法创建一个栈帧。每个方法从调用到执行结束,都对应一个栈帧的入栈和出栈。为Java代码方法提供服务。
【本地方法栈】与虚拟机栈类似,不同的是本地方法栈为Native方法提供服务。
【堆】被所有的线程共享的一块区域,在虚拟机启动时创建,所有的对象实例及数组都在堆上分配。使用new关键字,表示在堆上开辟一块新的存储空间。无法扩展时,会抛出OOM异常。
【方法区/元空间】又叫做静态区,被各个线程共享。用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
JDK1.6及以前,常量池在方法区,对应为“永久代”;
JDK1.7,方法区合并到了堆中,这个时候常量池可以理解为在堆内存中;
JDK1.8及之后,方法区从堆中分离出来,这时候的方法区对应叫做“元空间”,直接使用物理内存。因为原来的永久代大小不易评估,同时调优的效率较低。
一个对象被引用了一次,在当前的对象头上递增一次引用次数。如果这个对象的引用次数为0,代表这个对象可回收。缺点:无法解决循环引用的问题。
JVM应用的GC算法为可达性算法,从 GC Root 开始,标记包括 Root 及 Root 引用的东西,并且只会清除没有标记的东西。因此,Root 其实就是 JVM 认为一定有用的东西。
【可作为 GC Roots 的对象】
注:目前虚拟机均采用的可达性分析法。
方法:首先标记出需要回收的对象,标记完成后统一回收掉所有的被标记的对象。
优点:标记和清除的速度较快;
缺点:会产生很多不连续的内存碎片,不利于后期变量的内存分配。对于大对象,可能会找不到足够的连续空间,进而会触发新的垃圾回收动作。
方法:此方法在标记清除算法上做了改进,标记阶段同样需要标记出所有需要回收的对象,标记之后不直接对可回收对象进行整理,而是让所有的对象朝着一端移动。在移动的过程中,清理掉可回收的对象。
优点:内存被整理后不会产生大量不连续的内存碎片问题;
缺点:由于对象需要异动,效率较低。
多在老年代中使用。
方法:将内存容量划分为相等的两部分。每次只使用其中的一部分,当垃圾回收动作被触发时,将有用的存活的对象复制到另一部分的内存中。清除掉原来的那部分内存的所有数据。
优点:无碎片;
缺点:内存使用率低,可用的内存降为了原来的一半。
多在年轻代中使用。
概述:此方法是目前大部分JVM所采用的方法。核心思想是根据对象存活的不同周期将内存划分为不同的的区域,并对不同区域的对象采用不同的垃圾回收方法。
区域划分:年轻代,老年代,永久代(JAVA8之前) \ 元空间(>=Java8)。
分代原因及对象:堆内存是虚拟机管理内存最大的一块,也是垃圾回收最频繁的区域。程序所有的对象实例都存放在堆内存中。因为不同类型的对象在内存中存活的时间是不一样的,所以给堆内存分代可以提高对象内存分配和垃圾回收的效率。
Stop The World:JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿,会带给用户不良的体验。
(1)年轻代:使用复制和标记-清除垃圾收集算法。因为绝大部分的对象都是短声明周期的对象,所以不需要将新生代内存划分为容量相等的两个部分。而是将划分为Eden区、Survivor from、Survivor to三个部分,比例为 8:1:1。后两个部分一定有一个区域是空白的。Eden和另一个Survivor区域共计90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费。
(2)老年代:使用标记-整理的垃圾回收算法。其中的对象一半都是长生命周期的对象。
(3)# 永久代(方法区):Java虚拟机内存中的方法区在Sun HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。
“永久代”不是JVM规范中的一个部分,它只是HotSpot虚拟机特有的对于方法区的一种实现方式,即HotSpot虚拟机通过设置永久代来实现JVM要求的方法区;
在Java8及其之后,JVM已经取消“永生代”的使用,而是采用“元空间”(Metaspace)替代了。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
采用元空间代替永生代的原因主要是:
方法区(永久代)的垃圾收集主要回收两部分的内容:废弃常量、无用的类。
【废弃常量】:常量池中的字面量为例,一个字符串常量在没有任何 String 对象引用的情况;
【无用的类】:必须满足三个条件:
在发生Minor GC(新生代垃圾回收)之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC;
此处“风险”指的是:新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC
<Serial、Serial Old;Parallel new、Parallel Old;CMS、G1>,Java8 默认Parallel new + Parallel Old。
Q:JVM有哪些垃圾回收器
A:
在JVM中,实现了多种垃圾收集器,包括:
- 串行:Serial GC(新生代,复制算法)、Serial Old GC(老年代,标记清除)
- 并行:Parallel New GC、Parallel Old GC
- CMS(并发)垃圾收集器:CMS GC,作用在老年代
- G1垃圾收集器,作用在新生代和老年代
Q:介绍一下G1垃圾回收器
A:
- 应用于新生代和老年代,在JDK9之后默认使用G1;
- 划分成多个区域,每个区域都可以充当 eden、survivor、old;humongous,其中 humongous 专为大对象准备;
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收(stw)、并发标记(重新标记stw)、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
生命周期:加载--验证--准备--解析--初始化--使用--卸载。
其中,加载、验证、准备、解析、初始化,称为类的加载过程。验证、准备、解析,三个部分统称为连接。
正式为类变量分配内存并设置类变量初始值的阶段,这些类变量锁使用的内存都将在方法区中进行分配。
将常量池中的符号引用替换为直接引用。
根据程序员通过程序制定的主观计划去初始化类变量和其他资源:初始化阶段是执行类构造器<clinit>()方法的过程--主要是对类变量进行初始化,是执行类构造器的过程。
Q:说一下类加载的执行过程
A:
- 加载:查找和导入class文件;
- 验证:保证加载类的准确性、安全性;
- 准备:为类变量分配内存并设置类变量初始值;
- 解析:把类中的符号引用转换为直接引用;
- 初始化:对类的静态变量,静态代码块执行初始化操作;
- 使用:JVM 开始从入口方法开始执行用户的程序代码;
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。
JVM只会执行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而启动Java程序。
“通过一个类的全限定名来获取描述此类的二进制字节流(字节码文件)”加载到JVM中,以便让应用程序自己决定如何去获取所需要的类,即用于装载字节码文件。实现这个动作的代码模块称为“类加载器”。
类加载器的分类:
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法中:首先检查是否已经被加载过,如果没有加载则调用父加载器的loadClass()方法,若父加载器为空,则默认使用启动类加载器作为父加载器。如果父加载器失败,抛出ClassNotFoundException
即,自底向上请求,自顶向下尝试加载,加载成功则结束,否则抛出异常,交给下一级加载。
双亲委派机制的【优点】:
# 几个【注意点】:
【如何打破双亲委派机制】
主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。
相关参数设置位置:
结尾参数 & :让命令在后台执行,终端退出后命令仍旧执行。
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
<吞吐量、暂停时间、内存占用>
1)优先原则:优先架构调优和代码调优,最后才是JVM优化;
通常设置相同的值,避免运行时不断扩展,扩大为1.2-1.5倍FullGC后的永久代/元空间大小。
日志包括“发生的时间、停顿类型--是否发生了Stop-The-World、发生的区域、GC前后区域使用容量及总容量、GC执行的时间(单位s)”
名称 | 主要作用 |
jps | JVM Process Status Tool,虚拟机进程状态信息 |
jstack | Stack Trace for Java,查看java进程的线程的堆栈信息 |
jmap | Memory Map for Java,生成虚拟机的堆转内存快照(heapdump文件) |
jhat | JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果 |
jstat | Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面运行数据,包括垃圾回收信息、类加载信息、新生代统计信息等 |
jinfo | Configuration Info forJava,显示虚拟机的配置信息 |
<JConsole、VisualVM>
1)JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具。
命令行窗口输入 jconsole 即可开启。可以选择连接本地或者远程的进程,即可查看相关信息。
| ![]() |
2)VisualVM
支持运行监视、故障处理、性能分析等。注:许多版本的JDK不自带此工具,需要单独下载。
(1)监控GC的状态
(2)生成堆的dump文件
可以使用Java的jmap命令或者通过JMX的MBean工具生成当前的Heap信息,结果是一个hprof文件。
(3)分析dump文件
【可以使用的工具】:
- VisualVM
- IBM HeapAnalyzer
- JDK自带的Hprof工具
- Mat工具(Eclipse中的内存分析工具)
(4)分析结果,判断是否需要优化
(5)调整GC类型和内存分配
(6)不断分析和调整
异常信息:java.lang.OutOfMemoryError:Java heap space
java堆用于存储对象实例,只要程序不断的创建对象,且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆限制后产生内存溢出异常。
原因分析:
解决方案:
通常情况只需要修改“-Xmx”参数,调高JVM堆内存即可。如果未解决:
如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverFlowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
异常信息:java.lang.OutOfMemoryError:PermGen space
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
异常信息:java.lang.OutOfMemoryError:PermGen space
方法区用于存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
解决方案:
根据 Permgen space 报错的时机,可以采取不同的解决方案:
如果上述方法无法解决,可以通过jmap命令dump内存对象“jmap -dump:format=b,file=fump.hprof<process-id>”,然后利用Eclipse MAT功能逐步分析开销最大的classLoader 和重复的class。
JVM向操作系统创建native线程失败。
原因分析:
解决方案:
表示虚拟内存已经被耗尽。虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。当运行时程序请求的虚拟内存溢出时就会报该错误。
原因分析:
解决方案:
根据错误原因,
Java允许应用程序通过Direct ByteBuffer直接访问堆外内存,许多高性能程序通过Direct ByteBuffer结合内存映射文件(Memory Mapped File)实现高速IO。
原因分析:
Direct ByteBuffer默认大小为64MB,超出限制就会抛出“Direct Buffer Memory”错误。
解决方案:
1、堆内存溢出检查
(1)程序启动时,设置参数以便在内存溢出时生成dump文件:
-XX:Xms 20m; # 初始堆内存大小
-XX: Xmx 20m; # 最大堆内存大小
-XX:HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:/ # 发生内存溢出时dump文件存放的位置。
(2)使用jmap实时查看内存情况/导出dump文件
一般步骤:
2、Java进程CPU占用过高检查
3、Java进程内存泄漏
主要使用jstat命令查看GC情况。
jstat -gc pid [interval]
参考:
《深入理解Java虚拟机》;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。