JVM虚拟机
JVM内存
JVM内存可以分为这么几块:堆、栈、本地栈、方法区、程序计数器。
- 栈:这里的栈为虚拟机栈,栈是线程私有的,在调用方法时它还会在栈中生成一个栈帧用于存储方法调用时产生的一些临时数据例如局部变量表,操作数栈、动态链接和方法返回地址等等,方法调用完栈帧就会被销毁。并且栈中还会存储堆中对象的引用
- 本地栈:他的功能和栈类似不过他是为本地方法服务的也就是Native 修饰的方法
- 程序计数器:因为多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,而程序计数器则用于保存程序运行的位置
- 方法区: 他是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量以及编译后的代码(.class文件)等等。并且他属于jvm规范里面的概念,jdk1.7之前实现为永久代和部分堆空间1.8后实现为元空间和部分堆空间。垃圾收集行为在出现Full GC的时候也会对这个区域进行清理。
- 堆:堆是jvm中最大的一部分内存,他是被所有线程共享的。他用于存储程序运行时创建的一系列对象。并且堆里面又分为新生代和老年代。其中新生代里面用于存储一些生命周期较短的对象。老年代则用于存储一些生命周期较长的对象。并且新生代里面还分为Eden区和Survivor区通常为1个Eden区,2个Survivor区(from 和 to),新创建的对象会存在Eden区。如果Eden区的内存满了那么就会发起一次Minor GC,把Eden区和其中一个Survivor区中存活的对象复制到另一个Survivor区中。对象在Survivor区中每熬过一次 Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15)时,就会被晋升到老年代中。如果在晋升老年代的时候发现老年代的空间不够那么就会进行一次Full GC,Full GC会对整个堆以及方法区进行清理。如果进行Full GC后老年代的空间仍然不够那么就会出现内存溢出。
方法区补充:
方法区是jvm规范里面的概念,而他的实现会随着jdk版本的变更而变更。jdk1.7之前方法区的实现就是永久代, 1.7 把常量池和静态变量放入了堆中,也就是方法区由永久代和堆实现。1.8 把永久代删除使用元空间,也就是方法区由元空间(类信息)和堆实现(常量池、静态变量)。
所以在配置方法区的大小的时候1.8和其他版本的配置方式是不同的。JDK7及以前:-XX:PermSize设置永久代初始大小。-XX:MaxPermSize设置永久代最大可分配空间。JDK8及以后:可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置元空间初始大小以及最大可分配大小。如果不指定元空间的大小,默认情况下,元空间最大的大小是系统内存的大小,元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。
垃圾收集机制
首先垃圾收集存在于堆以及方法区中,GC有两种类型:Minor GC和Full GC。其中Minor GC用于对堆中新生代的垃圾进行收集而Full GC用于对堆以及方法区中的所有垃圾进行收集。因为Full GC很慢而且很浪费资源,所以我们应该尽量减少Full GC的次数,发生Full GC的情况有:老年代和方法区被写满了、System.gc()被显示调用、上一次GC之后堆的各域分配策略动态变化等等。
判断对象是否可以被回收:
判断一个对象是否可以被回收的方法有两种:引用计数法和可达性分析法
- 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。不过这样就会有一个问题。那就是如果两个对象是在堆内互相引用(对象中的都有一个属性指向对方),那么这个时候计数将不可能为0,这样就会造成内存泄漏。
- 可达性分析法:目前主要使用的就是这种方法。简单来说就是从gcroot入口有个引用链,如果堆内一个对象没有跟任何引用链关联的话,就被视为需要被垃圾回收的对象。说的直白点就是,从堆外出发,类似于链表或者树一样,如果有一个堆内的对象不能顺着这个链表或者树节点走到最初的根节点则视为对象不可达。
- 可被当做gc root的对象:虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象
需要注意的是在对象回收之前会调用对象的finalize()方法,在finalize()方法中用于释放非Java 资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存(比如C语言的malloc()系列函数),或者复活对象(给对象添加引用)。并且finalize()方法只会被调用一次。
垃圾回收算法:
垃圾回收算法主要有四种:复制算法、标记-清除算法、标记-整理算法以及分代收集算法。
目前主要使用的就是分代收集算法,所谓分代收集就是在堆中可以分为新生代和老年代,然后我们可以在新生代和老年代根据不同特点使用不同的收集算法。例如通常我们在新生代中使用复制算法,在老年代中使用标记-清除算法或标记-整理算法。
GC具体使用什么垃圾回收算法以及通过什么方式来回收垃圾取决于使用的是什么垃圾收集器
垃圾收集器
垃圾收集器从位置来分可以分为新生代的垃圾收集器、老年代的垃圾收集器以及跨新生代和老年代的收集器,而从收集器类型分可以分为单线程收集器、多线程并行收集器以及多线程并行并发收集器。
| 收集器 | 收集对象和算法 | 收集器类型及特点 |
|---|---|---|
| Serial | 新生代,复制算法 | 单线程 |
| ParNew(Parallel New) | 新生代,复制算法 | 并行的多线程收集器。和Serial基本没区别,只不过他是多线程的,停顿时间比Serial少 |
| Parallel Scavenge | 新生代,复制算法 | 并行的多线程收集器。相对于ParNew可以更高效的利用cup从而提高吞吐量,尽快地完成程序的运算任务。并且我们不仅可以手动设置垃圾收集器的吞吐量,他还可以自动适配调节吞吐量。他是一个关注吞吐量的垃圾收集器 |
| Serial Old | 老年代,标记整理算法 | 单线程 |
| Parallel Old | 老年代,标记整理算法 | 并行的多线程收集器。和Serial old基本没区别,只不过他是多线程的,停顿时间比Serial old少 |
| CMS | 老年代,标记清除算法 | 并行与并发收集器。以获取最短回收停顿时间为目标的收集器,不过由于他是并发的和用户程序一起运行所以会产生浮动垃圾,并且由于他采用标记清除会产生空间碎片,对cpu要求很高。并且他需要在内存用尽前,完成回收操作,否则会导致并发回收失败 |
| G1 | 跨新生代和老年代;标记整理和复制 | 并行与并发收集器。他弱化了分代的概念引入了分区的思想,将整个堆空间分成若干个大小相等的内存区域。每个区域可以是Eden、Survivor、old以及Humongous中的一种。通过这种分区的方式大部分的垃圾收集操作就只在分区内执行,而不是整个堆或者整个代。他适用于大内存,多处理器的机器,能够进一步降低暂停时间,同时兼顾良好的吞吐量 |
在使用的时候新生代和老年代收集器可以根据需要进行组合

