天天看點

面試阿裡必備:JVM記憶體是如何配置設定的?

自己經驗有限,篇幅也有限,這裡隻是記錄一些比較容易混淆或有難度和一些易忘的技術知識點,裡面有一些也是面試阿裡經常會被問到的問題,但是不保證答案全部正确,有錯誤的地方望大家指正

JVM相關

  1. JVM記憶體是如何配置設定的? 堆:占用記憶體最大的區塊,主要存放new出來的對象,線程共享,主要設定大小參數名是-Xms和-Xmx 棧(以前概括都叫棧,具體說其實是非堆記憶體):一般是線程私有 寄存器:即是程式計數器,存放目前正要執行的下一條指令位址本地方法棧(不同jvm的實作可能不同,比如平常所用的sun的實作中方法棧和虛拟機棧是一個):線程私有,存儲比如Object的hashCode();虛拟機棧中用于存儲局部變量表、動态連結、操作數、方法出口等資訊方法區:所有線程共享,用于存放加載類資訊,比如常量、靜态常量,需要注意的是1.8以後将靜态常量放在了堆裡
  2. GC 垃圾回收的算法基礎是标記和複制算法,标記一般是樹形結構,采用根搜尋算法,标記可以回收的對象,一般是對象搜尋不到根節點即可以回收,有2次機會,在第一次被标記回收後可以重新被挂靠根節點(也即是被重新引用,涉及的的方法是finalize()),如果沒有下一次判定對象死亡;基礎的複制算法我舉個例子說明,将一塊記憶體分成2份,運作時隻使用其中一塊,GC時将活的對象複制到另一塊記憶體,然後清除前一塊所有記憶體空間,類似于給U盤格式化,這樣比一個一個釋放記憶體要快得多,相信大家做格式化的時候體會過,現在jvm gc使用的的複制算法是結果改良的,不是平均的分成2份,預設比例好像是1/8,即平常見到的新生代、老年代、持久代等。具體的算法大家看資料文檔吧,這種東西不是說說就能清楚的。
  3. 記憶體洩漏和記憶體溢出 記憶體洩漏,當一個對象不會被使用但占着記憶體即會導緻記憶體洩漏,比如
Object o1 = new Object();
Object o2 = new Object();
o1 = o2; // 這時2個對象的引用位址是一樣的,但是o1申請的記憶體就沒有被使用           

複制

記憶體洩漏積累多了,記憶體不斷被無用對象占用,新的對象申請不到足夠的空間就會産生記憶體溢出。

架構相關

  1. springMVC的處理流程 一個http請求經過一些過濾器或攔截器到達DispatcherServlet将請求轉發給對應的@Controller和@RequestMapping 參數封裝,請求頭判定等等調用業務方法獲得Model等傳回ModelAndView查找ViewResolver傳回對應的View,可能是需要渲染的jsp,可能是json,可能是檔案流等等。
  2. 說說redis裡的bitmap bitmap一般用于計數或top計算,比如統計網站目前線上人數,假設使用者id是遞增的整數,當使用者上線時将使用者id存進bitmap,比如id是4,則bitmap就是00001000,id為8的使用者上線,bitmap的值變成10001000,對bitmap做count計算得出是2,而1KB=1024B=8196b且位運算是計算機最快的,這樣做的好處是速度快,還能知道是誰上下線的。同理如果要按月統計某個操作,隻需要用每天做key值,然後做并集得到新的bitset計數就可。
  3. Ioc和AOP分别使用了哪些設計模式? 工廠模式和代理模式,細一點還有單例、模版、原型,這裡說一下代理模式,常用的一般是動态代理模式,jdk中提供了InvocationHandler接口可以友善實作動态代理:
public interface IService {
    void service();
}
public class MyService implements IService {
    @Override
    public void service() {
        System.out.println("service...");
    }
}
public class MyProxy implements InvocationHandler {

    private IService service;

    public MyProxy(IService service) {
        this.service = service;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before service");
        Object invoke = method.invoke(service, args);
        System.out.println("after service ");
        return invoke;
    }
}
public class Main {

    public static void main(String[] args) {
        MyService myService = new MyService();
        MyProxy myProxy = new MyProxy(myService);

        // 動态生成代理對象
        IService instance = (IService) Proxy.newProxyInstance(MyService.class.getClassLoader(), MyService.class.getInterfaces(), myProxy);
        instance.service();// 利用代理對象調用service方法

    }
}           

