目录
Java对象内存布局
markWord 数据结构
JVM 内存结构(JDK1.8)
-
程序计数器:
线程私有,记录代码执行的位置. -
Java虚拟机栈:
线程私有,每个线程都有一个自己的Java虚拟机栈 ,默认大小是1M -
本地方法栈:
线程私有,每个线程都有一个自己的本地方法栈,Java虚拟机栈加载的是普通方法,本地方法加载的是native
修饰的方法.native
:表示这个方法不是java原生的,是由C或C++实现的 -
堆:
线程共享,用于存放对象,new的对象都存储在这个区域 -
元空间:
线程共享,存储class信息,类的信息,方法的定义,静态变量,常量池 等
- 堆和元空间是线程共享的,在Java虚拟机中只有一个堆、一个元空间,并在JVM启动的时候就创建,JVM停止才销毁。
- 栈、本地方法栈、程序计数器是每个线程私有的,随着线程的创建而创建,随着线程的结束而死亡。
每个存储位置会产生的异常:
堆内存结构(JDK1.8)
- 年轻代:Eden+S0+S1, S0和S1大小相等, 新创建的对象都在年轻代
- 老年代:经过年轻代 多次垃圾回收存活下来的对象存在年老代中.
GC垃圾回收
JVM的垃圾回收动作可以大致分为两大步:
- 如何发现垃圾
- 如何回收垃圾
线程私有的不存在垃圾回收,只有线程共享的才会存在垃圾回收,所以堆中存在垃圾回收.
如何发现垃圾
常见的用于「发现垃圾」的算法有两种,引用计数算法 和 根搜索算法。
-
引用计数算法
堆中的对象每被引用一次,则计数器加1,每减少一个引用就减1,当对象的引用计数器为0时可以被当作垃圾收集。
- 优点:快。
- 缺点:无法检测出循环引用。如两个对象互相引用时,他们的引用计数永远不可能为0。
-
根搜索算法(也叫根可达性分析)
根搜索算法是把所有的引用关系 看作一张图,从根节点(GCRoot)开始遍历,找出被根节点引用的节点,对于没有被根节点指向的节点,即可以当作垃圾。
- Java中可作为GCRoot的对象有:
- java虚拟机栈中引用的对象
- 本地方法栈引用的对象
- 元空间中静态属性引用的对象
- 元空间中常量引用的对象
- Java中可作为GCRoot的对象有:
如何回收垃圾
Java中用于「回收垃圾」的常见算法有4种:
-
标记-清除算法(markandsweep)
首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
缺点: 标记清除之后会产生大量的不连续的内存碎片
-
标记-整理算法
首先标记出所有需要回收的对象,让所有存活的对象移动到另一个位置,在移动过程中清理掉可回收的对象,这个过程叫做整理。
优点:内存被整理后不会产生大量不连续内存碎片
缺点:耗时耗力 -
复制算法(copying)
将空间分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。
缺点:可使用的内存只有原来一半。在某一个时刻点,总有一个 S 是空的,可能是S0 也可能是S1.。
-
分代收集算法(generation)
当前主流JVM都采用分代收集(GenerationalCollection)算法,这种算法会根据对象存活周期的不同将内存划分为年轻代、年老代,不同生命周期的对象可以采取不同 的回收算法,以便提高回收效率。-
年轻代(YoungGeneration)
- 所有新生成的对象首先都是放在年轻代的。
- 新生代内存按照
8:1:1
的比例分为一个eden区和两个Survivor(s0,s1)区。大部分对象在Eden区中生成。
回收流程:
回收时先将eden区存活的对象复制到一个s0区,然后清空eden区,当这个s区,也存放满了时,则将eden区和s0区存活对象复制到另一个s1区,然后清空eden和这个s0区,此时s0区是空的,然后将s0区和s1区交换,即保持s1区为空,如此往复.-
特殊情况:当一个大对象不足于存放到eden区时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次
FullGC
,也就是新生代、老年代都进行回收。 -
新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高
-
年老代(OldGeneration)
-
在年轻代中经历了 N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。默认是15次,且最大15次。
-
内存比新生代也大很多(大概是2倍),当老年代内存满时触发FullGC,
FullGC发生频率比较低,老年代对象存活时间比较长,存活率比较高。
-
-
元空间-持久代(PermanentGeneration)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
-
JVM调优参数
这里只给出一些常见的性能调优的参数及其代表的含义。
-
-Xms8g
: 设置JVM中堆初始堆大小为8g。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -
-Xmx8g
: 设置JVM中堆最大可用内存为8g。 -
-Xmn4g
: 设置年轻代大小为4G。 -
-XX:NewRatio=2 设置年轻代(包括Eden和两个Survivor区)与年老代的比值
-
-XX:SurvivorRatio=8 ,所以默认值 Eden:S0:S1=8:1:1。
-
-Xss1m:设置每个线程的栈大小
-
-XX:MaxMetaspaceSize=128m
: 设置元空间最大为为128m , -
-XX:MetaspaceSize=128m
用于设置元空间的初始大小, 默认值约21M -
-XX:MaxTenuringThreshold=15:设置垃圾最大年龄。最大不超过15。
为什么元空间初始和最大的大小要设置一样?
因为元空间是使用的直接内存,当内存不够时会重新申请空间,进行一次FullGC
,并且会产生一个比较长的STW
(时停,暂停其他操作),会非常影响性能。
并且每次扩容不是一部到位的,它会一点一点的申请空间,这样会产生多次 FullGC
和 STW
。
所以初始直接给最大内存可以避免这种情况。
什么是STW?有什么影响?
STW,是Stop-The-World
的缩写,Stop-The-World是指系统在执行特定操作时,必须暂停所有的应用程序线程。
比如在Java中,当需要进行垃圾回收的时候,垃圾回收器需要停止应用程序的所有线程,以便可以安全地识别和回收不再使用的对象。这个过程我们就会称之为是Stop The World
了。
垃圾回收器
-
ParNew
+CMS
回收算法:ParNew【年轻代】【复制算法】,CMS【年老代】【标记清除】
-
Parallel Scavenge
+Parallel Old
回收算法:Parallel Scavenge【年轻代】【复制算法】,Parallel Old【年老代】【标记整理】
-
G1
:JDK9 默认的收集器回收算法:年轻代【复制算法】,年老代【标记-整理】
- 优点:
- 可以处理年轻代和年老代,
- 对内存进行了分区,缩小STW的规模,提高了垃圾回收的性能
- 优点:
JVM监控工具
-
JConsole:可以用于监视 JVM 的性能和资源利用情况
-
VisualVM:强大的JVM图形化监控工具,比jconsole强大完善
-
Arthas:阿里巴巴开发并开源的 Java 应用诊断工具
JVM故障诊断工具
jps
:虚拟机进程状况jinfo
:java配置信息工具jhat
:虚拟机堆转储快照分析工具jstat
:虚拟机统计信息监控工具jmap
:java内存映像工具jstack
:java堆栈跟踪工具
JAVA类加载器有哪些?
-
启动类加载器(Bootstrap Class Loader):也称为根类加载器,它负责加载Java虚拟机的核心类库,如java.lang.Object, java.lang.String等。
-
扩展类加载器(Extension Class Loader):它是用来加载Java扩展类库的类加载器。
扩展类库包括javax和java.util等包 -
应用程序类加载器(App Class Loader):也称为系统类加载器,它负责加载应用程序的类。
-
自定义类加载器(custom Class Loader):开发人员可以根据需要实现的类加载器。
类加载器的工作原理
类加载器的工作可以简化为三个步骤:
- 加载(Loading):根据类的全类名,定位并读取类文件的字节码。
- 链接(Linking):将类的字节码转换为可以在虚拟机中运行的格式。链接过程包括三个阶段:
- 验证(Verification):验证字节码的正确性和安全性,确保它符合Java虚拟机的规范。
- 准备(Preparation):为类的静态变量分配内存,并设置默认的初始值。
- 解析(Resolution):将类的符号引用(比如方法和字段的引用)解析为直接引用(内存地址)。
- 初始化(Initialization):执行类的初始化代码,包括静态变量的赋值和静态块的执行。
双亲委派机制图解
类加载器 采用了 双亲委派模型(Parent Delegation Model)来加载类。
当一个类加载器需要加载类时,它会 首先委派给其 父类加载器 加载。
如果父类加载器无法加载,才由该类加载器自己去加载。
这种层级关系使得类加载器能够 实现类的共享和隔离,提高了代码的 安全性 和 可靠性
为什么需要双亲委派?
- 通过双亲委派机制,可以 避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
- 通过双亲委派机制,可以 保证类安全性。因为BootstrapClassLoader在加载的时候,只会加载JAVA_HOME中的jar核心类库,如java.lang.String,那么这个类是不会被加载。
内存泄漏和内存溢出的区别是什么?
-
内存泄漏指的是程序中分配的内存在不再需要时没有被正确释放或回收的情况。这会导致程序持续占用内存,随着时间的推移,可用内存逐渐减少,最终可能导致程序性能下降或崩溃。
-
内存溢出指的是程序试图分配超过其可用内存空间的情况。这通常会直接导致Java程序崩溃。