华为od 面试八股文_Java_03_含答案 拼搏现实的明天。 2024-04-20 14:14 96阅读 0赞 **目录** 1:Java是如何实现跨平台的? 2:什么时候会触发FullGC ? 3:Java中的String 为什么不可变? 4:简述下java中的对象分配规则 5:调优命令你用过哪些? 6:HashMap 的长度为什么是 2 的幂次方? 7:HashMap 为什么线程不安全? 8:synchronized、volatile、CAS 区别? 9:ThreadLocal 用过吗? 10:HashMap和Hashtable有什么区别? -------------------- #### 1:Java是如何实现跨平台的? #### Java是通过JVM(Java虚拟机)实现跨平台的。 JVM可以理解成一个软件,不同的平台有不同的版本。我们编写的Java代码,编译后会生成.class 文件(字节码文件)。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码,通过JVM翻译成机器码之后才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。 只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。 因此,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的翻译才能执行。 #### 2:什么时候会触发FullGC ? #### 除直接调用`System.gc`外,触发Full GC执行的情况有如下四种。 > 1.**旧生代空间不足** > 旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:`java.lang.OutOfMemoryError: Java heap space`为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 > 2.**Permanet Generation空间满** > `PermanetGeneration`中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:`java.lang.OutOfMemoryError: PermGen space` > 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 > 3.**CMS GC时出现promotion failed和concurrent mode failure** > 对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有`promotion failed和concurrent mode failure`两种状况,当这两种状况出现时可能会触发Full GC。promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 > **应对措施为**:**增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作**。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。 > 4.**统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间** > 这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 > 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 > 当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 > 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过`- java Dsun.rmi.dgc.client.gcInterval=3600000`来设置Full GC执行的间隔时间或通过`-XX:+ DisableExplicitGC`来禁止RMI调用`System.gc`。 #### 3:Java中的String 为什么不可变? #### 先看看什么是不可变的对象。 如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。 接着来看Java8 String类的源码: public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 } 从源码可以看出,String对象其实在内部就是一个个字符,存储在这个value数组里面的。 value数组用final修饰,final 修饰的变量,值不能被修改。因此value不可以指向其他对象。 String类内部所有的字段都是私有的,也就是被private修饰。而且String没有对外提供修改内部状态的方法,因此value数组不能改变。 所以,String是不可变的。 那为什么String要设计成不可变的? 主要有以下几点原因: 1. **线程安全**。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的。 2. **支持hash映射和缓存**。因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性使得 hash 值也不会变,不需要重新计算。 3. **出于安全考虑**。网络地址URL、文件路径path、密码通常情况下都是以String类型保存,假若String不是固定不变的,将会引起各种安全隐患。比如将密码用String的类型保存,那么它将一直留在内存中,直到垃圾收集器把它清除。假如String类不是固定不变的,那么这个密码可能会被改变,导致出现安全隐患。 4. **字符串常量池优化**。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。 既然我们的String是不可变的,它内部还有很多substring, replace, replaceAll这些操作的方法。这些方法好像会改变String对象?怎么解释呢? 其实不是的,我们每次调用replace等方法,其实会在堆内存中创建了一个新的对象。然后其value数组引用指向不同的对象。 #### 4:简述下java中的对象分配规则 #### > 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。 > 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。 > 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。 > 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 > 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。 ![2fdf497bc9934b328efa418fffd6be26.png][] #### 5:调优命令你用过哪些? #### Sun JDK监控和故障处理命令有`jps jstat jmap jhat jstack jinfo` > **jps,JVM Process Status Tool**,显示指定系统内所有的HotSpot虚拟机进程。 > **jstat,JVM statistics Monitoring**是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 > **jmap**,JVM Memory Map命令用于生成heap dump文件 > **jhat**,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看 > **jstack**,用于生成java虚拟机当前时刻的线程快照。 > **jinfo**,`JVM Configuration info` 这个命令作用是实时查看和调整虚拟机运行参数。 #### 6:HashMap 的长度为什么是 2 的幂次方? #### 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。这个算法应该如何设计呢?我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。 #### 7:HashMap 为什么线程不安全? #### JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和数据丢失的问题。数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全,具体来说会有数据覆盖的风险。举个例子:两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // ... // 判断是否出现 hash 碰撞 // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素(处理hash冲突) else { // ... } 还有一种情况是这两个线程同时 `put` 操作导致 `size` 的值不正确,进而导致数据覆盖的问题: 1. 线程 1 执行 `if(++size > threshold)` 判断时,假设获得 `size` 的值为 10,由于时间片耗尽挂起。 2. 线程 2 也执行 `if(++size > threshold)` 判断,获得 `size` 的值也为 10,并将元素插入到该桶位中,并将 `size` 的值更新为 11。 3. 随后,线程 1 获得时间片,它也将元素放入桶位中,并将 size 的值更新为 11。 4. 线程 1、2 都执行了一次 `put` 操作,但是 `size` 的值只增加了 1,也就导致实际上只有一个元素被添加到了 `HashMap` 中。 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // ... // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null; } #### 8:synchronized、volatile、CAS 区别? #### (1) synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。 (2) volatile 提供多线程共享变量可见性和禁止指令重排序优化。 (3) CAS 是基于冲突检测的乐观锁(非阻塞) #### 9:ThreadLocal 用过吗? #### ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用, 特别适用于各个线程依赖不通的变量值完成操作的场景。简单说 ThreadLocal 就是一种以空间 换 时 间 的 做 法 , 在 每 个 Thread 里 面 维 护 了 一 个 以 开 地 址 法 实 现 的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。 #### 10:**HashMap和Hashtable有什么区别?** #### 1. HashMap是Hashtable的轻量级实现,HashMap允许key和value为null,但最多允许一条记录的key为null.而HashTable不允许。 2. HashTable中的方法是线程安全的,而HashMap不是。在多线程访问HashMap需要提供额外的同步机制。 3. Hashtable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。 ![e339711c453f45849339c1992dab0fa8.png][] [2fdf497bc9934b328efa418fffd6be26.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/b54269d52af949509e2e887e76ef77c3.png [e339711c453f45849339c1992dab0fa8.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/a34777c203f546bf82b71b2df423bcab.png
相关 华为od 面试八股文_Java_03_含答案 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,... 拼搏现实的明天。/ 2024年04月20日 14:14/ 0 赞/ 97 阅读
还没有评论,来说两句吧...