运行时数据区域、溢出、垃圾收集、问题解决
运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
1)程序计数器
(1)一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
(2)字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
(3)因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
(4)如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2)Java虚拟机栈
(1)线程私有,它的生命周期与线程相同
(2)虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
(3)如果线程请求的栈深度大于虚拟机所允许的深度(栈帧过多),将抛出StackOverflowError异常;如果扩展时无法申请到足够的内存(栈),就会抛出OutOfMemoryError异常
注意
每个线程只有1个活动栈帧
可设置栈内存大小,-Xss256k
3)本地方法栈
(1)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务
(2)有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一
(3)与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
4)Java堆
(1)Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
(2)此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(非绝对)
(3)根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
(4)如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
注意
设置堆大小,-Xmx8m
5)方法区
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。简单来说由类加载器、运行时常量池、类信息组成,jdk1.8之后放置在系统内存中
注意
设置原空间内存,-XX:MetaspaceSize=8m
6)运行时常量池
(1)方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
(2)运行时常量池备动态性,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法
(3)StringTable,存放字符串常量,为hashtable结构,在jdk1.8中放置在堆内存中,会垃圾回收
public class test2 {
public static void main(String[] args) {
String s1="a";
String s2="b";
String s3="ab";
String s4=s1+s2;//存放在堆里,原理是StringBuilder
String s5="a"+"b";//编译器优化,会存放在Stringtable中,如果已存在则不会创建
System.out.println (s3 == s4);//false
System.out.println (s3 == s5);//true
}
}
public class test2 {
public static void main(String[] args) {
String x="ab";
String s1=new String ("a")+new String ("b");
String s2=s1.intern ();
//将这个字符串尝试放入串池,如果放入成功,s1将是放入串池的对象,否则s1仍然指向堆中的对象, s2h会接收到串池中的对象
System.out.println (s2=="ab");//true
System.out.println (s1=="ab");//false,s1放入串池失败,仍然指向堆中对象
}
}
(4)StringTable的优化
如果字符串常量非常多,可将StringTable设置得更大一些,减少hash冲突-XX:StringTableSize=桶个数
字符串存在重复问题,将字符串对象调用intern()入池
7)直接内存
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class test2 {
private static String FROM="G:\\技术视频教程\\数据结构-邓俊辉\\数据结构下\\第01章 绪论\\01-A-1 计算.mp4";
private static String TO="G:\\a.mp4";
public static void main(String[] args) {
long start=System.nanoTime ();
try {
FileChannel from=new FileInputStream (FROM).getChannel ();
FileChannel to=new FileOutputStream (TO).getChannel ();
ByteBuffer bb=ByteBuffer.allocateDirect (1024*1024);
while(true){
int len=from.read (bb);
if(len==-1){
break;
}
bb.flip ();//写入前调用
to.write (bb);
bb.clear ();
}
} catch (FileNotFoundException e) {
e.printStackTrace ( );
} catch (IOException e) {
e.printStackTrace ( );
} finally {
}
long end=System.nanoTime ();
System.out.println ((end-start));
}
}
注意
直接内存是无法直接由java虚拟机回收,需要调用unsafe类的freeMemory方法。当DirectByteBuffer被销毁时会掉用虚引用对象Cleaner的clean方法执行freeMemory方法
溢出
1)Java堆溢出
原因可能是内存泄漏或者内存溢出
2)虚拟机栈和本地方法栈溢出
(1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
(2)建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程,将栈深度调至1000~2000
3)方法区和运行时常量池溢出
当前的很多主流框架,如Spring、Hibernate,在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。另外,JVM上的动态语言(例如Groovy等)通常都会持续创建类来实现语言的动态性
4)本机直接内存溢出
(1)DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样
(2)DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因
垃圾收集
1
如何回收
1)引用计数算法
对于一个对象A,只要有任何一个对象引⽤用了了A,则A的引⽤计数器就加1,当引⽤用失效时,引用计数器就减1,只要对象A的引用计数器的值为0,则对象A就会被回收。
缺陷
很难解决对象之间相互循环引用的问题
2)可达性分析算法(标记清除)
(1)通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,所以它们将会被判定为是可回收的对象
(2)在可达性分析算法中不可达的对象,会进行筛选判定为有必要执行finalize()方法,判定为是的会被放在F-Queue的队列之中,稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它的finalize()方法
(3)稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可
在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(PhantomReference)4种,这4种引用强度依次逐渐减弱
软引用的使用
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class test3 {
public static void main(String[] args) {
//设置虚拟机内存
// -Xmx20m -XX:+PrintGCDetails -verbose:gc
List<SoftReference<byte[]>> list=new ArrayList<> ();
for (int i = 0; i <5 ; i++) {
SoftReference<byte[]> ref=new SoftReference<> (new byte[4*1024*1024]);
System.out.println (ref.get ());
list.add (ref);
System.out.println (list.size ( ));
}
for (SoftReference<byte[]> ref:list) {
System.out.println (ref.get ( ));
}
}
}
[B@1b2c6ec2
1
[B@4edde6e5
2
[B@70177ecd
3
[GC (Allocation Failure) [PSYoungGen: 3554K->512K(6144K)] 15842K->13454K(19968K), 0.0022867 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 512K->0K(6144K)] [ParOldGen: 12942K->13410K(13824K)] 13454K->13410K(19968K), [Metaspace: 3562K->3562K(1056768K)], 0.0077567 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[B@1e80bfe8
4
[Full GC (Ergonomics) [PSYoungGen: 4208K->4096K(6144K)] [ParOldGen: 13410K->13233K(13824K)] 17618K->17329K(19968K), [Metaspace: 3564K->3564K(1056768K)], 0.0073050 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4096K->0K(6144K)] [ParOldGen: 13233K->926K(8192K)] 17329K->926K(14336K), [Metaspace: 3564K->3564K(1056768K)], 0.0066002 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@66a29884
5
null
null
null
null
[B@66a29884
清除空对象
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class test3 {
public static void main(String[] args) {
//设置虚拟机内存
// -Xmx20m -XX:+PrintGCDetails -verbose:gc
List<SoftReference<byte[]>> list=new ArrayList<> ();
ReferenceQueue<byte[]> queue=new ReferenceQueue<> ();//引用队列
for (int i = 0; i <5 ; i++) {
SoftReference<byte[]> ref=new SoftReference<> (new byte[4*1024*1024],queue);//当软引用对象被清除时会将该对象放入队列中
System.out.println (ref.get ());
list.add (ref);
System.out.println (list.size ( ));
}
Reference<?extends byte[]> poll=queue.poll ();
while (poll!=null){
System.out.println (poll);
list.remove (poll);
poll=queue.poll ();
}
for (SoftReference<byte[]> ref:list) {
System.out.println (ref.get ( ));
}
}
}
[B@1b2c6ec2
1
[B@4edde6e5
2
[B@70177ecd
3
[GC (Allocation Failure) [PSYoungGen: 3554K->512K(6144K)] 15842K->13519K(19968K), 0.0012239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 512K->0K(6144K)] [ParOldGen: 13007K->13410K(13824K)] 13519K->13410K(19968K), [Metaspace: 3563K->3563K(1056768K)], 0.0073579 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@1e80bfe8
4
[Full GC (Ergonomics) [PSYoungGen: 4208K->4096K(6144K)] [ParOldGen: 13410K->13233K(13824K)] 17619K->17329K(19968K), [Metaspace: 3564K->3564K(1056768K)], 0.0070355 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4096K->0K(6144K)] [ParOldGen: 13233K->926K(8192K)] 17329K->926K(14336K), [Metaspace: 3564K->3564K(1056768K)], 0.0063917 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[B@66a29884
5
java.lang.ref.SoftReference@4769b07b
java.lang.ref.SoftReference@cc34f4d
java.lang.ref.SoftReference@17a7cec2
java.lang.ref.SoftReference@65b3120a
[B@66a29884
注意
强引用不会被回收,软引用在gc操作后内存仍不足时会被回收,弱引用是在每次gc操作后会被回收
3)回收方法区
Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁、自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出
2
垃圾收集算法
1)标记-清除算法
主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
2)复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。不产生内存碎片,实现简单,运行高效,代价是将内存缩小为了原来的一半,可采用这种收集算法来回收新生代。新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间
3)标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
4)分代收集算法
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收
过程
(1)对象首先分配在伊甸园区域
(2)新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活对象的年龄加一并交换from 、to
(3)minor gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
(4)当对象寿命超过阈值时,会晋升至老年代,最大寿命时15
(5)当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长
相关VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx或-XX:MaxHeapSize=size |
新生代大小 | -Xmm或(-XX:NewSize=size+-XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InnitlalSurvirorRatio=ratio和-XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurovorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGc前MinorGc | -XX:+ScavengeBeforeFullGc |
注意
1.如果是大对象,会直接晋升到老年代
2.如果要放入堆内存的对象在新生代和老年代都放不下,会抛出堆溢出异常;在多线程中,某一个线程发生溢出,不会影响主线程的运行
3
垃圾回收器
串行、吞吐量优先、响应时间优先(可与用户线程并行,碎片多)
问题解决
1
CPU占用过高
1)top定位哪个线程对cpu的占用过高
2)ps h -eo pid,tid,%cpu|grep 进程id
3)jstack进程id
2
迟迟得不到结果
1)运行程序
$ nohub java cn.zhanghuan
执行完后会返回一个进程号32753
2)分析
$ jstack 32753
执行后会返回详细信息,可发现发生死锁
3
堆内存诊断
工具名 | 作用 |
---|---|
jps | 查看当前系统中有哪些java进程 |
jmap | 查看堆内存占用情况 |
jconsole | 图形界面,多功能监测工具,可连续监测 |
jvisualvm | 图形界面,可抓取堆的快照 |
$ jhsdb jmap --heap --pid 81828
Attaching to process ID 81828, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 9+181
using thread-local object allocation.
Garbage-First (G1) GC with 10 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 4282384384 (4084.0MB)
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 2569011200 (2450.0MB)
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 1048576 (1.0MB)
Heap Usage:
G1 Heap:
regions = 4084
capacity = 4282384384 (4084.0MB)
used = 5539464 (5.282844543457031MB)
free = 4276844920 (4078.717155456543MB)
0.12935466560864425% used
G1 Young Generation:
Eden Space:
regions = 0
capacity = 4194304 (4.0MB)
used = 0 (0.0MB)
free = 4194304 (4.0MB)
0.0% used
Survivor Space:
regions = 1
capacity = 1048576 (1.0MB)
used = 1048576 (1.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:
regions = 5
capacity = 9437184 (9.0MB)
used = 4490888 (4.282844543457031MB)
free = 4946296 (4.717155456542969MB)
47.58716159396701% used
9883 interned Strings occupying 701160 bytes.
标签:java,MB,虚拟机,Java,线程,内存,JVM,2020
From: https://www.cnblogs.com/sylvesterzhang/p/18089866