1.展示堆内存溢出
设置堆的内存大小为10M,最大的堆内存为10M,这两个参数最好一致,即便最大内存设置为1G,很有可能也分配不到1G。
-Xmx10M -Xms10M
一直往list放东西
public class T1 {
public static void main(String[] args) {
ArrayList<Worker> list = new ArrayList<>();
for(;;){
list.add(new Worker());
}
}
}
最后导致堆内存溢出
2.展示方法区内存溢出
2.1设置Metaspace的大小,-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
2.2导入依赖
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
2.3代码
public class MyMetaspace extends ClassLoader {
public static List<Class<?>> createClasses() {
List<Class<?>> classes = new ArrayList<Class<?>>();
for (int i = 0; i < 10000000; ++i) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
"java/lang/Object", null);
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"()V", null, null);
mw.visitVarInsn(Opcodes.ALOAD, 0);
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"<init>", "()V");
mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
Metaspace test = new Metaspace();
byte[] code = cw.toByteArray();
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
}
@RestController
public class NonHeapController {
List<Class<?>> list=new ArrayList<Class<?>>();
@GetMapping("/nonheap")
public String nonheap(){
while(true){
list.addAll(MyMetaspace.createClasses());
}
}
}
导致溢出
java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191]
at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]
3. 展示虚拟机栈溢出
public class StackDemo {
public static long count=0;
public static void method(long i){
System.out.println(count++);
method(i);
}
public static void main(String[] args) {
method(1);
}
}
Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。
-Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右,一般设置-Xss512k。
线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
3.1在一个项目中什么时候进行垃圾回收合适
设计GC的回收有这几种方式,一次全给回收了,也就是回收的多,但是一次GC时间会很长,还有就是一次回收的少,但是GC的时间短。(ps:GC收垃圾的时候会占用线程,抢占业务的线程)
在业务中基本会让GC回收的时间可以让客户感觉不出来,但是又可以保证及时的回收垃圾,当一个项目用起来,CPU的占用率特别高的时候要适当降低垃圾回收的频率
4.对象的生命周期,GC什么时候回收对象
4.1创建阶段
(1)为对象分配存储空间
(2)开始构造对象
(3)从超类到子类对static成员进行初始化
(4)超类成员变量按顺序初始化,递归调用超类的构造方法
(5)子类成员变量按顺序初始化,子类构造方法调用,并且一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
4.2应用阶段
(1)系统至少维护着对象的一个强引用(Strong Reference)
(2)所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))
4.2.1引用的定义:
1.我们的数据类型必须是引用类型
2.我们这个类型的数据所存储的数据必须是另外一块内存的起始地址
4.2.1.1.强引用
JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用
4.2.1.2.软引用
软引用是用来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将于发生内存溢出异常之前,将会把这些对象列进回收范围中进行二次回收。
(当你去处理占用内存较大的对象 并且生命周期比较长的,不是频繁使用的)
问题:软引用可能会降低应用的运行效率与性能。比如:软引用指向的对象如果初始化很耗时,或者这个对象在进行使用的时候被第三方施加了我们未知的操作。
4.2.1.3.弱引用
弱引用(Weak Reference)对象与软引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收
用于查找定位对象,看对象到哪里了
4.2.1.4.虚引用
也叫幽灵引用和幻影引用,为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一**个系统通知。也就是说,如果一个对象被设置上了一个虚引用,实际上跟没有设置引用没有**任何的区别
4.2.1.4.软引用的使用
public class SoftReferenceDemo {
public static void main(String[] args) {
//。。。一堆业务代码
Worker a = new Worker();
//。。业务代码使用到了我们的Worker实例
// 使用完了a,将它设置为soft 引用类型,并且释放强引用;
SoftReference sr = new SoftReference(a);
a = null;
//这个时候他是有可能执行一次GC的
System.gc();
// 下次使用时
if (sr != null) {
a = (Worker) sr.get();
System.out.println(a );
} else {
// GC由于内存资源不足,可能系统已回收了a的软引用,
// 因此需要重新装载。
a = new Worker();
sr = new SoftReference(a);
}
}
}
4.2.1.5.弱引用的使用
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
//将缓存数据用软引用持有
WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//进行一次GC后查看对象的回收情况
System.gc();
//因为我们不确定我们的System什么时候GC
Thread.sleep(1000);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//将缓存数据的强引用去除
cacheData = null;
System.gc(); //默认通知一次Full GC
//等待GC
Thread.sleep(500);
System.out.println("第二次GC后" + cacheData);
System.out.println("第二次GC后" + cacheRef.get());
// // 弱引用Map
// WeakHashMap<String, String> whm = new WeakHashMap<String,String>();
}
}
4.2.1.6.虚引用的使用
public class PhantomReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object value = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
Thread thread = new Thread(() -> {
try {
int cnt = 0;
WeakReference<byte[]> k;
while ((k = (WeakReference) referenceQueue.remove()) != null) {
System.out.println((cnt++) + "回收了:" + k);
}
} catch (InterruptedException e) {
//结束循环
}
});
thread.setDaemon(true);
thread.start();
Map<Object, Object> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
byte[] bytes = new byte[1024 * 1024];
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);
map.put(weakReference, value);
}
System.out.println("map.size->" + map.size());
}
}
4.3不可见阶段
不可见阶段的对象在虚拟机的对象根引用集合中再也找不到直接或者间接的强引用,最常见的就是线程或者函数中的临时变量。程序不在持有对象的强引用。(但是某些类的静态变量或者JNI是有可能持有的 )
4.4不可达阶段
指对象不再被任何强引用持有,GC发现该对象已经不可达。
4.4.1如何确定一个对象是垃圾?
要想进行垃圾回收,得先知道什么样的对象是垃圾。
4.4.2引用计数法
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
弊端:如果AB相互持有引用(循环引用),导致永远不能被回收。
4.4.2.1循环引用
创建了两个对象,将对象哦o1指向o2,o2指向o1,在将o1和o2置空,这时o1和o2指向实例的指针就会被回收,但是在实例中有o1指向o2且o2指向o1,这两个实例之间是强引用,但是虚拟机栈中可以操作这连个实例的栈针已经被回收掉了,无法操作这两个实例,导致这两个实例一直在堆中无法回收,导致内存溢出。
主要发生循环依赖的地方是“本地方法栈”,当本地方法栈指向堆中的实例时,由于我们不能直接操作本地方法栈,导致无法回收堆中的对象
4.4.3解决循环依赖:可达性分析
通过GC Root的对象,开始向下寻找,看某个对象是否可达 ,根对象(错误的叫法)
能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。GC Roots本质上一组活跃的引用
可作为GC Root的东西
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
可达性算法是单向的而且是向外的,这时obj7,obj8就可以被回收了
4.5收集阶段(Collected)
GC发现对象处于不可达阶段并且GC已经对该对象的内存空间重新分配做好准备,对象进程收集阶段。如果,该对象的finalize()函数被重写,则执行该函数。
不可达阶段的时候并不是这个对象已经销毁了,他是可以复活的当执行了finalize方法时这个对象就可以复活,这个方法只能执行一次
4.5.1finalize方法代码Demo:
public class Finalize {
private static Finalize save_hook = null;//类变量
public void isAlive() {
System.out.println("我还活着");
}
@Override
public void finalize() {
System.out.println("finalize方法被执行");
Finalize.save_hook = this;
}
public static void main(String[] args) throws InterruptedException {
save_hook = new Finalize();//对象
//对象第一次拯救自己
save_hook = null;
System.gc();
//暂停0.5秒等待他
Thread.sleep(500);
if (save_hook != null) {
save_hook.isAlive();
} else {
System.out.println("好了,现在我死了");
}
//对象第二次拯救自己
save_hook = null;
System.gc();
//暂停0.5秒等待他
Thread.sleep(500);
if (save_hook != null) {
save_hook.isAlive();
} else {
System.out.println("我终于死亡了");
}
}
}
4.5.2finalize方法缺点(这个方法基本不用)
大多数可以使用try,catch中的finally方法进行补救
1.在业务代码不熟悉的时候会导致有个对象杀不死
2.会影响到JVM的对象以及分配回收速度,要连续判断这个对象要不要复活,他本来该销毁,但是又复活了需要JVM再将他分配到应用阶段
3.可能造成对象再次复活(诈尸)
4.6终结阶段(Finalized)
对象的finalize()函数执行完成后,对象仍处于不可达状态,该对象进程终结阶段。
4.7对象内存空间重新分配阶段(Deallocaled)
GC对该对象占用的内存空间进行回收或者再分配,该对象彻底消失。
5.一些GC的特殊情况
GC执行的时候需要获取CPU的执行权,强占时间片
假如第一次GC抢占到CPU的执行权的时候是这样的情况,这样就是4要被回收掉,但是这时GC的时间片结束了,这时计数器会记录到4要被回收
但是这时业务线程将3指向了4,这样就导致有问题
或者反过来,一开始3-4之间是有指向的,但是下一次GC的时候3-4指向没有了,这样也会出现问题。
这样的情况时因为GC与业务线程并行导致的,只要GC的时候把所有业务线程停掉就行,GC完了再进行业务线程,Stop The World