我们常用的虚拟机是HotSpot,除此之外,还有比如OpenJDK、IBM等
一、Java内存结构
★ 栈[本地方法栈和虚拟机栈]:本地方法栈(Native Method Stack) 与虚拟机栈所发挥的作用非常相似,它们的区别不过是虚拟机栈
为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一。
①、每个线程有一个私有的栈,随着线程的创建而创建。在jvm中栈用来存储一些对象的引用、局部变量以及方法中的形参或计算过程的中间数据,
在方法退出后那么这些变量也会被销毁。它的存储比堆快得多,只比CPU里的寄存器慢。
②、栈里面存着的是一种叫“栈帧”的东西,每一个方法对应一个栈帧,方法中的形参,局部变量都放在栈帧中。栈的大小可以固定也可以动态扩展。
当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误。当申请不到空间时,会抛出 OutOfMemoryError。
这里有一个小细节需要注意,catch 捕获的是 Throwable,而不是 Exception。因为StackOverflowError 和 OutOfMemoryError
都不属于 Exception 的子类。
③、如果JVM开启了逃逸分析和标量替换,小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上,直接分配在栈上,可以自动
回收,减轻GC压力,但大对象或者逃逸对象无法栈上分配。逃逸分析和标量替换是JVM的一种优化,默认时开启的,Java没有寄存器,所有参数
传递都是使用操作数栈。
java以栈帧为单位保存线程的运行状态。虚拟机只会对java栈执行两种操作:以栈帧为单位的压栈或者出栈。栈是私有的,是线程安全的
栈帧由三部分组成:局部变量区,操作数栈和帧数据区。
● 栈内存在JVM中默认是1M,可以通过 -Xss 参数进行设置
★ 堆:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配,这部分空间可通过 GC 进行回收。
当申请不到空间时会抛出 OutOfMemoryError。
● 最小堆内存在JVM中默认物理内存的64分之1,用参数 -Xms 设置,最大堆内存在JVM中默认物理内存4分之一,用参数 -Xmx 设置,且建议
最大堆内存不大于4G,并且设置-Xms=-Xmx,避免每次GC后,调整堆的大小,减少系统内存分配开销
★ 方法区(元空间):方法区也是所有线程共享。主要用于存储类的信息、常量池等。 方法区逻辑上属于堆的一部分,
但是为了与堆进行区分,通常又叫“非堆”。
【java 虚拟机规范只是规定了方法区这么个概念和它的作用,并没有规定如何实现它,在其它JVM中不存在永久代,
永久代存在在HOtsPOt虚拟机中,hotspot虚拟机在jdk1.7的时候把方法区叫做永久代,但是由于永久代内存经常不够用或发生内存泄漏,
出现OOM error, 所以jdk1.8把移除永久代,改为元空间,元空间和永久代的本质类似,都是对JVM规范中方法区的实现,
不过元空间和永久代之间最大的区别在于:元空间不在虚拟机中,而是使用本地内存中】
● 在1.7之前,可以使用如下参数来调节方法区的大小
-XX:PermSize 方法区初始大小
-XX:MaxPermSize 方法区最大大小
超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen
● 在jdk8中已经将永久带移除了。就是说-XX:PermSize这些参数在jdk8中将是无效的。新出现的元空间(Metaspace)来代替原来的永久带。
在1.8中,可以使用如下参数来调节方法区的大小
-XX:MetaspaceSize 元空间初始大小
-XX:MaxMetaspaceSize 元空间最大大小
超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: Metadata space
在jdk1.7中抛出的异常是这样:java.lang.OutOfMemoryError: PermGen
★ 程序计数器:是Java运行时数据区中的一小块内存区域
● 程序计数器是线程私有的,也就是说,每一个线程都拥有仅属于自己的程序计数器。
● 当虚拟机执行的方法不是native的时,程序计数器指向虚拟机正在执行字节码指令的地址;
● 当虚拟机执行的方法是native的时,程序计数器中的值是空。因为计数器记录的字节码指令地址,
但是native 本地(如:System.currentTimeMillis())方法是大多是通过C实现并未编译成需要执行的字节码指令,
所以在计数器中当然是空(undefined).
● 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
Java内存结构.png
二、堆内存的构成
1.年轻代:用于存放新产生的对象。
可以使用参数 -Xmn 配置年轻代的大小,jvm中默认Eden区的内存比上survivor的内存等于8。
即:如果年轻代的Xmn配置的100M,那么Eden就会被分配80M内存,每个survivor分配10M内存
2.老年代:用于存放被长期引用的对象。年轻代在垃圾回收多次都没有被GC回收的时候就会被放到老年代,以及一些大的对象
(比如缓存,这里的缓存是弱引用),这些大对象可以不进入年轻代就直接进入老年代
3.持久带:用于存放Class,method元信息。
● survivor是幸存者,堆内存中两个survivor大小是一样的,两个survivor是彼此相互复制的
● 当new出一个对象,如果对象比较大的时候,直接放到tenured(老年代)中,否则放到eden(新生代)中,如果GC垃圾收集器回收了一次,
没有被垃圾收集器回收的幸存者会放到一个survivor中,如果GC再回收一次,把幸存者放到另一个survivor中,
然后把之前survivor中的幸存者复制到这个survivor中.......
堆内存.png
三、堆内存参数的调整
● 用 VisualVM 观察虚拟机堆信息
四、GC如何确定垃圾
▲ 什么是垃圾?
● 没有任何引用指向的对象(除去循环引用)
▲ 如何确定垃圾【JVM内存垃圾收集算法】?
● 引用计数:会有循环引用的问题
● 正向可达:从roots对象计算可以到达的对象【main方法中创建的对象,classLoader等都可以是roots对象】
五、垃圾收集算法
1、Mark-Sweep : 标记清除
★ 标记清除最大的问题就是内存的碎片化严重(可用内存不连续),后续可能发生大对象不能找到可利用空间的问题。
标记清除.png
2、Copying : 复制算法 (用于新生代)
★ 效率高,解决了内存碎片化的问题,同时可以压缩内存,但是浪费内存(所有内存分为两部分,只能使用其中一半的内存)
复制.png
3、Mark-Compact:标记压缩
★ 效率比Cpoy略低,一般用于老年代中
标记压缩.png
六、垃圾收集器选择
JVM采用分代算法
1、new(新生代):
● 存活对象少
● 使用copying算法,占用的内存空间也不大,效率也高
2、old(老年代):
● 垃圾少
● 一般使用 Mark-Compact算法
七、JVM参数
● - :标准参数,所有JVM都应该支持
● -X:非标准参数,每个JVM实现都不同
● -XX:不稳定参数,下一个版本可能会取消
八、JVM中的垃圾收集器
● Serial Collector
XX:+UseSerialGC
单线程
● Parallel Collector
并发量大,不过每次垃圾收集,JVM都会停下来等垃圾收集完再运行执行
● CMS Collector
停顿时间短,JVM会在垃圾收集的同时执行
● G1
不仅停顿时间短,同时并发大
九、Java对象的分配
如果JVM开启了逃逸分析和标量替换,默认是开启的,则会先将对象分配到栈上
▲ 栈上分配
● 线程私有小对象
● 无逃逸
● 支持标量替换
● 无需调整
▲ 线程本地分配TLAB(Thread Local Allacation Buffer)
● 占用eden,默认1%
● 多线程的时候不用竞争eden就可以申请空间,提高效率
● 小对象
● 无需调整
▲ old(老年代)
● 大对象
▲ aden
十、常用参数设置
▲ 堆设置
● -Xms:初始堆大小
● -Xmx:最大堆大小
● -Xss:线程堆大小
● -XX:NewSize=n:设置年轻代大小
● -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。
如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
● -XX:MaxPermSize=n:设置持久代大小
▲ 收集器设置
● -XX:+UseSerialGC:设置串行收集器
● -XX:+UseParallelGC:设置并行收集器
● -XX:+UseConcMarkSweepGC:设置并发收集器
▲ 垃圾回收统计信息
● -XX:+PrintGC
● -XX:+PrintGCDetails
● -Xloggc:filename
十一、调优Tomcat并使用JMeter进行评测
● tomcat 的bin目录下,catalina.bat中进行设置
set JAVA_OPTS=
-Xms4g
-Xmx4g
-Xss512k
-XX:+AggressiveOpts //虚拟机中能用到的优化项都设置上,包括升级后的优化项
-XX:+UseBiasedLocking //优化锁的
-XX:PermSize=64M (Java1.8没有了) //永久区的大小
-XX:MaxPermSize=300M //永久区最大大小
-XX:+DisableExplicitGC //关掉显示调用GC 【System.gc();】
-XX:+UseConcMarkSweepGC //使用CMS缩短响应时间,并发收集,低停顿
-XX:+UseParNewGC //并发收集新生代的垃圾
-XX:+CMSParallelRemarkEnabled //在使用UseParNewGC的情况看下,尽量减少mark的时间
-XX:+UseCMSCompactAtFullCollection //使用并发收集器时,开启对年老代的压缩,使碎片减少
-XX:LargePageSizeInBytes=128m //内存分页大小对性能的提升
-XX:+UseFastAccessorMethods //get/set方法转成本地代码
-Djava.awt.headless=true //修复linux下的tomcat处理图表时可能产生的一个bug
十二、ThreadLocal(ThreadLocal采用了“以空间换时间”的方式)
1、ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。
2、顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,
是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。
网友评论