複制

  1. MySQL資料庫的鎖 MySQL常見的有2種引擎,MyISAM采用的是表級鎖,給整表加鎖,速度快,不會死鎖,但是但是鎖競争激烈,效率低,lock table_name read or write;InnoDB采用的是行級鎖,隻鎖行,效率高,但是會死鎖,另外MySQL的行級鎖采用鎖索引實作,是以隻有通過索引檢索資料才能使用行級鎖,否則會使用表級鎖
  2. 幂等性 幂等性是數學裡的一個概念,我并不是很精通,簡單來說就是N次變換和1次變換的結果應該保持一緻,計算機裡他是一種Http協定中提到的性質,注意幂等性本身并不是協定,沒有辦法通過規範一緻化操作,多用于分布式系統,用于保證分布式系統中資料的一緻性操作,類似于分布式事務,但是分布式事務中間件一般較重且效率有很大虧損,對于要求高性能的分布式場景中,幂等設計可能是唯一的選擇。實作場景簡單舉例來說,假設微信支付後端是分布式的(肯定是的),我發起了一個支付,如果伺服器端已經處理完成但是我的手機沒網了,我會誤以為支付失敗,重新支付,幂等設計在此類場景中一般會這樣設計,在發起支付操作前會先向服務端申請一個ticket,這個ticket會關聯此次支付的操作,這個ticket隻能增長一次,這樣在我重新發起支付的時候,伺服器就可以正确傳回支付成功且保證我隻支付了一次。

java基礎

  1. ConcurrentHashMap ConcurrentHashMap是線程安全的集合類,功能類似于Hashtable,但是Hashtable雖然也是線程安全的,但是Hashtable隻有一把同步鎖,并發性能不高,ConcurrentHashMap則是利用了鎖分段技術,簡單來說就是,多個類似的HashTable,單獨維護自己的鎖,這樣多線程操作的時候減少了競争鎖的等待,在多線程應用裡是最常用的線程安全集合類。檢視源碼可以知道,ConcurrentHashMap内部主要成員是Segment和Node,Segment充當鎖,繼承ReentrantLock,Node相當于一個Map.Entry,其他大部分成員變量都是volatile的,因為happen before的存在,volatile字段的寫入操作先于讀操作,這也是用volatile替換鎖的經典應用場景。
  2. ThreadLocal ThreadLocal,利用線程局部變量來實作線程安全的方式,使用時需要小心應對,因為線程局部變量一旦使用完沒有被釋放就會導緻記憶體洩漏。
  3. 有沒有可能兩個不相等的對象有相同的 hashcode? hashcode并不是唯一的,隻是重複機率非常小而已,但是相等的對象hashcode一定是一樣的。
  4. 編寫多線程程式的時候你需要注意哪些? 盡量使用volatile替換同步鎖給線程取個name使用并發集合而不是讓集合同步合理建立線程數,一般而言是CPU的核心數*2+1給需要同步的代碼同步,而不是圖簡單給整個方法或類加同步
  5. DateFormat的所有實作都不是線程安全的,如果一定要在多線程中使用可以利用ThreadLocal
  6. 對稱加密和非對稱加密 對稱加密:需要同一把密鑰來解密,速度快,一般用于需要加密大量資料時使用,常見用于對稱加密的算法有DES、3DES、RC系、AES等。 非對稱加密:需要2把密鑰才能解密,分作公鑰和私鑰,如果用公開密鑰對資料進行加密,隻有用對應的私有密鑰才能解密;如果用私有密鑰對資料進行加密,那麼隻有用對應的公開密鑰才能解密;常見的https協定裡的證書機制就是采用的這種方式,常用于非對稱加密算法的有RSA、ECC、Elgamal。
  7. NIO和普通IO的差別? 最主要的差別在于非阻塞與阻塞,NIO是先寫入緩沖區在再讀出操作,是非阻塞的,而普通IO操作主要是針對流的,一個線程讀寫流時是不能做其他操作的,就好比如下載下傳檔案有些軟體可以斷點續傳,有些不可以。

問題

  1. 統計log檔案裡所有出現的單詞以及出現的次數并且按照次數排序找出最頻繁的單詞? 步驟其實很簡單: 讀取檔案排序 這裡直接提供代碼,分别是jdk 1.7和jdk 1.8的2個版本 1.7:
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

public class Test {

