JVM内存分配机制详解
1. JVM内存分配机制(TODO)A a = new A()过程分为:1. 类加载检查,如果加载过进行下一步,没有加载过使用类加载器加载类2. 分配内存3. 初始化4. 设置对象头5. 执行init方法1.1 jvm分配内存jvm分配内存方式:1.指针碰撞:将一块内存分配为已使用和未使用,中间分配使用指针分隔,分配内存就是将指针移动到与对象大小相同的内存块处。2.空闲列表:一般内存中可能有不相邻
目录
1. 对象创建流程(TODO)
A a = new A()过程 分为:
1. 类加载检查,如果加载过进行下一步,没有加载过使用类加载器加载类
2. 分配内存
3. 初始化
4. 设置对象头
5. 执行init方法
1.1 jvm分配内存
jvm分配内存方式:
1.指针碰撞:将一块内存分配为已使用和未使用,中间分配使用指针分隔,分配内存就是将指针移动到与对象大小相同的内存块处。
2.空闲列表:一般内存中可能有不相邻的内存块,虚拟机会维护一个列表,标记哪些是使用过的内存和未使用的内存。分配内存的时候会更新这个列表。
jvm解决分配内存并发问题的方法:
1.CAS+失败重试的方式:cas就是判断是否是否相同,如果相同更新的方式,没有更新成功,还有失败重试机制。
2.TLAB:将分配内存的动作按照线程划分为在不同空间,每个空间预先分配一段内存。默认使用-XX:+/-UseTLAB参数设定是否开启TLAB,默认jdk1.8开启,使用-XX:TLABSIZE指定TLAB大小
1.2 设置对象头
对象在内存中分为对象头,实例数据,对齐填充。
对象头:
1.mark Word
2.Klass Pointer指针(堆中对象指向方法区的类信息)
3.数组长度
1.2.1 对象头实例
打印对象头实战:
1.引入pom.xml
<!-- 引入jol包--> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
import org.openjdk.jol.info.ClassLayout;
public class Math {
public static void main(String[] args) {
//SIZE 前8位markword 中间4位jclass Pointer指针指向方法区类信息 最后4位对齐填充8的整数位
//总共16字节
ClassLayout classLayout = ClassLayout.parseInstance(new Object());
System.out.println(classLayout.toPrintable());
System.out.println("----------------------");
//SIZE 前8位markword 中间4位jclass Pointer指针指向方法区类信息 4位数组才有 8位数组大小,1个int占4个字节
//总共24字节
ClassLayout classLayout1 = ClassLayout.parseInstance(new int[]{1,2});
System.out.println(classLayout1.toPrintable());
System.out.println("----------------------");
//SIZE 前8位markword 中间4位jclass Pointer指针指向方法区类信息 4位int占4个字节
//1位 byte 3位对齐整数4填充位 4位string对象占4位 4位Object对象占4位 4位填充位
//总共32字节
ClassLayout classLayout2 = ClassLayout.parseInstance(new A());
System.out.println(classLayout2.toPrintable());
}
}
class A{
int a; //占4个字节
String name; //占4个字节
byte c; //占1个字节,默认会对齐填充4的整数位,补齐后面3位(alignment/padding gap)
Object o ;//占4个字节
}
hostspot源码对象内存分配如下:
1.2.2 指针压缩
jdk1.6以后默认开启指针压缩,主要对对象头Kclass Pointer指针进行压缩,以得到压缩内存的目的,帮助减少内存压力。
-XX:-UseCompressedOops(默认开启)
-XX:-UseCompressedOops(关闭指针压缩)
-XX:+UseCompressedClassPointers(默认只开启压缩对象头里的KClass Pointer指针)
为什么要进行指针压缩?
在早期32位操作系统中,内存最多可以占用2的32次方数据,如果使用64位系统,内存可以有2的64次方数据,在进行指针压缩以后将超过2的32次方数据进行压缩可以放入内存,能存储更多数据,在拿到CPU寄存器寻址时会进行解码操作还原数据本来内存大小。
在内存小于4G时,默认不开启指针压缩。
如果内存大于32G,默认使用8字节进行寻址,压缩指针会失效,因为64位操作系统使用32位,内存会多出1.5倍,会导致占用较大带宽,GC承受较大压力,所以互联网一般不采用超过内存32G的服务器。
2.JVM对象内存分配
2.1 逃逸分析和标量替换
逃逸分析:一个对象在方法参数列表里面,在方法返回值里面返回,就代表这个对象逃逸了。如果只能在某个方法内使用,代表对象没有逃出方法区,jdk7以后默认开启逃逸分析,我们可以使用-XX:-DoEscapeAnalysis关闭逃逸分析。非逃逸的对象在方法区分配内存的时候默认分配内存如果不超过桟帧大小可以放入桟空间中,大大减少了堆内存的压力。
标量替换:在对象确定不能逃逸以后,并且对象还可以分解成成员变量时,jvm就不会创建对象,而是将对象的成员变量由方法使用的成员变量替换,方法使用的成员变量由寄存器或者桟帧中分配内存,这样可以存在不连续的内存空间上。jdk7以后默认开启,可以使用-XX:+EliminateAllocations空间,+号代表开启,-号代表关闭。
public class EscapeAnalysis {
/**
* A对象逃逸出getA方法
* @return
*/
public A getA(){
A a = new A();
a.setName("逃逸分析");
return a;
}
/**
* A对象未逃逸出getB方法
* @return
*/
public void getB(){
A a = new A();
a.setName("逃逸分析");
}
}
class A{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2.1.1 逃逸分析和标量替换实战
/**
* 1.默认会开启逃逸分析和标量替换
* -Xmx15M -Xms15M -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* 2.关闭逃逸分析
* -Xmx15M -Xms15M -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* 3.关闭标量替换
* -Xmx15M -Xms15M -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class Test {
/**
* A对象未逃逸出getB方法
* @return
*/
public static void getB(){
A a = new A();
a.setName("逃逸分析");
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for(int i = 0;i < 100000000;i++){
getB();
}
long end = System.currentTimeMillis();
System.out.println("占用时间:"+(end - start)/1000+"秒");
}
}
class A{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2.2 eden区分配内存过程
经过测试得知eden区默认63.5M内存,在57.6M时占满Eden区内存,不知道中间为什么会有一部分空间没有占用仍然显示100%的情况。在eden区占满以后往S0区移动,大对象会往老年代放入,后续的对象继续往eden区放入。
/**
* eden区分配内存
* -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
byte[] obj1,obj2,obj3,obj4,obj5;
obj1 = new byte[59000*1024];//eden区默认63.5M内存,在57.6M时占满Eden区内存
obj2 = new byte[8000*1024];//进行mimor gc将大对象往老年代移动一部分,S0区移动一部分
obj3 = new byte[8000*1024];//下面三个对象继续往Eden区放入
obj4 = new byte[8000*1024];
obj5 = new byte[8000*1024];
}
}
2.3 大对象分配进入老年代
在使用serial或ParNew垃圾回收器时可以设置,下面使用serial垃圾回收器
-XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC -XX:+PrintGCDetails参数来让大对象直接进入老年代。
/**
* eden区分配内存
* -XX:+PrintGCDetails 打印GC详细内容
* -XX:PretenureSizeThreshold=1000000 单位字节 -XX:+UseSerialGC --在使用serial年轻代垃圾回收器时可以设置大对象参数
* -XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC -XX:+PrintGCDetails
* --大对象会直接进入老年代
*/
public class GCTest {
public static void main(String[] args) {
byte[] obj1;
obj1 = new byte[8000*1024];//直接将大对象放入老年代
}
}
2.4 长期存活的对象进入老年代
正常年轻代中对象的年龄要达到15次就要进入老年代,但是我们可以通过设置参数来让这个年龄提前进入老年代。不同的垃圾回收器年龄可能不同,CMS垃圾回收器默认6岁。
-XX:MaxTenuringThreshold设置回收年龄
2.5 动态年龄进入老年代
在eden占满以后,发生Minor GC,会把对象从eden挪动到s0区,如果对象超过s0区的一半,会直接进入老年代。公式:年龄>=1的对象内存总和大于s0区的50%就会进入老年代。
2.6 老年代空间分配担保机制
简单说:就是每次再进入minor GC之前会判断是否要进行一次Full GC,这样就可以减少Minor GC,通过设置-XX:-HandlePromotionFailure来实现。
2.5 计算生产模拟系统JVM参数设置
如下计算后的JVM内存参数建议如下:
java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar jar包
如果要设置eden,s0,s1区比例,需要设置-XX:+UseAdaptiveSizePolicy(默认开启),参数会默认开启,动态扩容年轻代比例。如果要关闭使用-XX:+UseAdaptiveSizePolicy参数
3.对象内存回收机制
程序计数器法:jdk默认不使用,当一个对象被其他对象引用程序技术器就会加1,如果对象=null没有引用时减1,但是可能会出现A引用B,B引用A导致对象无法回收的情况。
GC Roots引用计数法:从GC Roots往下引用到其他对象形成一条引用链,当下面没有引用时对象被标记为垃圾,当下一次垃圾回收时会被垃圾回收器回收。常用的GC Roots:本地方法桟中引用的对象,方法区中的常量,静态变量等。
java的4大引用:强,虚,软,弱。
强引用:new 对象。这样的对象即使发生异常也不会被强制回收。
弱引用:new SoftReference<User>new User();应用场景:当一个页面需要回退时会返回到上一个页面,当发生GC时会回收掉这个对象。
虚引用和软引用几乎不用,忽略。
3.1 finalize方法判断对象是否存活
1.第一次判断是否覆盖了finalize方法没有直接将没有与GC Roots关联的对象直接回收。
2.使用finalize方法可以进行对象的自救,比如将对象赋值给引用链上的某一个类变量或常量。
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 测试使用finallize方法进行对象的自救
*/
public class User {
private int id ;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
//对象自救方法
// OOMTest.list.add(this);
System.out.println("id为:"+id+"的对象即将被回收!");
}
}
class OOMTest{
public static List<User> list = new ArrayList<User>();
public static void main(String[] args) {
List<User> strings = new ArrayList<User>();
int i = 0,j = 0;
while(true){
strings.add(new User(i++, UUID.randomUUID().toString()));
new User(j--,UUID.randomUUID().toString());
}
}
}
3.2 如何判断一个类是一个无用的类
1.java中堆和堆指向方法区的指针都被回收
2.类的类加载器被回收掉,应用场景:tomcat中的大量自定义加载器比如webappClassLoader和jspClassLoader都应该被回收掉。
3.java中不存在引用的对象,如果存在还是会通过反射生成相应的类。
更多推荐
所有评论(0)