JVM笔记
JVM内存结构
一、程序计数器
线程私有(指令最终由cpu执行,所以必须线程私有),记录了下一条指令的地址,不会出现内存溢出
二、虚拟机栈
线程运行时需要的一个内存空间,Java虚拟机栈和线程是同时创建的,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈的组成部分:栈帧,一个栈帧也就对应着一个方法的调用,所以栈帧也就是方法运行时需要的内存(参数,局部变量,返回地址)
栈遵循先进后出原则:方法1调用方法2,肯定方法1先入栈,方法2后入栈,所以方法2执行完成后,先出栈,方法1后出
具体参照:https://zhuanlan.zhihu.com/p/45354152
每个线程只能有一个活动栈帧,对应着正在执行的方法
问题:
1、垃圾回收是否涉及栈内存?
否,因为栈内存会随着出栈而自动回收掉
2、栈内存是否设置的越大越好呢?
否,-Xss1m 可以设置最大栈内存为1M,设置的越大,理论上虚拟机能够支持的线程数就会变少,因为一个线程对应一个虚拟机栈
3、方法内的局部变量是否线程安全?
不一定,要看局部变量是否逃离方法范围,如果局部变量作为参数或者是返回值,就可能存在线程安全问题
4、栈内存溢出可能的问题?
1)栈帧过多(例如常见的递归调用)
2)栈帧过大(不常见,一般的栈内存为1M,所以不容易出现超出内存的情况)
5、cpu占用过多?
1)top命令可以查看应用的内存占用情况,找到进程id(pid)
2)通过ps H -eo pid(进程id),tid(线程id),%cpu | grep pid 可以定位到哪个线程对cpu的占用过高
3)然后通过jstack命令 jstack pid 查看后台正在进行的线程信息
"Thread-11" #52 daemon prio=5 os_prio=0 tid=0x00007f5989c16000 nid=0x4c in Object.wait() [0x00007f58aa098000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at io.netty.util.concurrent.DefaultPromise.await(DefaultPromise.java:252)
- locked <0x00000000bd416658> (a io.netty.channel.AbstractChannel$CloseFuture)
at io.netty.channel.DefaultChannelPromise.await(DefaultChannelPromise.java:131)
at io.netty.channel.DefaultChannelPromise.await(DefaultChannelPromise.java:30)
at io.netty.util.concurrent.DefaultPromise.sync(DefaultPromise.java:403)
at io.netty.channel.DefaultChannelPromise.sync(DefaultChannelPromise.java:119)
at io.netty.channel.DefaultChannelPromise.sync(DefaultChannelPromise.java:30)
at com.xxl.rpc.remoting.net.impl.netty_http.server.NettyHttpServer$1.run(NettyHttpServer.java:70)
at java.lang.Thread.run(Thread.java:748)
4)nid就是线程id(注意这里是十六进制的),然后判断哪个地方的代码有问题
6、线程死锁问题
通过jstack命令 jstack pid 查看后台正在进行的线程信息,可以发现Found one Java level deadlock这样的信息,下面会有具体的信息,可以看到哪两个线程存在线程死锁问题。
三、本地方法栈
是与操作系统打交道的方法,也就是native方法,这部分方法使用的内存空间是由本地方法栈提供,线程私有
四、Heap堆
使用new创建的对象,都会使用到堆内存
特点
1)线程共享,所以堆中的对象都需要考虑线程安全问题
2)有垃圾回收机制
堆内存溢出排查
1、jps工具,查看当前系统中有哪些java进程
2、jmap工具,查看堆内存占用情况 jmap -heap pid
3、jconsole工具,图形界面的多功能监测工具,可以连续监测
Q:垃圾回收后,内存占用仍然很高?
1、jps可以看到进程id
2、jmap -heap pid,看下eden区占用,old区占用
3、可以使用jconsole工具执行一次垃圾回收,再看看内存占用情况
4、使用另一种监控工具jvisualvm,可以dump内存快照,可以看到存在的对象以及对象占用空间的大小,点击进去可以具体的原因
五、方法区
1、虚拟机线程共享的一块区域
2、存储类的结构相关的信息(filed-成员变量,方法数据-method data,成员方法和构造器方法的代码-the code for methods and constructors,特殊方法-special methods),运行时常量池
3、启动时创建,逻辑上是堆的组成一部分,具体实现上不同的jvm厂商有所不同,比如hotspot1.7之前永久代就是堆内存,现在是元空间就不在堆内存,使用操作系统内存
4、也会内存溢出 java.lang.OutOfMemoryError: Metaspace
Q:元空间内存溢出?
默认是没有上限的使用的系统内存,可以使用-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
设置元空间初始大小以及最大可分配大小
实际生产环境中,使用字节码技术(CGLib)生成的一些动态代理类,就会出现元空间内存溢出,但是目前使用的系统内存,并且是自己的垃圾回收机制,所以很少出现元空间内存溢出
5、运行时常量池
一个类编译成二进制字节码,包括类的基本信息,常量池,类的方法定义(虚拟机指令)。
编译后,常量池实质上是一张常量表,提供指令符号查找。
运行时常量池,就是把常量池中的信息加载到内存中,以字符串举例,一开始只是一些符号,当代码运行到那一行后,那一行的对象才会被创建
StringTable-字符串常量池,
String a = "a";
String b = "b";
String x = "ab";//在字符串常量池中的
String y = a + b;//在堆内存中,相当于new StringBuilder().append("a").append("b").toString(),new String("ab")
String z = "a" + "b";//javac在编译期的优化处理
x == y false
x==z true
String w = new String("a")+new String("b");
w == x false
String w2 = w.intern();//尝试放入字符串常量池中,如果池中存在,则不放入,不存在则放入,之后返回
w2 == x true
StringTable从1.7开始移到堆中存放,可以被垃圾回收掉
StringTable的性能调优
常量池底层使用StringTable数据结构保存字符串引用,实现和HashMap类似,根据字符串的hashcode定位到对应的数组,遍历链表查找字符串,当字符串比较多时,会降低查询效率。当冲突次数超过100次就会自动做rehash,但是表如果size小的话,那再怎么rehash也是冲突,只能换来的是不断做rehash,因此性能会比较差。所以建议大家将这个参数设置成一个比较大的质数,减少冲突。通过perf能看到看到stringtable的lookup方法占cpu很高 就是这个问题。所以可以通过调整hash表的长度来进行调优 -XX:StringTableSize,当程序需要创建大量的重复的字符串时,建议调用String.intern进行入池操作。
六、直接内存(操作系统内存)
常见于NIO操作时,用于数据缓冲区。
特点:
分配回收成本较高,但读写性能高
不受JVM内存回收管理
也会造成内存溢出
Q:如何分配直接内存
可以通过ByteBuffer.allocateDirect()分配直接内存,内部使用的是Unsafe类实现
Q:释放直接内存
垃圾回收不能回收直接内存,所以通过Unsafe类回收直接内存,ByteBuffer内部使用了一个Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer被回收,那么会有ReferenceHandler线程(守护线程)通过Cleaner的clean方法来释放直接内存。Unsafe类分配内存unsafe.allocateMemory()和unsafe.freeMemory(),释放内存unsafe.freeMemory()
-XX:+DisableExplicitGC:禁用显式GC,可能会影响,回收直接内存,这时可以手动调用Unsafe操作
参考:https://zhuanlan.zhihu.com/p/44694290