    /**
     * 根據map的value進行排序
     * @param map
     * @param <K>
     * @param <V>
     * @return
     */
    public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
        List<Map.Entry<K, V>> list = new LinkedList<Map.Entry<K, V>>(map.entrySet());
        // 先将map轉換成List便于使用sort排序
        Collections.sort(list, new Comparator<Map.Entry<K, V>>() {
            public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
                return (o2.getValue()).compareTo(o1.getValue());
            }
        });

        Map<K, V> result = new LinkedHashMap<K, V>();
        for (Map.Entry<K, V> entry : list) {
            result.put(entry.getKey(), entry.getValue());
        }
        return result;
    }

    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        String fileName = "ngen.log";
        TreeMap<String, Integer> map = new TreeMap<>();
//      SortedMap<String, Integer> map = new
        try {
            fileInputStream = new FileInputStream(fileName);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
            String line = null;
            // 按行讀取檔案分解單詞
            while ((line = bufferedReader.readLine()) != null) {
                String[] ss = line.split(" ");
                for (int i = 0; i < ss.length; i++) {
                    String s = ss[i];
                    if (s != null && s.matches("\\w+")) {
                        // 如果map中有此單詞就将次數+1
                        // 否則此單詞第一次出現
                        if (map.containsKey(s)) {
                            map.put(s, map.get(s) + 1);
                        } else {
                            map.put(s, 1);
                        }
                    }
                }
            }

            Map<String, Integer> sortedMap = sortByValue(map);
            for (Map.Entry<String, Integer> entry : sortedMap.entrySet()) {
                String key = entry.getKey();
                Integer value = entry.getValue();
                System.out.println(key + ":" + value);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}           

複制

1.8:

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {

    /**
     * 根據map的value進行排序
     * @param map
     * @param <K>
     * @param <V>
     * @return
     */
    public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
        return map.entrySet()
                .stream()
                .sorted(Map.Entry.comparingByValue(Collections.reverseOrder())) // 逆序
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (e1, e2) -> e1,
                        LinkedHashMap::new
                ));
    }

    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        String fileName = "D:/app.log";
        TreeMap<String, Integer> map = new TreeMap<>();
        try {
            fileInputStream = new FileInputStream(fileName);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
            bufferedReader.lines()
                    .flatMap(line -> Stream.of(line.split(" ")))
                    .filter(word -> word.matches("\\w+"))
                    .forEach(s -> { // Stream文法不太熟悉,不知道有木有更友善的方法?
                        if (map.containsKey(s)) {
                            map.put(s, map.get(s) + 1);
                        } else {
                            map.put(s, 1);
                        }
                    })
            ;

            Map<String, Integer> sortedMap = sortByValue(map);
            sortedMap.forEach((k, v) -> System.out.println(k + "," + v));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}           

複制

  1. 從日志檔案中讀取的是位元組還是字元? 乍一看這問題不要太簡單,但是我挺佩服問這問題的面試官的,這問題向後衍生無論是廣度還是深度都無可挑剔。如何回答這問題要從兩方面出發,首先一點,所有作業系統存放在磁盤的任何資料肯定都是位元組的,那麼讀出來肯定也是位元組的;第二點,通常日志檔案操作寫代碼的時候讀出來的肯定是字元,不然你如何操作呢?隻是java提供的友善的I/O操作方法而已,其實裡面是将位元組轉成了字元而已。向後延伸就會問比如java的IO操作注意事項、編碼等等問題,還有作業系統底層如何處理等等的問題,這道題很簡單,但是切記不要盲目作答。
  2. 秒殺系統設計 自己并沒有實際秒殺系統設計經驗,這裡從朋友以及網絡總結幾點: 高并發,總的來說肯定是Nginx做負載均衡,背景做服務叢集秒殺計時前的靜态頁面使用cdn秒殺計時不需要做高并發處理,因為new一個Date傳回給前台,任何語言支援個幾億并發都是沒問題的先緩存再查庫,保證低延遲秒殺系統單獨設計,不要與已有業務混淆,不然一旦阻塞會全盤崩潰庫存要保持事務唯一,資料庫最好另建表,不要與日常業務沖突預估請求處理最大量,當請求過多時攔截并直接傳回等待預防惡意刷單,比如同一個IP隻能有一個請求動态加載js來激活秒殺按鈕,避免秒殺沒有開始時被惡意操作并發請求隊列

本文已在版權印備案,如需轉載請通路版權印。40142943

本文參與 騰訊雲自媒體分享計劃 ,歡迎熱愛寫作的你一起參與!

本文分享自作者個人站點/部落格:https://www.jianshu.com/u/de13d6c70d6c複制

如有侵權,請聯系 [email protected] 删除。