垃圾收集器详解:
-
Serial、ParNew和Parallel Scavenge类似他们都是采用复制算法,用于新生代的垃圾收集,不同的是Serial是单线程的而ParNew和Parallel Scavenge是并行多线程的,并且Parallel Scavenge相对ParNew可以更高效的利用cup从而提高吞吐量,尽快地完成程序的运算任务。所以Parallel Scavenge相对于ParNew更关注于吞吐量。他们在运行垃圾收集的时候都会暂停用户进程
-
Serial Old和Parallel Old也类似他们都采用标记整理算法,用于老年代的垃圾收集。不同的是Serial Old是单线程的而Parallel Old是并行多线程的。他们在运行垃圾收集的时候都会暂停用户进程
-
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。他是多线程并发的,使用的算法是标记清除算法,他收集的过程分为七个步骤:
-
初始标记:会标记老年代中所有的GC Roots对象以及年轻代中活着的对象引用到的老年代的对象,在进行初始标记的时候会发生停顿(STW -Stop the world)但是停顿的时间很短
-
并发标记:根据初始标记阶段标记的对象找出与之关联的存活对象进行标记。这个阶段因为是并发运行的,所以在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象的情况,所以这个阶段还负责将引用发生改变对象所在的Card标记为Dirty状态。它在整个回收过程中耗时最长,但不需要停顿。
-
预清理阶段:阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty的Card
-
可终止的预处理:主要工作是尽可能承担更多的并发预处理工作,例如发起一次Minor GC对年轻代进行回收,从而减轻在重新标记(下一个阶段)的停顿时间。
-
重新标记:该阶段的任务是完成标记整个年老代的所有的存活对象。重新标记的内存范围是整个堆,包括年轻代和老年代。因为如果老年代的对象被新生代的对象引用了,即使这些新生代的对象已经不可达了,被引用的老年代对象也会被当做GC Roots。所以需要清理新生代的对象。为了减少重新标记的时间所以我们可以让他在可终止的预处理阶段对新生代进行一次垃圾回收。这个阶段是会发生停顿的。
-
并发清除:这个阶段主要是清除那些没有标记的对象并且回收空间,但由于CMS并发清理阶段用户线程还在运行着,所以在清理的时候也会产生垃圾,而这些垃圾只能交给下次GC的时候清理。这部分垃圾被称为浮动垃圾
-
并发重置:收尾工作重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
-
注意能和CMS匹配使用的年轻代垃圾收集器有Serial和ParNew,而Parallel Scavenge是不能和CMS一起使用的,原因是因为Parallel Scavenge没有使用HotSpot虚拟机中通用的GC框架,所以不能跟使用了通用GC框架的CMS搭配使用
-
-
G1
-
G1也是多线程并发的,他弱化了分代的概念引入了分区的思想,将整个堆空间分成若干个大小相等的内存区域(Region),每个区域可以是Eden、Survivor、old以及Humongous中的一种。其中这里的Eden、Survivor、old就是新生代的Eden、Survivor和老年代,而Humongous表示Region存储的是巨型对象(当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为Humongous)。通过这种分区的方式大部分的垃圾收集操作就只在分区内执行,而不是整个堆或者整个代。
-
G1的垃圾收集周期主要有4种类型:年轻代收集周期、并发标记周期、混合收集周期和full GC
- 年轻代收集周期:当Eden区满了之后就会触发年轻代收集,将Eden区存活的对象复制到Survivor区或老年区中
- 并发标记周期:当老年代对于堆空间的占比到达某个值,就会触发并发标记周期,这个阶段将会为混合收集周期识别垃圾最多的老年代分区。并且这个周期包括5个阶段,分别是:
- 初始标记:这个阶段会发生停顿,负责标记所有GC Root根对象
- 根分区扫描:在初始标记暂停结束后,年轻代收集也完成对象复制到Survivor的工作,此时为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根。
- 并发标记:这个阶段简单的理解就是根据前面两个阶段标记的GC Root对象,找到存活的对象然后标记,以及收集各个Region的存活对象信息(这里是简单的理解,具体会很复杂)
- 重新标记阶段:这个阶段会发生停顿,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
- 清理阶段:回收没有存活对象的Region并加入可用Region列表中
- 混合收集周期:在混合收集过程中会进行年轻代收集以及部分老年代收集,这里收集那些老年代分区由并发标记周期中获取,这个周期完成后会恢复到常规的年轻代垃圾收集周期
- Full GC:当从年轻代分区拷贝存活对象以及老年代分区转移存活对象,无法找到可用的空闲分区或分配巨型对象时在老年代无法找到足够的连续分区那么就会触发Full GC,G1中Full GC的效率是非常低的。
-
G1可以说是CMS的替代,他适用于大内存,多处理器的机器。他能够进一步降低暂停时间,同时兼顾良好的吞吐量
-
垃圾收集器配置:
查看一下当前 JVM 默认参数的命令:java -XX:+PrintCommandLineFlags -version
查看GC的详细信息命令:java -XX:+PrintGCDetails -version
在JVM中为我们提供了配置垃圾收集器的参数,他提供了很多种组合
| 参数 | 回收器 |
|---|---|
| -XX:-UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
| -XX:-UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收 |
| -XX:-UseParallelGC | 虚拟机运行在Server模式下的默认值,所以我们使用的jdk默认值都是这个(JDk9以上的默认值是UseG1GC),打开此开关后,使用Parallel Scavenge + Serial Old (PS Mark Sweep)的收集器组合进行内存回收 |
| -XX:-UseParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
| -XX:-UseConcMarkSweepGC | 打开此开关后,使用ParNew+ CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
| -XX:-UseG1GC | 打开此开关后,使用G1收集器组合进行内存回收 |
注意:其实自从JDK7u4开始,就对 -XX:+UseParallelGC默认的老年代收集器进行了改进,改进使得HotSpot VM在选择使用 -XX:+UseParallelGC时,会默认开启 -XX:+UseParallelOldGC,也就是说默认的老年代收集器是 Parallel Old。综上,JDK8中默认的选择是-XX:+UseParallelGC,是 Parallel Scavenge + Parallel Old组合。
可以参考java8文档:其中有这么一段话: Parallel compaction is enabled by default if the option -XX:+UseParallelGC has been specified. The option to turn it off is -XX:-UseParallelOldGC. 大意如下:如果指定了-XX:+UseParallelGC参数,并行压缩默认是启用的。可以使用-XX:-UseParallelOldGC来禁用该功能。 也就是说当指定了参数-XX:+UseParallelGC,则默认也指定了-XX:+UseParallelOldGC。即默认使用了 Parallel old垃圾收集器。
JVM调优
查看JVM状态
查看jvm的内存常用的工具有两个jconsole和jvisualvm,这两个都是jdk自带的工具。可以直接在jdk安装目录下的bin目录中找到,所以配置了jdk的环境变量之后可以直接在命令行中敲这两个命令就可以启动程序
Jconsole
jconsole中可以查看jvm的内存并且可以查看不同部分的内存例如年轻代Eden区和Survivor区、老年代以及方法区等等。并且还可以查看线程的状态检测死锁等等。这个工具使用的时候挺简单的,主要是需要注意下面的问题:
-
在内存界面的详细信息中GC时间下的
PS MarkSweep表示老年代的垃圾收集器,jdk8中代表的是Parallel Old收集器,而PS Scavenge表示Parallel Scavenge收集器为年轻代收集器,而后面接着的时间为垃圾收集器所占用的总时间 -
jconsole的MBean,其实就是一种规范的JavaBean,他会实现一套规范标准。MBean会被注册到MBeanServer中,然后我们就可以通过JMX的客户端来连接MBeanServer访问MBean中的数据。这个了解一下就行,平时根本不会用到。
连接
jconsole可以连接本地的java进程进行监控也可以远程连接其他机器上的java进程。本地连接直接选择本地要监控的进程连接进去就可以了。而远程连接则需要配置一下。
远程连接:
如果要远程监控java进程那么目标服务器在启动java程序的时候需要设置一些参数
-Dcom.sun.management.jmxremote.ssl=false # 是否需要ssl 安全连接方式
-Dcom.sun.management.jmxremote.authenticate=false #是否需要秘钥
-Dcom.sun.management.jmxremote.port=8081 #自定义jmx 端口号
-Dcom.sun.management.jmxremote.rmi.port=8081 #当存在防火墙等网络访问限制时,可通过 com.sun.management.jmxremote.rmi.port 参数指定 RMI 连接器所使用的端口并进行开放。在这种场景下,必须设置此参数。
-Djava.rmi.server.hostname=8.129.86.120 #本机ip地址,但要确保hostname -i得到的是本机的真实ip,而不是127.0.0.1
示例:
java -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=8081 -Dcom.sun.management.jmxremote.rmi.port=8081 -Djava.rmi.server.hostname=8.129.86.120 -jar myadmin-main-1.0.0.jar
注意:在这个例子中并没有设置认证,所以在远程连接的时候不用用户以及密码也可以连接
具体可以参考阿里云文档:
https://help.aliyun.com/knowledge_detail/100768.html
远程连接设置认证
设置认证需要对jre中的配置文件进行配置,
首先进入jdk的安装目录/usr/local/jdk/jdk1.8.0_171/jre/lib/management/中,其中目录下的jmxremote.access为远程连接的用户配置文件而jmxremote.password.template密码模板文件。
配置用户:
在jmxremote.access中添加用户我们只需要在最后添加用户名然后设置用户的权限就可以了,而权限有两个一个是readonly只读权限,一个是readwrite可读可写。例如
monitorRole readonly
controlRole readwrite \
create javax.management.monitor.*,javax.management.timer.* \
unregister
#上面两个是文件默认就有的
root readonly
配置密码:
给用户添加密码我们需要将jmxremote.password.template文件复制为jmxremote.password然后再文件中添加密码就可以。例如
root password123
最后启动的时候将参数-Dcom.sun.management.jmxremote.authenticate改为true。然后指定密码文件-Dcom.sun.management.jmxremote.pwd.file=/usr/local/jdk/jdk1.8.0_171/jre/lib/management/jmxremote.password所以最后的命令为:
java -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.pwd.file=/usr/local/jdk/jdk1.8.0_171/jre/lib/management/jmxremote.password -Dcom.sun.management.jmxremote.port=8081 -Dcom.sun.management.jmxremote.rmi.port=8081 -Djava.rmi.server.hostname=8.129.86.120 -jar myadmin-main-1.0.0.jar
注意如果文件的权限不足我们可以给文件重新授予权限chmod 600 jmxremote.password jmxremote.access
这样启动后我们连接就需要给出用户名和密码才能连接
jvisualvm
这个工具功能其实和jconsole差不多,他们都可以查看jvm的内存以及线程。并且远程连接的时候都是基于JMX连接的,所以远程连接的配置跟jconsole一样(在目标进程启动的时候配置好参数直接连接就可以了)
注意:jvisualvm远程连接的时候需要新增主机然后再新增连接

jvisualvm跟jconsole的区别
- jvisualvm优点
- 在jvisualvm中可以将数据dump出来然后分析
- 在jvisualvm中可以很直观的查看线程的情况
- jvisualvm中的抽样器可以查看某个类所占的字节大小以及实例的数量,所以通过这个可以检查某个类是否出现了内存泄漏
- jconsole优点
- 可以很清晰的查看jvm堆内存中年轻代、老年代、元空间中的内存情况,这个功能jvisualvm中默认是没有提供的,需要安装对应的插件
总结:
jvisualvm通常用于查看线程运行情况以及检查死锁,检查内存泄漏。而jconsole用于对堆内存进行分析,可以查看年轻代Eden区和Survivor区、老年代以及方法区详细数据,然后可以根据实际情况对内存进行配置,当然也可以用于查看线程的运行情况。当然其实两个都差不多,不过jvisualvm会强大一点因为他可以安装额外的插件实现更多的功能
JVM配置
jvm问题其实就两种情况,一种是运行的慢,一种是出现内存溢出报错。这两种情况的解决方案其实都差不多,只不过内存溢出报错的时候可以查看具体的错误。
jvm运行慢问题
首先系统运行慢的原因有很多可能是网络、数据库数据太多当然也可能跟jvm的垃圾回收有关,如果跟jvm垃圾回收有关那就是full GC太频繁导致的。而full GC很频繁的原因也有很多需要我们逐个排除。
首先确认是否是由于jvm频繁full GC导致的,判断是否由于频繁full GC导致,我们可以对GC的情况进行监控,对GC的监控我们可以通过jconsole查看GC的次数以及GC的时间。如果次数很多并且占用时间很长那么就是频繁GC导致的。
如果是GC很频繁我们最先需要确认的是是否出现了内存泄漏,因为内存泄漏会导致老年代的内存持续增加从而导致频繁的full GC,最终会导致内存溢出。而排除是否内存泄漏我们可以通过jvisualvm来对jvm的情况进行查看,首先可以查看堆的内存使用图看是否是持续增长的,如果是持续增长的,我们可以通过jvisualvm中的抽样器查看内存中对象所占的内存以及实例数。如果我们对象占用的内存很大或有很多的实例,那么就可以对这个对象涉及的代码进行分析,从而找的内存发生泄漏的地方。当然如果通过jvisualvm中的信息找不到内存泄漏的地方我们可以将堆的信息导出来,然后用Java堆分析器例如Eclipse中的MAT工具对堆信息进行分析,不过如果堆数据很大那么导出需要很长时间
注意:HashMap、ArrayList等等这些如果占用很多是没有关系的因为很多地方都会使用,而如果出现自己的对象占用内存很多并且对象中有HashMap、ArrayList等等那么他们自然就很多,所以jvisualvm中的抽样器中主要是查看我们自己的对象是否有很多,如果我们自己的对象很多那么这个对象涉及的代码就可能出现的内存泄漏
如果GC很频繁是由于jvm空间分配有问题,例如老年代太小导致出现频繁的full GC那么我们就可以修改年轻代和老年代的比值。
内存溢出问题
发生内存溢出的时候我们可以根据报错信息进行对应的处理,内存溢出的情况,OutOfMemoryError的类型:
- java.lang.OutOfMemoryError: Java heap space
- 堆对空间满了导致内存溢出,这种需要考虑是否出现了内存泄漏,或堆空间的太小了
- java.lang.OutOfMemoryError: PermGen space 或 java.lang.OutOfMemoryError: Metadata space
- 1.8之前存在永久代所以会出现PermGen space(Permanent Generation space),而1.8之后永久代被替换为元空间了所以会出现 Metadata space,并且默认情况下Metadata space默认为系统内存的大小,元空间可以一直扩大。但是我可以设置他的最大值,当设置最大值后如果超过这个值那么就会出现java.lang.OutOfMemoryError: Metadata space。如果出现PermGen space或Metadata space说明加载的类太多了,那么我们可以增加永久代或元空间的大小
- java.lang.OutOfMemoryError: unable to create new native thread
- 线程空间满了,其实线程基本只占用heap以外的内存区域,也就是这个错误说明除了heap以外的区域,无法为线程分配一块内存区域了,这个要么是内存本身就不够,要么heap的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了
- java.lang.StackOverflowError
- 栈溢出,一般就是递归太深或没返回,或者循环调用造成,可以对相关代码进行优化
内存溢出通常就java.lang.OutOfMemoryError: Java heap space需要考虑是否发生内存泄漏,而其他的可以通过jconsole查看堆中的内存情况从而进行对应的调整就可以了。
内存泄漏的情况
常见的内存泄漏情况:
- 资源未关闭,例如数据库连接,网络连接(socket)和io连接,需要显式的调用close方法将其连接关闭,否则涉及的对象是不会自动被GC 回收的
- hashcode数据结构变化产生的内存泄漏,当一个对象被存储进HashSet集合中以后修改参与哈希值计算的属性,那么在通过这个对象去HashSet中检索是检索不到的同时也无法删除,所以就造成了内存泄漏
- 内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。所以如果内部类不需要访问包含的类成员,可以考虑将其转换为静态内部类
- 当集合中存储了多个对象,如果某些对象不再使用,但集合所在的对象一直有引用,那么集合中不再使用的那些对象就是内存泄漏,所以成员变量的集合或数组中如果对象元素不再使用的之后可以显示将他赋值为null或删除
总之如果一个对象不再使用但是还是跟某个引用链相关联,那么这个对象就属于内存泄漏
配置参数
java中配置的参数有很多可以分为:
- 内存相关可以对堆、栈、元空间或持久代进行配置
- 垃圾收集器相关的可以选择垃圾收集器以及配置垃圾收集器
- 日志辅助信息相关的可以选择输出一些辅助信息如打印GC相关信息、保存错误信息到指定文件等等
常用的内存配置:
- Xms 堆区初始值。例如:-Xms1024m或-Xms1g
- Xmx 堆区最大值。例如:-Xmx1024m或-Xmx1g。可以与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
- Xmn 堆中年轻代大小。例如:-Xmn1024m或-Xmn1g
- XX:NewRatio 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为-XX:NewRatio=4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
- XX:SurvivorRatio 设置年轻代中Eden区与Survivor区的大小比值。设置为-XX:SurvivorRatio=4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
- Xss 设置每个线程的堆栈大小。例如:-Xss128k
类加载
一个java文件从被加载到被卸载这个生命过程,总共要经历5个阶段:
加载->链接(验证+准备+解析)->初始化->使用->卸载
- 加载
-
加载主要是将java文件编译后的.class文件加载到方法区并且转化为方法区运行时的数据结构,然后在堆中创建相应的class类对象,作为方法区这些数据的访问入口。
-
这里将class文件加载到内存是通过类加载器来实现类的加载的。其中类加载器有这么几个分别是:
- bootstrapLoader启动类加载器他由C++语言实现,属于JVM的一部分,其作用是加载系统变量
sun.boot.class.path所指定的路径或jar,可以通过System.getProperty("sun.boot.class.path")获取,其实就是加载jre/lib目录下的指定jar - extendedLoader扩展类加载器他负责加载系统变量
java.ext.dirs所指定的路径或jar,可以通过System.getProperty("java.ext.dirs")获取,其实就是jre/lib/ext目录下的文件。并且我们还可以在运行的时候指定加载的文件 - AppClassLoader应用程序类加载器他用于加载用户类路径中的文件可以通过
System.getProperty("java.class.path")获取 - 最后就是我们自定义类的加载器。通常我们都是直接使用应用程序类加载器,但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时应用程序类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,然后重写findClass 方法,在findClass方法中读取class文件创建class类对象
- bootstrapLoader启动类加载器他由C++语言实现,属于JVM的一部分,其作用是加载系统变量
-
这些加载器加载的时候他是通过双亲委派机制来进行的。所谓双亲委派机制就是说:每个类加载器都有一个父类加载器(关系:bootstrapLoader<-extendedLoader<-AppClassLoader<-userClassLoader:简单的说就是bootstrapLoader是extendedLoader的父类加载器,而extendedLoader是AppClassLoader的父类加载器,AppClassLoader是我们自定义加载器的父类加载器),类加载器在加载类之前会先去递归的尝试使用自己的父类加载器去加载。如果父类加载器加载失败那么才自己去加载。通过双亲委派机制能很好地解决类加载的统一性问题,他保证了基类都由相同的类加载器加载,这样就避免了同一个字节码文件被多次加载生成不同的 Class 对象的问题
-
类加载的模型除了双亲委派模型还有OSGI模型,OSGI事实上是java模块化标准,他自定义的类加载器,能很好实现模块化和模块的热部署。在OSGI里面,模块(Bundle)之间的依赖关系从传统的上层下层模块依赖转变为平级模块之间的依赖,从而在运行时形成一个网状结构。OSGI模型虽然更灵活但是他比双亲委派模型复杂很多所以也更容易出现死锁以及内存泄漏。这个了解就行,OSGI的加载顺序
- 以java.*开头的类,委派给父类加载器完成
- 否则,将委派列表名单内的类,委派给父类加载器完成
- 否则,Import列表中的类,委派给Export这个类的Bundle类加载完成
- 否则,查找当前Bundle的Classpath 使用自己的类加载器加载
-
- 链接
- 链接里面又分为3个部分分别是:验证、准备、解析。其中验证用于确保被加载的类满足虚拟机的规范、准备用于负责为类的静态成员分配内存,并设置默认初始值。解析将类中的符号引用替换为直接引用,也就是将符号替换为内存地址或偏移量
- 注意在准备接口如果遇到
public static int value=123;,那么value的值将会是0,而不是123,因为这个时候还没开始执行任何java代码,123还是不可见的,而我们所看到的把123赋值给value的putstatic指令是程序被编译后存在于<clinit>()方法中,所以,给value赋值为123是在初始化的时候才会执行的
- 初始化
- 初始化主要是根据加载的class文件给类中的静态数据进行初始化,以及执行类中的静态块,其实就是执行程序的类构造器也就是
<clinit>()方法。这个方法中包含类中静态数据的初始化 - 而初始化的条件是当java程序第一次对类主动引用的时候就会进行初始化,并且如果在初始化的时候如果发现这个类没有被加载那么就会执行类的加载。而主动引用的情况包括
- 通过new来创建对象的时候
- 读取或设置类的静态字段(除了常量(被final修饰的静态变量已在编译期把结果放入常量池))或调用类的静态方法时
- 对类进行反射调用的时候(如Class.forName(String className)),如果类没有初始化就会进行初始化
- 初始化一个类时发现其父类没初始化,则要先初始化其父类。不过对于接口而言子接口初始化的时候并不要求其父接口也完成初始化,只有在真正使用到父接口的时候它才会被初始化
- 含main方法的那个类,jvm启动时,需要指定一个执行主类,jvm先初始化这个类
- 虽然主动引用会引起类的初始化但被动引用是不会引起类的初始化的。初始化完了之后接下来就可以对类进行使用了,当不在使用之后类就会被卸载出虚拟机。被动引用的情况包括:
- 子类调用父类的静态变量以及静态方法的时候子类不会被初始化,只有父类会被初始化
- 通过数组定义来引用类,不会触发类的初始化
- 访问类的常量,不会初始化类
- 初始化主要是根据加载的class文件给类中的静态数据进行初始化,以及执行类中的静态块,其实就是执行程序的类构造器也就是
常见考点
对于类加载有一个很常见的考点那就是类的初始化时类中数据初始化的顺序问题。
类中数据的初始化顺序为:
- 如果有父类则先初始化其父类,然后在初始化子类
- 在类中根据static变量和块出现顺序进行初始化,那个在先执行那个
- 如果有创建对象则,先初始化父类的普通变量和普通代码块,那个在先执行那个,然后调用父类的构造函数。在初始化子类的普通变量和普通代码块,那个在先执行那个,然后调用子类的构造函数
所以执行顺序可总结为:
- 父类静态数据
- 子类静态数据
- 父类普通数据
- 父类构造方法
- 子类普通数据
- 子类构造方法
注意:如果静态数据中有引用对象如果对象未初始化那么就会初始化对象然后执行对象的普通变量然后再执行对象的构造方法,当然如果引用的对象已经初始化了那么就不会在初始化了而是直接执行对象的普通变量然后再执行对象的构造方法
具体可以参考下面示例:
class A {
public static int count1;
{
System.out.println("A 普通代码块");
}
private B b = new B();
static {
System.out.println("A static 块");
}
private static A a = new A();
public static int count2 = 0;
private A() {
System.out.println("A 构造方法");
count1++;
count2++;
}
public static A getInstance() {
return a;
}
}
class B {
static {
System.out.println("B static 块");
}
public B() {
System.out.println("B 构造方法");
}
}
public class Test {
public static void main(String[] args) {
A a = A.getInstance();
System.out.println("count1=" + A.count1 + ",count2=" + A.count2);
}
}
最后输出:
A static 块
A 普通代码块
B static 块
B 构造方法
A 构造方法
count1=1,count2=0
具体流程:
- 首先在main方法中调用了A中的静态方法所以对A进行初始化
- 初始化会先初始化count1什么也没做,因为count1=0是在链接的准备阶段赋的默认值,然后执行静态块输出
A static 块 - 然后初始化a静态变量,但是由于a是A的一个对象所以会创建A的对象,这里创建A对象属于第二次引用所以就不会初始化了,因此不再涉及到静态数据,创建对象首先遇到A的普通代码块因此执行代码块输出
A 普通代码块,然后在执行成员变量b,所以就会创建B的对象,而B属于第一次引用所以需要对B进行初始化因此会执行B的静态块所以输出B static 块,初始化完成之后在创建B的对象所以会执行B的构造方法输出B 构造方法,然后再执行A的构造方法创建A对象所以输出A 构造方法,并且构造方法执行后count1和count2都为1 - 接下来初始化count2这个静态变量,这里将count2赋值为0,所以count2就等于0了,而count1依旧为1,最后初始化完成返回a对象。所以输出结果如上


1253

被折叠的 条评论
为什么被折叠?



